Progress
This commit is contained in:
@@ -1,86 +0,0 @@
|
||||
use rustyline::completion::{Candidate, Completer};
|
||||
|
||||
use crate::{expand::cmdsub::expand_cmdsub_string, parse::lex::KEYWORDS, prelude::*};
|
||||
|
||||
use super::readline::SynHelper;
|
||||
|
||||
impl<'a> Completer for SynHelper<'a> {
|
||||
type Candidate = String;
|
||||
fn complete( &self, line: &str, pos: usize, ctx: &rustyline::Context<'_>,) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
|
||||
let mut shenv = self.shenv.clone();
|
||||
let mut comps = vec![];
|
||||
shenv.new_input(line);
|
||||
let mut token_stream = Lexer::new(line.to_string(), &mut shenv).lex();
|
||||
if let Some(comp_token) = token_stream.pop() {
|
||||
let raw = comp_token.as_raw(&mut shenv);
|
||||
let is_cmd = if let Some(token) = token_stream.pop() {
|
||||
match token.rule() {
|
||||
TkRule::Sep => true,
|
||||
_ if KEYWORDS.contains(&token.rule()) => true,
|
||||
_ => false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if let TkRule::Ident | TkRule::Whitespace = comp_token.rule() {
|
||||
if is_cmd {
|
||||
let cmds = shenv.meta().path_cmds();
|
||||
comps.extend(cmds.iter().map(|cmd| cmd.to_string()));
|
||||
comps.retain(|cmd| cmd.starts_with(&raw));
|
||||
if !comps.is_empty() && comps.len() > 1 {
|
||||
if get_bin_path("fzf", &self.shenv).is_some() {
|
||||
if let Some(mut selection) = fzf_comp(&comps, &mut shenv) {
|
||||
while selection.starts_with(&raw) {
|
||||
selection = selection.strip_prefix(&raw).unwrap().to_string();
|
||||
}
|
||||
comps = vec![selection];
|
||||
}
|
||||
}
|
||||
} else if let Some(mut comp) = comps.pop() {
|
||||
while comp.starts_with(&raw) {
|
||||
comp = comp.strip_prefix(&raw).unwrap().to_string();
|
||||
}
|
||||
comps = vec![comp];
|
||||
}
|
||||
return Ok((pos,comps))
|
||||
} else {
|
||||
let (start, matches) = self.file_comp.complete(line, pos, ctx)?;
|
||||
comps.extend(matches.iter().map(|c| c.display().to_string()));
|
||||
|
||||
if !comps.is_empty() && comps.len() > 1 {
|
||||
if get_bin_path("fzf", &self.shenv).is_some() {
|
||||
if let Some(selection) = fzf_comp(&comps, &mut shenv) {
|
||||
return Ok((start, vec![selection]))
|
||||
} else {
|
||||
return Ok((start, vec![]))
|
||||
}
|
||||
} else {
|
||||
return Ok((start, comps))
|
||||
}
|
||||
} else if let Some(comp) = comps.pop() {
|
||||
// Slice off the already typed bit
|
||||
return Ok((start, vec![comp]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((pos,comps))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fzf_comp(comps: &[String], shenv: &mut ShEnv) -> Option<String> {
|
||||
// All of the fzf wrapper libraries suck
|
||||
// So we gotta do this now
|
||||
let echo_args = comps.join("\n");
|
||||
let echo = format!("echo \"{echo_args}\"");
|
||||
let fzf = "fzf --height=~30% --layout=reverse --border --border-label=completion";
|
||||
let command = format!("{echo} | {fzf}");
|
||||
|
||||
shenv.ctx_mut().set_flag(ExecFlags::NO_EXPAND); // Prevent any pesky shell injections with filenames like '$(rm -rf /)'
|
||||
let selection = expand_cmdsub_string(&command, shenv).ok()?;
|
||||
if selection.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(selection.trim().to_string())
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
use rustyline::highlight::Highlighter;
|
||||
use sys::get_bin_path;
|
||||
|
||||
use crate::{parse::lex::KEYWORDS, prelude::*};
|
||||
|
||||
use super::readline::SynHelper;
|
||||
|
||||
impl<'a> Highlighter for SynHelper<'a> {
|
||||
fn highlight<'l>(&self, line: &'l str, pos: usize) -> std::borrow::Cow<'l, str> {
|
||||
let mut shenv_clone = self.shenv.clone();
|
||||
shenv_clone.new_input(line);
|
||||
|
||||
let mut result = String::new();
|
||||
let mut tokens = Lexer::new(line.to_string(),&mut shenv_clone).lex().into_iter();
|
||||
let mut is_command = true;
|
||||
let mut in_array = false;
|
||||
let mut in_case = false;
|
||||
|
||||
while let Some(token) = tokens.next() {
|
||||
let raw = token.as_raw(&mut shenv_clone);
|
||||
match token.rule() {
|
||||
TkRule::Comment => {
|
||||
let styled = &raw.styled(Style::BrightBlack);
|
||||
result.push_str(&styled);
|
||||
}
|
||||
TkRule::ErrPipeOp |
|
||||
TkRule::OrOp |
|
||||
TkRule::AndOp |
|
||||
TkRule::PipeOp |
|
||||
TkRule::RedirOp |
|
||||
TkRule::BgOp => {
|
||||
is_command = true;
|
||||
let styled = &raw.styled(Style::Cyan);
|
||||
result.push_str(&styled);
|
||||
}
|
||||
TkRule::CasePat => {
|
||||
let pat = raw.trim_end_matches(')');
|
||||
let len_delta = raw.len().saturating_sub(pat.len());
|
||||
let parens = ")".repeat(len_delta);
|
||||
let styled = pat.styled(Style::Magenta);
|
||||
let rebuilt = format!("{styled}{parens}");
|
||||
result.push_str(&rebuilt);
|
||||
}
|
||||
TkRule::FuncName => {
|
||||
let name = raw.strip_suffix("()").unwrap_or(&raw);
|
||||
let styled = name.styled(Style::Cyan);
|
||||
let rebuilt = format!("{styled}()");
|
||||
result.push_str(&rebuilt);
|
||||
}
|
||||
TkRule::DQuote | TkRule::SQuote => {
|
||||
let styled = raw.styled(Style::BrightYellow);
|
||||
result.push_str(&styled);
|
||||
}
|
||||
_ if KEYWORDS.contains(&token.rule()) => {
|
||||
if in_array || in_case {
|
||||
if &raw == "in" {
|
||||
let styled = &raw.styled(Style::Yellow);
|
||||
result.push_str(&styled);
|
||||
if in_case { in_case = false };
|
||||
} else {
|
||||
let styled = &raw.styled(Style::Magenta);
|
||||
result.push_str(&styled);
|
||||
}
|
||||
} else {
|
||||
if &raw == "for" {
|
||||
in_array = true;
|
||||
}
|
||||
if &raw == "case" {
|
||||
in_case = true;
|
||||
}
|
||||
let styled = &raw.styled(Style::Yellow);
|
||||
result.push_str(&styled);
|
||||
}
|
||||
}
|
||||
TkRule::BraceGrp => {
|
||||
let body = &raw[1..raw.len() - 1];
|
||||
let highlighted = self.highlight(body, 0).to_string();
|
||||
let styled_o_brace = "{".styled(Style::BrightBlue);
|
||||
let styled_c_brace = "}".styled(Style::BrightBlue);
|
||||
let rebuilt = format!("{styled_o_brace}{highlighted}{styled_c_brace}");
|
||||
|
||||
is_command = false;
|
||||
result.push_str(&rebuilt);
|
||||
}
|
||||
TkRule::CmdSub => {
|
||||
let body = &raw[2..raw.len() - 1];
|
||||
let highlighted = self.highlight(body, 0).to_string();
|
||||
let styled_o_paren = "$(".styled(Style::BrightBlue);
|
||||
let styled_c_paren = ")".styled(Style::BrightBlue);
|
||||
let rebuilt = format!("{styled_o_paren}{highlighted}{styled_c_paren}");
|
||||
|
||||
is_command = false;
|
||||
result.push_str(&rebuilt);
|
||||
}
|
||||
TkRule::Subshell => {
|
||||
let body = &raw[1..raw.len() - 1];
|
||||
let highlighted = self.highlight(body, 0).to_string();
|
||||
let styled_o_paren = "(".styled(Style::BrightBlue);
|
||||
let styled_c_paren = ")".styled(Style::BrightBlue);
|
||||
let rebuilt = format!("{styled_o_paren}{highlighted}{styled_c_paren}");
|
||||
|
||||
is_command = false;
|
||||
result.push_str(&rebuilt);
|
||||
}
|
||||
TkRule::VarSub => {
|
||||
let styled = raw.styled(Style::Magenta);
|
||||
result.push_str(&styled);
|
||||
}
|
||||
TkRule::Ident => {
|
||||
if in_array || in_case {
|
||||
if &raw == "in" {
|
||||
let styled = &raw.styled(Style::Yellow);
|
||||
result.push_str(&styled);
|
||||
if in_case { in_case = false };
|
||||
} else {
|
||||
let styled = &raw.styled(Style::Magenta);
|
||||
result.push_str(&styled);
|
||||
}
|
||||
} else if let Some((var,val)) = raw.split_once('=') {
|
||||
let var_styled = var.styled(Style::Magenta);
|
||||
let val_styled = val.styled(Style::Cyan);
|
||||
let rebuilt = vec![var_styled,val_styled].join("=");
|
||||
result.push_str(&rebuilt);
|
||||
} else if raw.starts_with(['"','\'']) {
|
||||
let styled = &raw.styled(Style::BrightYellow);
|
||||
result.push_str(&styled);
|
||||
} else if &raw == "{" || &raw == "}" {
|
||||
result.push_str(&raw);
|
||||
|
||||
} else if is_command {
|
||||
if get_bin_path(&token.as_raw(&mut shenv_clone), self.shenv).is_some() ||
|
||||
self.shenv.logic().get_alias(&raw).is_some() ||
|
||||
self.shenv.logic().get_function(&raw).is_some() ||
|
||||
BUILTINS.contains(&raw.as_str()) {
|
||||
let styled = &raw.styled(Style::Green);
|
||||
result.push_str(&styled);
|
||||
|
||||
} else {
|
||||
let styled = &raw.styled(Style::Red | Style::Bold);
|
||||
result.push_str(&styled);
|
||||
}
|
||||
|
||||
is_command = false;
|
||||
|
||||
} else {
|
||||
result.push_str(&raw);
|
||||
}
|
||||
}
|
||||
TkRule::Sep => {
|
||||
is_command = true;
|
||||
in_array = false;
|
||||
result.push_str(&raw);
|
||||
}
|
||||
_ => {
|
||||
result.push_str(&raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::borrow::Cow::Owned(result)
|
||||
}
|
||||
|
||||
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
|
||||
&'s self,
|
||||
prompt: &'p str,
|
||||
default: bool,
|
||||
) -> std::borrow::Cow<'b, str> {
|
||||
let _ = default;
|
||||
std::borrow::Cow::Borrowed(prompt)
|
||||
}
|
||||
|
||||
fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> {
|
||||
std::borrow::Cow::Borrowed(hint)
|
||||
}
|
||||
|
||||
fn highlight_candidate<'c>(
|
||||
&self,
|
||||
candidate: &'c str, // FIXME should be Completer::Candidate
|
||||
completion: rustyline::CompletionType,
|
||||
) -> std::borrow::Cow<'c, str> {
|
||||
let _ = completion;
|
||||
std::borrow::Cow::Borrowed(candidate)
|
||||
}
|
||||
|
||||
fn highlight_char(&self, line: &str, pos: usize, kind: rustyline::highlight::CmdKind) -> bool {
|
||||
let _ = (line, pos, kind);
|
||||
true
|
||||
}
|
||||
}
|
||||
260
src/prompt/history.rs
Normal file
260
src/prompt/history.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
use std::{fs::{File, OpenOptions}, ops::{Deref, DerefMut}, path::PathBuf};
|
||||
|
||||
use bitflags::bitflags;
|
||||
use rustyline::history::{History, SearchResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prelude::*};
|
||||
|
||||
#[derive(Deserialize,Serialize,Debug)]
|
||||
pub struct HistEntry {
|
||||
body: String,
|
||||
id: usize
|
||||
}
|
||||
|
||||
impl HistEntry {
|
||||
pub fn new(body: String, id: usize) -> Self {
|
||||
Self { body, id }
|
||||
}
|
||||
pub fn cmd(&self) -> &str {
|
||||
&self.body
|
||||
}
|
||||
pub fn id(&self) -> usize {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize,Serialize,Default)]
|
||||
pub struct HistEntries {
|
||||
entries: Vec<HistEntry>
|
||||
}
|
||||
|
||||
impl HistEntries {
|
||||
pub fn new() -> Self {
|
||||
Self { entries: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for HistEntries {
|
||||
type Target = Vec<HistEntry>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.entries
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for HistEntries {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.entries
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FernHist {
|
||||
file_path: Option<PathBuf>,
|
||||
entries: HistEntries,
|
||||
max_len: usize,
|
||||
pub flags: HistFlags
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
pub struct HistFlags: u32 {
|
||||
const NO_DUPES = 0b0000001;
|
||||
const IGNORE_SPACE = 0b0000010;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'e> FernHist {
|
||||
pub fn new() -> Self {
|
||||
Self { file_path: None, entries: HistEntries::new(), max_len: 1000, flags: HistFlags::empty() }
|
||||
}
|
||||
pub fn from_path(file_path: PathBuf) -> ShResult<'e,Self> {
|
||||
let mut new_hist = FernHist::new();
|
||||
new_hist.file_path = Some(file_path);
|
||||
new_hist.load_hist()?;
|
||||
Ok(new_hist)
|
||||
}
|
||||
pub fn create_entry(&self, body: &str) -> HistEntry {
|
||||
let id = self.len() + 1;
|
||||
HistEntry::new(body.to_string(), id)
|
||||
}
|
||||
pub fn init_hist_file(&mut self) -> ShResult<'e,()> {
|
||||
let Some(path) = self.file_path.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
self.save(&path)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn load_hist(&mut self) -> ShResult<'e,()> {
|
||||
let Some(file_path) = self.file_path.clone() else {
|
||||
return Err(
|
||||
ShErr::simple(
|
||||
ShErrKind::InternalErr,
|
||||
"History file not set"
|
||||
)
|
||||
)
|
||||
};
|
||||
if !file_path.is_file() {
|
||||
self.init_hist_file()?;
|
||||
}
|
||||
let hist_file = File::open(&file_path)?;
|
||||
self.entries = serde_yaml::from_reader(hist_file).unwrap_or_default();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Default for FernHist {
|
||||
fn default() -> Self {
|
||||
let home = std::env::var("HOME").unwrap();
|
||||
let file_path = PathBuf::from(&format!("{home}/.fernhist"));
|
||||
Self::from_path(file_path).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl History for FernHist {
|
||||
fn add(&mut self, line: &str) -> rustyline::Result<bool> {
|
||||
let new_entry = self.create_entry(line);
|
||||
if self.flags.contains(HistFlags::NO_DUPES) {
|
||||
let most_recent = self.get(self.len(), rustyline::history::SearchDirection::Reverse)?.unwrap();
|
||||
dbg!(&most_recent);
|
||||
if new_entry.body == most_recent.entry.to_string() {
|
||||
return Ok(false)
|
||||
}
|
||||
}
|
||||
self.entries.push(new_entry);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn get(&self, index: usize, dir: rustyline::history::SearchDirection) -> rustyline::Result<Option<rustyline::history::SearchResult>> {
|
||||
Ok(self.entries.iter().find(|ent| ent.id() == index).map(|ent| {
|
||||
SearchResult { entry: ent.cmd().to_string().into(), idx: index, pos: 0 }
|
||||
}))
|
||||
}
|
||||
|
||||
fn add_owned(&mut self, line: String) -> rustyline::Result<bool> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
fn set_max_len(&mut self, len: usize) -> rustyline::Result<()> {
|
||||
self.max_len = len;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ignore_dups(&mut self, yes: bool) -> rustyline::Result<()> {
|
||||
if yes {
|
||||
self.flags |= HistFlags::NO_DUPES;
|
||||
} else {
|
||||
self.flags &= !HistFlags::NO_DUPES;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ignore_space(&mut self, yes: bool) {
|
||||
if yes {
|
||||
self.flags |= HistFlags::IGNORE_SPACE;
|
||||
} else {
|
||||
self.flags &= !HistFlags::IGNORE_SPACE;
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&mut self, path: &std::path::Path) -> rustyline::Result<()> {
|
||||
let hist_file = File::create(path)?;
|
||||
serde_yaml::to_writer(hist_file, &self.entries).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append(&mut self, path: &std::path::Path) -> rustyline::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn load(&mut self, path: &std::path::Path) -> rustyline::Result<()> {
|
||||
let path = path.to_path_buf();
|
||||
self.file_path = Some(path);
|
||||
self.load_hist().map_err(|_| rustyline::error::ReadlineError::Io(std::io::Error::last_os_error()))
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> rustyline::Result<()> {
|
||||
self.entries.clear();
|
||||
if self.file_path.is_some() {
|
||||
self.save(&self.file_path.clone().unwrap())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
term: &str,
|
||||
start: usize,
|
||||
dir: rustyline::history::SearchDirection,
|
||||
) -> rustyline::Result<Option<rustyline::history::SearchResult>> {
|
||||
if term.is_empty() {
|
||||
return Ok(None)
|
||||
}
|
||||
let mut matches: Vec<&HistEntry> = self.entries.iter()
|
||||
.filter(|ent| is_subsequence(&ent.body, term))
|
||||
.collect();
|
||||
|
||||
matches.sort_by(|ent_a, ent_b| {
|
||||
let ent_a_rank = fuzzy_rank(term, &ent_a.body);
|
||||
let ent_b_rank = fuzzy_rank(term, &ent_b.body);
|
||||
ent_a_rank.cmp(&ent_b_rank)
|
||||
.then(ent_a.id().cmp(&ent_b.id()))
|
||||
});
|
||||
|
||||
Ok(matches.last().map(|ent| {
|
||||
SearchResult {
|
||||
entry: ent.body.clone().into(),
|
||||
idx: ent.id(),
|
||||
pos: start
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn starts_with(
|
||||
&self,
|
||||
term: &str,
|
||||
start: usize,
|
||||
dir: rustyline::history::SearchDirection,
|
||||
) -> rustyline::Result<Option<rustyline::history::SearchResult>> {
|
||||
let mut matches: Vec<&HistEntry> = self.entries.iter()
|
||||
.filter(|ent| ent.body.starts_with(term))
|
||||
.collect();
|
||||
|
||||
matches.sort_by(|ent_a, ent_b| ent_a.id().cmp(&ent_b.id()));
|
||||
dbg!(&matches);
|
||||
Ok(matches.first().map(|ent| {
|
||||
SearchResult {
|
||||
entry: ent.body.clone().into(),
|
||||
idx: ent.id(),
|
||||
pos: start
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn fuzzy_rank(search_term: &str, search_result: &str) -> u8 {
|
||||
if search_result == search_term {
|
||||
4
|
||||
} else if search_result.starts_with(search_term) {
|
||||
3
|
||||
} else if search_result.contains(search_term) {
|
||||
2
|
||||
} else if is_subsequence(search_result, search_term) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a search term is a subsequence of the body (characters in order but not necessarily adjacent)
|
||||
fn is_subsequence(search_result: &str, search_term: &str) -> bool {
|
||||
let mut result_chars = search_result.chars();
|
||||
search_term.chars().all(|ch| result_chars.any(|c| c == ch))
|
||||
}
|
||||
@@ -1,59 +1,38 @@
|
||||
use crate::prelude::*;
|
||||
use readline::SynHelper;
|
||||
use rustyline::{config::Configurer, history::{DefaultHistory, History}, ColorMode, CompletionType, Config, EditMode, Editor};
|
||||
|
||||
pub mod history;
|
||||
pub mod readline;
|
||||
pub mod highlight;
|
||||
pub mod validate;
|
||||
pub mod comp;
|
||||
|
||||
fn init_rl<'a>(shenv: &'a mut ShEnv) -> Editor<SynHelper<'a>, DefaultHistory> {
|
||||
let hist_path = std::env::var("FERN_HIST").unwrap_or_default();
|
||||
let config = Config::builder()
|
||||
.max_history_size(1000).unwrap()
|
||||
.history_ignore_dups(true).unwrap()
|
||||
.completion_prompt_limit(100)
|
||||
.edit_mode(EditMode::Vi)
|
||||
.color_mode(ColorMode::Enabled)
|
||||
.tab_stop(2)
|
||||
.build();
|
||||
use std::path::Path;
|
||||
|
||||
let mut editor = Editor::with_config(config).unwrap();
|
||||
editor.set_completion_type(CompletionType::List);
|
||||
editor.set_helper(Some(SynHelper::new(shenv)));
|
||||
if !hist_path.is_empty() {
|
||||
editor.load_history(&PathBuf::from(hist_path)).unwrap();
|
||||
}
|
||||
editor
|
||||
use history::FernHist;
|
||||
use readline::FernReadline;
|
||||
use rustyline::{error::ReadlineError, history::{FileHistory, History}, Config, Editor};
|
||||
|
||||
use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prelude::*};
|
||||
|
||||
fn init_rl<'s>() -> ShResult<'s,Editor<FernReadline,FernHist>> {
|
||||
let hist = FernHist::default();
|
||||
let rl = FernReadline::new();
|
||||
let config = Config::default();
|
||||
let mut editor = Editor::with_history(config,hist)?;
|
||||
editor.set_helper(Some(rl));
|
||||
Ok(editor)
|
||||
}
|
||||
|
||||
pub fn read_line(shenv: &mut ShEnv) -> ShResult<String> {
|
||||
log!(TRACE, "Entering prompt");
|
||||
shenv.meta_mut().stop_timer();
|
||||
let ps1 = std::env::var("PS1").unwrap_or("\\$ ".styled(Style::Green | Style::Bold));
|
||||
let prompt = expand_prompt(&ps1,shenv)?;
|
||||
let mut editor = init_rl(shenv);
|
||||
pub fn read_line<'s>() -> ShResult<'s,String> {
|
||||
let mut editor = init_rl()?;
|
||||
let prompt = "$ ".styled(Style::Green | Style::Bold);
|
||||
match editor.readline(&prompt) {
|
||||
Ok(line) => {
|
||||
if !line.is_empty() {
|
||||
let hist_path = std::env::var("FERN_HIST").ok();
|
||||
editor.history_mut().add(&line).unwrap();
|
||||
if let Some(path) = hist_path {
|
||||
editor.history_mut().save(&PathBuf::from(path)).unwrap();
|
||||
}
|
||||
editor.add_history_entry(&line)?;
|
||||
editor.save_history(&Path::new("/home/pagedmov/.fernhist"))?;
|
||||
}
|
||||
Ok(line)
|
||||
},
|
||||
Err(rustyline::error::ReadlineError::Eof) => {
|
||||
kill(Pid::this(), Signal::SIGQUIT)?;
|
||||
Ok(String::new())
|
||||
}
|
||||
Err(rustyline::error::ReadlineError::Interrupted) => {
|
||||
Ok(String::new())
|
||||
}
|
||||
Err(ReadlineError::Eof) => std::process::exit(0),
|
||||
Err(ReadlineError::Interrupted) => Ok(String::new()),
|
||||
Err(e) => {
|
||||
log!(ERROR, e);
|
||||
Err(e.into())
|
||||
return Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,66 @@
|
||||
use rustyline::{completion::{Candidate, Completer, FilenameCompleter}, hint::{Hint, Hinter}, history::{History, SearchDirection}, Helper};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::prelude::*;
|
||||
use rustyline::{completion::Completer, highlight::Highlighter, hint::{Hint, Hinter}, validate::{ValidationResult, Validator}, Helper};
|
||||
|
||||
pub struct SynHelper<'a> {
|
||||
pub file_comp: FilenameCompleter,
|
||||
pub shenv: &'a mut ShEnv,
|
||||
use crate::{libsh::term::{Style, Styled}, prelude::*};
|
||||
|
||||
pub struct FernReadline {
|
||||
}
|
||||
|
||||
impl<'a> Helper for SynHelper<'a> {}
|
||||
|
||||
impl<'a> SynHelper<'a> {
|
||||
pub fn new(shenv: &'a mut ShEnv) -> Self {
|
||||
Self {
|
||||
file_comp: FilenameCompleter::new(),
|
||||
shenv,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hist_search(&self, term: &str, hist: &dyn History) -> Option<String> {
|
||||
let limit = hist.len();
|
||||
let mut latest_match = None;
|
||||
for i in 0..limit {
|
||||
if let Some(hist_entry) = hist.get(i, SearchDirection::Forward).ok()? {
|
||||
if hist_entry.entry.starts_with(term) {
|
||||
latest_match = Some(hist_entry.entry.into_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
latest_match
|
||||
impl FernReadline {
|
||||
pub fn new() -> Self {
|
||||
Self { }
|
||||
}
|
||||
}
|
||||
|
||||
impl Helper for FernReadline {}
|
||||
|
||||
impl Completer for FernReadline {
|
||||
type Candidate = String;
|
||||
}
|
||||
|
||||
|
||||
pub struct SynHint {
|
||||
text: String,
|
||||
pub struct FernHint {
|
||||
raw: String,
|
||||
styled: String
|
||||
}
|
||||
|
||||
impl SynHint {
|
||||
pub fn new(text: String) -> Self {
|
||||
let styled = (&text).styled(Style::BrightBlack);
|
||||
Self { text, styled }
|
||||
}
|
||||
pub fn empty() -> Self {
|
||||
Self { text: String::new(), styled: String::new() }
|
||||
impl FernHint {
|
||||
pub fn new(raw: String) -> Self {
|
||||
let styled = (&raw).styled(Style::Dim | Style::BrightBlack);
|
||||
Self { raw, styled }
|
||||
}
|
||||
}
|
||||
|
||||
impl Hint for SynHint {
|
||||
impl Hint for FernHint {
|
||||
fn display(&self) -> &str {
|
||||
&self.styled
|
||||
&self.styled
|
||||
}
|
||||
fn completion(&self) -> Option<&str> {
|
||||
if !self.text.is_empty() {
|
||||
Some(&self.text)
|
||||
if !self.raw.is_empty() {
|
||||
Some(&self.raw)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Hinter for SynHelper<'a> {
|
||||
type Hint = SynHint;
|
||||
impl Hinter for FernReadline {
|
||||
type Hint = FernHint;
|
||||
fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<Self::Hint> {
|
||||
if line.is_empty() {
|
||||
return None
|
||||
}
|
||||
let history = ctx.history();
|
||||
let result = self.hist_search(line, history)?;
|
||||
let window = result[line.len()..].trim_end().to_string();
|
||||
Some(SynHint::new(window))
|
||||
let ent = ctx.history().search(line, pos, rustyline::history::SearchDirection::Reverse).ok()??;
|
||||
let entry_raw = ent.entry.get(pos..)?.to_string();
|
||||
Some(FernHint::new(entry_raw))
|
||||
}
|
||||
}
|
||||
|
||||
impl Highlighter for FernReadline {
|
||||
fn highlight<'l>(&self, line: &'l str, pos: usize) -> std::borrow::Cow<'l, str> {
|
||||
Cow::Owned(line.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Validator for FernReadline {
|
||||
fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result<rustyline::validate::ValidationResult> {
|
||||
Ok(ValidationResult::Valid(None))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
use rustyline::validate::{ValidationResult, Validator};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use super::readline::SynHelper;
|
||||
|
||||
pub fn check_delims(line: &str) -> bool {
|
||||
let mut delim_stack = vec![];
|
||||
let mut chars = line.chars();
|
||||
let mut case_depth: u64 = 0;
|
||||
let mut case_check = String::new();
|
||||
let mut in_quote = None; // Tracks which quote type is open (`'` or `"`)
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
case_check.push(ch);
|
||||
if case_check.ends_with("case") {
|
||||
case_depth += 1;
|
||||
}
|
||||
if case_check.ends_with("esac") {
|
||||
case_depth = case_depth.saturating_sub(1);
|
||||
}
|
||||
match ch {
|
||||
'{' | '(' | '[' if in_quote.is_none() => delim_stack.push(ch),
|
||||
'}' if in_quote.is_none() && delim_stack.pop() != Some('{') => return false,
|
||||
')' if in_quote.is_none() && delim_stack.pop() != Some('(') => {
|
||||
if case_depth == 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
']' if in_quote.is_none() && delim_stack.pop() != Some('[') => return false,
|
||||
'"' | '\'' => {
|
||||
if in_quote == Some(ch) {
|
||||
in_quote = None;
|
||||
} else if in_quote.is_none() {
|
||||
in_quote = Some(ch);
|
||||
}
|
||||
}
|
||||
'\\' => { chars.next(); } // Skip next character if escaped
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
delim_stack.is_empty() && in_quote.is_none()
|
||||
}
|
||||
|
||||
pub fn check_keywords(line: &str, shenv: &mut ShEnv) -> bool {
|
||||
shenv.new_input(line);
|
||||
let tokens = Lexer::new(line.to_string(),shenv).lex();
|
||||
Parser::new(tokens, shenv).parse().is_ok()
|
||||
}
|
||||
|
||||
impl<'a> Validator for SynHelper<'a> {
|
||||
fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result<rustyline::validate::ValidationResult> {
|
||||
let input = ctx.input();
|
||||
let mut shenv_clone = self.shenv.clone();
|
||||
match check_delims(input) && check_keywords(input, &mut shenv_clone) {
|
||||
true => Ok(ValidationResult::Valid(None)),
|
||||
false => Ok(ValidationResult::Incomplete),
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user