diff --git a/Cargo.lock b/Cargo.lock index ee5739c..01c5fe6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,7 +47,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.59.0", + "windows-sys", ] [[package]] @@ -58,7 +58,7 @@ checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", "once_cell", - "windows-sys 0.59.0", + "windows-sys", ] [[package]] @@ -119,15 +119,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" -[[package]] -name = "clipboard-win" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" -dependencies = [ - "error-code", -] - [[package]] name = "colorchoice" version = "1.0.3" @@ -143,7 +134,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "windows-sys 0.59.0", + "windows-sys", ] [[package]] @@ -158,39 +149,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" -[[package]] -name = "endian-type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" - -[[package]] -name = "errno" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "error-code" -version = "3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" - -[[package]] -name = "fd-lock" -version = "4.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" -dependencies = [ - "cfg-if", - "rustix", - "windows-sys 0.52.0", -] - [[package]] name = "fern" version = "0.1.0" @@ -202,7 +160,7 @@ dependencies = [ "nix", "pretty_assertions", "regex", - "rustyline", + "unicode-width", ] [[package]] @@ -217,15 +175,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "insta" version = "1.42.2" @@ -257,33 +206,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "log" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" - [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - [[package]] name = "nix" version = "0.29.0" @@ -350,16 +278,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radix_trie" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" -dependencies = [ - "endian-type", - "nibble_vec", -] - [[package]] name = "regex" version = "1.11.1" @@ -389,65 +307,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustyline" -version = "15.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f" -dependencies = [ - "bitflags", - "cfg-if", - "clipboard-win", - "fd-lock", - "home", - "libc", - "log", - "memchr", - "nix", - "radix_trie", - "rustyline-derive", - "unicode-segmentation", - "unicode-width", - "utf8parse", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustyline-derive" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327e9d075f6df7e25fbf594f1be7ef55cf0d567a6cb5112eeccbbd51ceb48e0d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" -[[package]] -name = "smallvec" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" - [[package]] name = "strsim" version = "0.11.1" @@ -471,12 +336,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "unicode-width" version = "0.2.0" @@ -489,15 +348,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index b4b899b..409109c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ insta = "1.42.2" nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] } pretty_assertions = "1.4.1" regex = "1.11.1" -rustyline = { version = "15.0.0", features = [ "derive" ] } +unicode-width = "0.2.0" [[bin]] name = "fern" diff --git a/src/libsh/error.rs b/src/libsh/error.rs index 2206599..c0ec7f0 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -303,12 +303,6 @@ impl From for ShErr { } } -impl From for ShErr { - fn from(value: rustyline::error::ReadlineError) -> Self { - ShErr::simple(ShErrKind::ParseErr, value.to_string()) - } -} - impl From for ShErr { fn from(value: Errno) -> Self { ShErr::simple(ShErrKind::Errno, value.to_string()) diff --git a/src/prompt/highlight.rs b/src/prompt/highlight.rs index 82f6ff5..e69de29 100644 --- a/src/prompt/highlight.rs +++ b/src/prompt/highlight.rs @@ -1,248 +0,0 @@ -use std::{env, mem, os::unix::fs::PermissionsExt, path::{Path, PathBuf}, sync::Arc}; -use crate::builtin::BUILTINS; - -use rustyline::highlight::Highlighter; -use crate::{libsh::term::{Style, StyleSet, Styled}, parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, state::read_logic}; - -use super::readline::FernReadline; - -fn is_executable(path: &Path) -> bool { - path.metadata() - .map(|m| m.permissions().mode() & 0o111 != 0) - .unwrap_or(false) -} - -#[derive(Default,Debug)] -pub struct FernHighlighter { - input: String, -} - -impl FernHighlighter { - pub fn new(input: String) -> Self { - Self { - input, - } - } - pub fn highlight_subsh(&self, token: Tk) -> String { - if token.flags.contains(TkFlags::IS_SUBSH) { - let raw = token.as_str(); - Self::hl_subsh_raw(raw) - } else if token.flags.contains(TkFlags::IS_CMDSUB) { - let raw = token.as_str(); - Self::hl_cmdsub_raw(raw) - } else { - unreachable!() - } - } - pub fn hl_subsh_raw(raw: &str) -> String { - let mut body = &raw[1..]; - let mut closed = false; - if body.ends_with(')') { - body = &body[..body.len() - 1]; - closed = true; - } - let sub_hl = FernHighlighter::new(body.to_string()); - let body_highlighted = sub_hl.hl_input(); - let open_paren = "(".styled(Style::BrightBlue); - let close_paren = ")".styled(Style::BrightBlue); - let mut result = format!("{open_paren}{body_highlighted}"); - if closed { - result.push_str(&close_paren); - } - result - } - pub fn hl_cmdsub_raw(raw: &str) -> String { - let mut body = &raw[2..]; - let mut closed = false; - if body.ends_with(')') { - body = &body[..body.len() - 1]; - closed = true; - } - let sub_hl = FernHighlighter::new(body.to_string()); - let body_highlighted = sub_hl.hl_input(); - let dollar_paren = "$(".styled(Style::BrightBlue); - let close_paren = ")".styled(Style::BrightBlue); - let mut result = format!("{dollar_paren}{body_highlighted}"); - if closed { - result.push_str(&close_paren); - } - result - } - pub fn hl_command(&self, token: Tk) -> String { - let raw = token.as_str(); - let paths = env::var("PATH") - .unwrap_or_default(); - let mut paths = paths.split(':'); - - let is_in_path = { - loop { - let Some(path) = paths.next() else { - break false - }; - - let mut path = PathBuf::from(path); - path.push(PathBuf::from(raw)); - - if path.is_file() && is_executable(&path) { - break true - }; - } - }; - // TODO: zsh is capable of highlighting an alias red even if it exists, if the command it refers to is not found - // Implement some way to find out if the content of the alias is valid as well - let is_alias_or_function = read_logic(|l| { - l.get_func(raw).is_some() || l.get_alias(raw).is_some() - }); - - let is_builtin = BUILTINS.contains(&raw); - - if is_alias_or_function || is_in_path || is_builtin { - raw.styled(Style::Green) - } else { - raw.styled(Style::Bold | Style::Red) - } - } - pub fn hl_dquote(&self, token: Tk) -> String { - let raw = token.as_str(); - let mut chars = raw.chars().peekable(); - const YELLOW: &str = "\x1b[33m"; - const RESET: &str = "\x1b[0m"; - let mut result = String::new(); - let mut dquote_count = 0; - - result.push_str(YELLOW); - - while let Some(ch) = chars.next() { - match ch { - '\\' => { - result.push(ch); - if let Some(ch) = chars.next() { - result.push(ch); - } - } - '"' => { - dquote_count += 1; - result.push(ch); - if dquote_count >= 2 { - break - } - } - '$' if chars.peek() == Some(&'(') => { - let mut raw_cmd_sub = String::new(); - raw_cmd_sub.push(ch); - raw_cmd_sub.push(chars.next().unwrap()); - let mut cmdsub_count = 1; - - while let Some(cmdsub_ch) = chars.next() { - match cmdsub_ch { - '\\' => { - raw_cmd_sub.push(cmdsub_ch); - if let Some(ch) = chars.next() { - raw_cmd_sub.push(ch); - } - } - '$' if chars.peek() == Some(&'(') => { - cmdsub_count += 1; - raw_cmd_sub.push(cmdsub_ch); - raw_cmd_sub.push(chars.next().unwrap()); - } - ')' => { - cmdsub_count -= 1; - raw_cmd_sub.push(cmdsub_ch); - if cmdsub_count <= 0 { - let styled = Self::hl_cmdsub_raw(&mem::take(&mut raw_cmd_sub)); - result.push_str(&styled); - result.push_str(YELLOW); - break - } - } - _ => raw_cmd_sub.push(cmdsub_ch) - } - } - if !raw_cmd_sub.is_empty() { - let styled = Self::hl_cmdsub_raw(&mem::take(&mut raw_cmd_sub)); - result.push_str(&styled); - result.push_str(YELLOW); - } - } - _ => result.push(ch) - } - } - - result.push_str(RESET); - - result - } - pub fn hl_input(&self) -> String { - let mut output = self.input.clone(); - - // TODO: properly implement highlighting for unfinished input - let lex_results = LexStream::new(Arc::new(output.clone()), LexFlags::LEX_UNFINISHED); - let mut tokens = vec![]; - - for result in lex_results { - let Ok(token) = result else { - return self.input.clone(); - }; - tokens.push(token) - } - - // Reverse the tokens, because we want to highlight from right to left - // Doing it this way allows us to trust the spans in the tokens throughout the entire process - let tokens = tokens.into_iter() - .rev() - .collect::>(); - for token in tokens { - match token.class { - _ if token.flags.intersects(TkFlags::IS_CMDSUB | TkFlags::IS_SUBSH) => { - let styled = self.highlight_subsh(token.clone()); - output.replace_range(token.span.start..token.span.end, &styled); - } - TkRule::Str => { - if token.flags.contains(TkFlags::IS_CMD) { - let styled = self.hl_command(token.clone()); - output.replace_range(token.span.start..token.span.end, &styled); - } else if is_dquote(&token) { - let styled = self.hl_dquote(token.clone()); - output.replace_range(token.span.start..token.span.end, &styled); - } else { - output.replace_range(token.span.start..token.span.end, &token.to_string()); - } - } - TkRule::Pipe | - TkRule::ErrPipe | - TkRule::And | - TkRule::Or | - TkRule::Bg | - TkRule::Sep | - TkRule::Redir => self.style_with_token(&token,&mut output,Style::Cyan.into()), - TkRule::CasePattern => self.style_with_token(&token,&mut output,Style::Blue.into()), - TkRule::BraceGrpStart | - TkRule::BraceGrpEnd => self.style_with_token(&token,&mut output,Style::Cyan.into()), - TkRule::Comment => self.style_with_token(&token,&mut output,Style::BrightBlack.into()), - _ => { output.replace_range(token.span.start..token.span.end, &token.to_string()); } - } - } - - output - } - fn style_with_token(&self, token: &Tk, highlighted: &mut String, style: StyleSet) { - let styled = token.to_string().styled(style); - highlighted.replace_range(token.span.start..token.span.end, &styled); - } -} - -impl Highlighter for FernReadline { - fn highlight<'l>(&self, line: &'l str, _pos: usize) -> std::borrow::Cow<'l, str> { - let highlighter = FernHighlighter::new(line.to_string()); - std::borrow::Cow::Owned(highlighter.hl_input()) - } - fn highlight_char(&self, _line: &str, _pos: usize, _kind: rustyline::highlight::CmdKind) -> bool { - true - } -} - -fn is_dquote(token: &Tk) -> bool { - let raw = token.as_str(); - raw.starts_with('"') -} diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index 6d52879..0c6bea4 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -3,89 +3,25 @@ pub mod highlight; use std::path::Path; -use readline::FernReadline; -use rustyline::{error::ReadlineError, history::FileHistory, ColorMode, Config, Editor}; +use readline::FernReader; use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, state::read_shopts}; /// Initialize the line editor -fn init_rl() -> ShResult> { - let rl = FernReadline::new(); - - let tab_stop = read_shopts(|s| s.prompt.tab_stop); - let edit_mode = read_shopts(|s| s.prompt.edit_mode).into(); - let bell_style = read_shopts(|s| s.core.bell_style).into(); - let ignore_dups = read_shopts(|s| s.core.hist_ignore_dupes); - let comp_limit = read_shopts(|s| s.prompt.comp_limit); - let auto_hist = read_shopts(|s| s.core.auto_hist); - let max_hist = read_shopts(|s| s.core.max_hist); - let color_mode = match read_shopts(|s| s.prompt.prompt_highlight) { - true => ColorMode::Enabled, - false => ColorMode::Disabled, - }; - - let config = Config::builder() - .tab_stop(tab_stop) - .indent_size(1) - .edit_mode(edit_mode) - .bell_style(bell_style) - .color_mode(color_mode) - .history_ignore_dups(ignore_dups).unwrap() - .completion_prompt_limit(comp_limit) - .auto_add_history(auto_hist) - .max_history_size(max_hist).unwrap() - .build(); - - let mut editor = Editor::with_config(config).unwrap(); - - editor.set_helper(Some(rl)); - editor.load_history(&Path::new("/home/pagedmov/.fernhist"))?; - Ok(editor) -} - fn get_prompt() -> ShResult { let Ok(prompt) = env::var("PS1") else { // username@hostname // short/path/to/pwd/ // $ - let default = "\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$ "; + let default = "\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m "; return Ok(format!("\n{}",expand_prompt(default)?)) }; Ok(format!("\n{}",expand_prompt(&prompt)?)) } -fn get_hist_path() -> ShResult { - if let Ok(path) = env::var("FERN_HIST") { - Ok(PathBuf::from(path)) - } else { - let home = env::var("HOME")?; - let path = PathBuf::from(format!("{home}/.fernhist")); - Ok(path) - } - -} - pub fn read_line() -> ShResult { - assert!(isatty(STDIN_FILENO).unwrap()); - let mut editor = init_rl()?; let prompt = get_prompt()?; - match editor.readline(&prompt) { - Ok(line) => { - if !line.is_empty() { - let hist_path = get_hist_path()?; - editor.add_history_entry(&line)?; - editor.save_history(&hist_path)?; - } - Ok(line) - } - Err(ReadlineError::Eof) => { - kill(Pid::this(), Signal::SIGQUIT)?; - Ok(String::new()) - } - Err(ReadlineError::Interrupted) => Ok(String::new()), - Err(e) => { - Err(e.into()) - } - } + let mut reader = FernReader::new(prompt); + reader.readline() } diff --git a/src/prompt/readline.rs b/src/prompt/readline.rs index 4050835..85d5109 100644 --- a/src/prompt/readline.rs +++ b/src/prompt/readline.rs @@ -1,86 +1,295 @@ -use rustyline::{completion::Completer, hint::{Hint, Hinter}, history::SearchDirection, validate::{ValidationResult, Validator}, Helper}; +use std::{arch::asm, os::fd::BorrowedFd}; -use crate::{libsh::term::{Style, Styled}, parse::{lex::{LexFlags, LexStream}, ParseStream}}; -use crate::prelude::*; +use nix::{libc::STDIN_FILENO, sys::termios::{self, Termios}, unistd::read}; +use unicode_width::UnicodeWidthStr; + +use crate::{libsh::error::ShResult, prelude::*}; + +#[derive(Clone,Copy,Debug)] +pub enum Key { + Char(char), + Enter, + Backspace, + Esc, + Up, + Down, + Left, + Right, + Ctrl(char), + Unknown, +} + +#[derive(Debug)] +pub struct Terminal { + stdin: RawFd, + stdout: RawFd, +} + +impl Terminal { + pub fn new() -> Self { + assert!(isatty(0).unwrap()); + Self { + stdin: 0, + stdout: 1, + } + } + fn raw_mode() -> termios::Termios { + // Get the current terminal attributes + let orig_termios = unsafe { termios::tcgetattr(BorrowedFd::borrow_raw(STDIN_FILENO)).expect("Failed to get terminal attributes") }; + + // Make a mutable copy + let mut raw = orig_termios.clone(); + + // Apply raw mode flags + termios::cfmakeraw(&mut raw); + + // Set the attributes immediately + unsafe { termios::tcsetattr(BorrowedFd::borrow_raw(STDIN_FILENO), termios::SetArg::TCSANOW, &raw) } + .expect("Failed to set terminal to raw mode"); + + // Return original attributes so they can be restored later + orig_termios + } + pub fn restore_termios(termios: Termios) { + unsafe { termios::tcsetattr(BorrowedFd::borrow_raw(STDIN_FILENO), termios::SetArg::TCSANOW, &termios) } + .expect("Failed to restore terminal settings"); + } + pub fn with_raw_mode R,R>(func: F) -> R { + let saved = Self::raw_mode(); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func)); + Self::restore_termios(saved); + + match result { + Ok(r) => r, + Err(e) => std::panic::resume_unwind(e) + } + } + pub fn read_byte(&self, buf: &mut [u8]) -> usize { + Self::with_raw_mode(|| { + let ret: usize; + unsafe { + let buf_ptr = buf.as_mut_ptr(); + let len = buf.len(); + asm! ( + "syscall", + in("rax") 0, + in("rdi") self.stdin, + in("rsi") buf_ptr, + in("rdx") len, + lateout("rax") ret, + out("rcx") _, + out("r11") _, + ); + } + ret + }) + } + pub fn write_bytes(&self, buf: &[u8]) { + Self::with_raw_mode(|| { + let _ret: usize; + unsafe { + let buf_ptr = buf.as_ptr(); + let len = buf.len(); + asm!( + "syscall", + in("rax") 1, + in("rdi") self.stdout, + in("rsi") buf_ptr, + in("rdx") len, + lateout("rax") _ret, + out("rcx") _, + out("r11") _, + ); + } + }); + } + pub fn write(&self, s: &str) { + self.write_bytes(s.as_bytes()); + } + pub fn writeln(&self, s: &str) { + self.write(s); + self.write_bytes(b"\r\n"); + } +} + +impl Default for Terminal { + fn default() -> Self { + Self::new() + } +} #[derive(Default,Debug)] -pub struct FernReadline; +pub struct FernReader { + pub term: Terminal, + pub prompt: String, + pub line: LineBuf, + pub editor: EditMode +} -impl FernReadline { +impl FernReader { + pub fn new(prompt: String) -> Self { + Self { + term: Terminal::new(), + prompt, + line: Default::default(), + editor: Default::default() + } + } + fn pack_line(&self) -> String { + self.line + .buffer + .iter() + .collect::() + } + pub fn readline(&mut self) -> ShResult { + self.display_line(false); + loop { + let key = self.read_key().unwrap(); + self.process_key(key); + self.display_line(true); + if let Key::Enter = key { + self.term.write_bytes(b"\r"); + break + } + } + Ok(self.pack_line()) + } + pub fn process_key(&mut self, key: Key) { + match key { + Key::Char(ch) => { + self.line.insert_at_cursor(ch); + } + Key::Enter => { + self.line.insert_at_cursor('\n'); + } + Key::Backspace => self.line.backspace_at_cursor(), + Key::Esc => todo!(), + Key::Up => todo!(), + Key::Down => todo!(), + Key::Left => self.line.move_cursor_left(), + Key::Right => self.line.move_cursor_right(), + Key::Ctrl(ctrl) => todo!(), + Key::Unknown => todo!(), + } + } + fn clear_line(&self) { + let prompt_lines = self.prompt.lines().count(); + let buf_lines = self.line.count_lines().saturating_sub(1); // One of the buffer's lines will overlap with the prompt + let total = prompt_lines + buf_lines; + self.term.write_bytes(b"\r\n"); + for _ in 0..total { + self.term.write_bytes(b"\r\x1b[2K\x1b[1A"); + } + self.term.write_bytes(b"\r\x1b[2K"); + } + fn display_line(&self, refresh: bool) { + if refresh { + self.clear_line(); + } + let mut prompt_lines = self.prompt.lines().peekable(); + let mut last_line_len = 0; + while let Some(line) = prompt_lines.next() { + if prompt_lines.peek().is_none() { + last_line_len = strip_ansi_codes(line).width(); + self.term.write(line); + } else { + self.term.writeln(line); + } + } + self.term.write(&self.pack_line()); + + let cursor_offset = self.line.cursor + last_line_len; + self.term.write_bytes(format!("\r\x1b[{}C", cursor_offset).as_bytes()); + } + fn read_key(&mut self) -> Option { + let mut buf = [0; 3]; + + let n = self.term.read_byte(&mut buf); + if n == 0 { + return None; + } + match buf[0] { + b'\x1b' => { + if n == 3 { + match (buf[1], buf[2]) { + (b'[', b'A') => Some(Key::Up), + (b'[', b'B') => Some(Key::Down), + (b'[', b'C') => Some(Key::Right), + (b'[', b'D') => Some(Key::Left), + _ => Some(Key::Esc), + } + } else { + Some(Key::Esc) + } + } + b'\r' | b'\n' => Some(Key::Enter), + 0x7f => Some(Key::Backspace), + c if (c as char).is_ascii_control() => { + let ctrl = (c ^ 0x40) as char; + Some(Key::Ctrl(ctrl)) + } + c => Some(Key::Char(c as char)) + } + } +} + +#[derive(Default,Debug)] +pub enum EditMode { + Normal, + #[default] + Insert, +} + +#[derive(Default,Debug)] +pub struct LineBuf { + buffer: Vec, + cursor: usize +} + +impl LineBuf { pub fn new() -> Self { - Self + Self::default() } - pub fn search_hist(value: &str, ctx: &rustyline::Context<'_>) -> Option { - let len = ctx.history().len(); - for i in 0..len { - let entry = ctx.history().get(i, SearchDirection::Reverse).unwrap().unwrap(); - if entry.entry.starts_with(value) { - return Some(entry.entry.into_owned()) - } + pub fn count_lines(&self) -> usize { + self.buffer.iter().filter(|&&c| c == '\n').count() + } + pub fn insert_at_cursor(&mut self, ch: char) { + self.buffer.insert(self.cursor, ch); + self.move_cursor_right(); + } + pub fn backspace_at_cursor(&mut self) { + if self.buffer.is_empty() { + return } - None + self.buffer.remove(self.cursor.saturating_sub(1)); + self.move_cursor_left(); + } + pub fn move_cursor_left(&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + pub fn move_cursor_right(&mut self) { + self.cursor = self.cursor.saturating_add(1); } } -impl Helper for FernReadline {} +pub fn strip_ansi_codes(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut chars = s.chars().peekable(); -impl Completer for FernReadline { - type Candidate = String; -} - -pub struct FernHint { - raw: String, - styled: String -} - -impl FernHint { - pub fn new(raw: String) -> Self { - let styled = (&raw).styled(Style::Dim | Style::BrightBlack); - Self { raw, styled } - } -} - -impl Hint for FernHint { - fn display(&self) -> &str { - &self.styled - } - fn completion(&self) -> Option<&str> { - if !self.raw.is_empty() { - Some(&self.raw) + while let Some(c) = chars.next() { + if c == '\x1b' && chars.peek() == Some(&'[') { + // Skip over the escape sequence + chars.next(); // consume '[' + while let Some(&ch) = chars.peek() { + if ch.is_ascii_lowercase() || ch.is_ascii_uppercase() { + chars.next(); // consume final letter + break; + } + chars.next(); // consume intermediate characters + } } else { - None + out.push(c); } } -} - -impl Hinter for FernReadline { - type Hint = FernHint; - fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option { - if line.is_empty() { - return None - } - let ent = Self::search_hist(line,ctx)?; - let entry_raw = ent.get(pos..)?.to_string(); - Some(FernHint::new(entry_raw)) - } -} - -impl Validator for FernReadline { - fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result { - let mut tokens = vec![]; - let tk_stream = LexStream::new(Arc::new(ctx.input().to_string()), LexFlags::empty()); - for tk in tk_stream { - if tk.is_err() { - return Ok(ValidationResult::Incomplete) - } - tokens.push(tk.unwrap()); - } - let nd_stream = ParseStream::new(tokens); - for nd in nd_stream { - if nd.is_err() { - return Ok(ValidationResult::Incomplete) - } - } - Ok(ValidationResult::Valid(None)) - } + out } diff --git a/src/prompt/readline_old.rs b/src/prompt/readline_old.rs new file mode 100644 index 0000000..4050835 --- /dev/null +++ b/src/prompt/readline_old.rs @@ -0,0 +1,86 @@ +use rustyline::{completion::Completer, hint::{Hint, Hinter}, history::SearchDirection, validate::{ValidationResult, Validator}, Helper}; + +use crate::{libsh::term::{Style, Styled}, parse::{lex::{LexFlags, LexStream}, ParseStream}}; +use crate::prelude::*; + +#[derive(Default,Debug)] +pub struct FernReadline; + +impl FernReadline { + pub fn new() -> Self { + Self + } + pub fn search_hist(value: &str, ctx: &rustyline::Context<'_>) -> Option { + let len = ctx.history().len(); + for i in 0..len { + let entry = ctx.history().get(i, SearchDirection::Reverse).unwrap().unwrap(); + if entry.entry.starts_with(value) { + return Some(entry.entry.into_owned()) + } + } + None + } +} + +impl Helper for FernReadline {} + +impl Completer for FernReadline { + type Candidate = String; +} + +pub struct FernHint { + raw: String, + styled: String +} + +impl FernHint { + pub fn new(raw: String) -> Self { + let styled = (&raw).styled(Style::Dim | Style::BrightBlack); + Self { raw, styled } + } +} + +impl Hint for FernHint { + fn display(&self) -> &str { + &self.styled + } + fn completion(&self) -> Option<&str> { + if !self.raw.is_empty() { + Some(&self.raw) + } else { + None + } + } +} + +impl Hinter for FernReadline { + type Hint = FernHint; + fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option { + if line.is_empty() { + return None + } + let ent = Self::search_hist(line,ctx)?; + let entry_raw = ent.get(pos..)?.to_string(); + Some(FernHint::new(entry_raw)) + } +} + +impl Validator for FernReadline { + fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result { + let mut tokens = vec![]; + let tk_stream = LexStream::new(Arc::new(ctx.input().to_string()), LexFlags::empty()); + for tk in tk_stream { + if tk.is_err() { + return Ok(ValidationResult::Incomplete) + } + tokens.push(tk.unwrap()); + } + let nd_stream = ParseStream::new(tokens); + for nd in nd_stream { + if nd.is_err() { + return Ok(ValidationResult::Incomplete) + } + } + Ok(ValidationResult::Valid(None)) + } +} diff --git a/src/shopt.rs b/src/shopt.rs index d9ba848..e5e7480 100644 --- a/src/shopt.rs +++ b/src/shopt.rs @@ -1,6 +1,5 @@ use std::{collections::HashMap, fmt::Display, str::FromStr}; -use rustyline::{config::BellStyle, EditMode}; use crate::{libsh::error::{Note, ShErr, ShErrKind, ShResult}, state::ShFunc}; @@ -29,17 +28,6 @@ impl FromStr for FernBellStyle { } } -impl From for BellStyle { - fn from(val: FernBellStyle) -> Self { - match val { - FernBellStyle::Audible => BellStyle::Audible, - FernBellStyle::Visible => BellStyle::Visible, - FernBellStyle::Disable => BellStyle::None - } - } -} - - impl Display for FernBellStyle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -56,15 +44,6 @@ pub enum FernEditMode { Emacs } -impl From for EditMode { - fn from(val: FernEditMode) -> Self { - match val { - FernEditMode::Vi => EditMode::Vi, - FernEditMode::Emacs => EditMode::Emacs - } - } -} - impl FromStr for FernEditMode { type Err = ShErr; fn from_str(s: &str) -> Result { diff --git a/src/tests/highlight.rs b/src/tests/highlight.rs index 71f9653..8b13789 100644 --- a/src/tests/highlight.rs +++ b/src/tests/highlight.rs @@ -1,27 +1 @@ -use insta::assert_snapshot; - -use crate::prompt::highlight::FernHighlighter; - -use super::super::*; - -#[test] -fn highlight_simple() { - let line = "echo foo bar"; - let styled = FernHighlighter::new(line.to_string()).hl_input(); - assert_snapshot!(styled) -} - -#[test] -fn highlight_cmd_sub() { - let line = "echo foo $(echo bar)"; - let styled = FernHighlighter::new(line.to_string()).hl_input(); - assert_snapshot!(styled) -} - -#[test] -fn highlight_cmd_sub_in_dquotes() { - let line = "echo \"foo $(echo bar) biz\""; - let styled = FernHighlighter::new(line.to_string()).hl_input(); - assert_snapshot!(styled) -}