From 1f2077af1d3513b5fb92fc58ace8ba0b1e1688cc Mon Sep 17 00:00:00 2001 From: pagedmov Date: Tue, 3 Mar 2026 20:39:09 -0500 Subject: [PATCH] Added 'read_key' builtin that allows widget scripts to handle input --- src/builtin/mod.rs | 4 +- src/builtin/read.rs | 118 ++++++++++++++++++++++++++++++++-- src/expand.rs | 22 ++++--- src/libsh/error.rs | 19 ++++++ src/parse/execute.rs | 12 +++- src/parse/lex.rs | 46 ++++++++++--- src/parse/mod.rs | 9 ++- src/readline/keys.rs | 105 ++++++++++++++++++++++++++++++ src/readline/linebuf.rs | 39 ++++++++--- src/readline/mod.rs | 89 +++++++++++++++++++------ src/readline/vimode/visual.rs | 9 +++ src/state.rs | 33 ++++++---- 12 files changed, 433 insertions(+), 72 deletions(-) diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index 4dd16e2..b5674c5 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -27,12 +27,12 @@ pub mod intro; pub mod getopts; pub mod keymap; -pub const BUILTINS: [&str; 45] = [ +pub const BUILTINS: [&str; 46] = [ "echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type", - "getopts", "keymap" + "getopts", "keymap", "read_key" ]; pub fn true_builtin() -> ShResult<()> { diff --git a/src/builtin/read.rs b/src/builtin/read.rs index a128246..ee2a6c6 100644 --- a/src/builtin/read.rs +++ b/src/builtin/read.rs @@ -4,14 +4,10 @@ use nix::{ libc::{STDIN_FILENO, STDOUT_FILENO}, unistd::{isatty, read, write}, }; +use yansi::Paint; use crate::{ - getopt::{Opt, OptSpec, get_opts_from_tokens}, - libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, - parse::{NdRule, Node, execute::prepare_argv}, - procio::borrow_fd, - readline::term::RawModeGuard, - state::{self, VarFlags, VarKind, read_vars, write_vars}, + expand::expand_keymap, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, sys::TTY_FILENO}, parse::{NdRule, Node, execute::prepare_argv}, procio::borrow_fd, readline::term::{KeyReader, PollReader, RawModeGuard}, state::{self, VarFlags, VarKind, read_vars, write_vars} }; pub const READ_OPTS: [OptSpec; 7] = [ @@ -45,6 +41,21 @@ pub const READ_OPTS: [OptSpec; 7] = [ }, // read until delimiter ]; +pub const READ_KEY_OPTS: [OptSpec;3] = [ + OptSpec { + opt: Opt::Short('v'), // var name + takes_arg: true + }, + OptSpec { + opt: Opt::Short('w'), // char whitelist + takes_arg: true + }, + OptSpec { + opt: Opt::Short('b'), // char blacklist + takes_arg: true + } +]; + bitflags! { pub struct ReadFlags: u32 { const NO_ESCAPES = 0b000001; @@ -245,3 +256,98 @@ pub fn get_read_flags(opts: Vec) -> ShResult { Ok(read_opts) } + +pub struct ReadKeyOpts { + var_name: Option, + char_whitelist: Option, + char_blacklist: Option +} + +pub fn read_key(node: Node) -> ShResult<()> { + let blame = node.get_span().clone(); + let NdRule::Command { argv, .. } = node.class else { unreachable!() }; + + if !isatty(*TTY_FILENO)? { + state::set_status(1); + return Ok(()); + } + + let (_, opts) = get_opts_from_tokens(argv, &READ_KEY_OPTS).blame(blame.clone())?; + let read_key_opts = get_read_key_opts(opts).blame(blame.clone())?; + + let key = { + let _raw = crate::readline::term::raw_mode(); + let mut buf = [0u8; 16]; + match read(*TTY_FILENO, &mut buf) { + Ok(0) => { + state::set_status(1); + return Ok(()); + } + Ok(n) => { + let mut reader = PollReader::new(); + reader.feed_bytes(&buf[..n]); + let Some(key) = reader.read_key()? else { + state::set_status(1); + return Ok(()); + }; + key + }, + Err(Errno::EINTR) => { + state::set_status(130); + return Ok(()); + } + Err(e) => return Err(ShErr::simple(ShErrKind::ExecFail, format!("read_key: {e}"))), + } + }; + + let vim_seq = key.as_vim_seq()?; + + if let Some(wl) = read_key_opts.char_whitelist { + let allowed = expand_keymap(&wl); + if !allowed.contains(&key) { + state::set_status(1); + return Ok(()); + } + } + + if let Some(bl) = read_key_opts.char_blacklist { + let disallowed = expand_keymap(&bl); + if disallowed.contains(&key) { + state::set_status(1); + return Ok(()); + } + } + + if let Some(var) = read_key_opts.var_name { + write_vars(|v| v.set_var(&var, VarKind::Str(vim_seq), VarFlags::NONE))?; + } else { + write(borrow_fd(STDOUT_FILENO), vim_seq.as_bytes())?; + } + + state::set_status(0); + Ok(()) +} + +pub fn get_read_key_opts(opts: Vec) -> ShResult { + let mut read_key_opts = ReadKeyOpts { + var_name: None, + char_whitelist: None, + char_blacklist: None + }; + + for opt in opts { + match opt { + Opt::ShortWithArg('v', var_name) => read_key_opts.var_name = Some(var_name), + Opt::ShortWithArg('w', char_whitelist) => read_key_opts.char_whitelist = Some(char_whitelist), + Opt::ShortWithArg('b', char_blacklist) => read_key_opts.char_blacklist = Some(char_blacklist), + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("read_key: Unexpected flag '{opt}'") + )); + } + } + } + + Ok(read_key_opts) +} diff --git a/src/expand.rs b/src/expand.rs index dc875d5..b36f0a2 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -14,7 +14,7 @@ use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; use crate::readline::keys::{KeyCode, KeyEvent, ModKeys}; use crate::readline::markers; use crate::state::{ - ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_shopts, read_vars, write_jobs, write_meta, write_vars + self, ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_shopts, read_vars, write_jobs, write_meta, write_vars }; use crate::prelude::*; @@ -925,7 +925,8 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult { e.print_error(); unsafe { libc::_exit(1) }; } - unsafe { libc::_exit(0) }; + let status = state::get_status(); + unsafe { libc::_exit(status) }; } ForkResult::Parent { child } => { std::mem::drop(cmd_sub_io_frame); // Closes the write pipe @@ -950,7 +951,10 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult { }; match status { - WtStat::Exited(_, _) => Ok(io_buf.as_str()?.trim_end().to_string()), + WtStat::Exited(_, code) => { + state::set_status(code); + Ok(io_buf.as_str()?.trim_end().to_string()) + }, _ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")), } } @@ -1386,8 +1390,11 @@ impl FromStr for ParamExp { pub fn parse_pos_len(s: &str) -> Option<(usize, Option)> { let raw = s.strip_prefix(':')?; if let Some((start, len)) = raw.split_once(':') { + let start = expand_raw(&mut start.chars().peekable()).unwrap_or_else(|_| start.to_string()); + let len = expand_raw(&mut len.chars().peekable()).unwrap_or_else(|_| len.to_string()); Some((start.parse::().ok()?, len.parse::().ok())) } else { + let raw = expand_raw(&mut raw.chars().peekable()).unwrap_or_else(|_| raw.to_string()); Some((raw.parse::().ok()?, None)) } } @@ -1620,8 +1627,9 @@ pub fn glob_to_regex(glob: &str, anchored: bool) -> Regex { '\\' => { // Shell escape: next char is literal if let Some(esc) = chars.next() { - regex.push('\\'); - regex.push(esc); + // Some characters have special meaning after \ in regex + // (e.g. \< is word boundary), so use hex escape for safety + regex.push_str(&format!("\\x{:02x}", esc as u32)); } } '*' => regex.push_str(".*"), @@ -2145,7 +2153,6 @@ pub fn expand_aliases( } pub fn expand_keymap(s: &str) -> Vec { - log::debug!("Expanding keymap for '{}'", s); let mut keys = Vec::new(); let mut chars = s.chars().collect::>(); while let Some(ch) = chars.pop_front() { @@ -2165,13 +2172,11 @@ pub fn expand_keymap(s: &str) -> Vec { } } '>' => { - log::debug!("Found key alias '{}'", alias); if alias.eq_ignore_ascii_case("leader") { let mut leader = read_shopts(|o| o.prompt.leader.clone()); if leader == "\\" { leader.push('\\'); } - log::debug!("Expanding leader key to '{}'", leader); keys.extend(expand_keymap(&leader)); } else if let Some(key) = parse_key_alias(&alias) { keys.push(key); @@ -2188,7 +2193,6 @@ pub fn expand_keymap(s: &str) -> Vec { } } - log::debug!("Expanded keymap '{}' to {:?}", s, keys); keys } diff --git a/src/libsh/error.rs b/src/libsh/error.rs index 6a5db4f..024059f 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -87,6 +87,7 @@ pub trait ShResultExt { fn blame(self, span: Span) -> Self; fn try_blame(self, span: Span) -> Self; fn promote_err(self, span: Span) -> Self; + fn is_flow_control(&self) -> bool; } impl ShResultExt for Result { @@ -101,6 +102,9 @@ impl ShResultExt for Result { fn promote_err(self, span: Span) -> Self { self.map_err(|e| e.promote(span)) } + fn is_flow_control(&self) -> bool { + self.as_ref().is_err_and(|e| e.is_flow_control()) + } } #[derive(Clone, Debug)] @@ -179,6 +183,9 @@ impl ShErr { pub fn simple(kind: ShErrKind, msg: impl Into) -> Self { Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()], io_guards: vec![] } } + pub fn is_flow_control(&self) -> bool { + self.kind.is_flow_control() + } pub fn promote(mut self, span: Span) -> Self { if self.notes.is_empty() { return self @@ -356,6 +363,18 @@ pub enum ShErrKind { Null, } +impl ShErrKind { + pub fn is_flow_control(&self) -> bool { + matches!(self, + Self::CleanExit(_) | + Self::FuncReturn(_) | + Self::LoopContinue(_) | + Self::LoopBreak(_) | + Self::ClearReadline + ) + } +} + impl Display for ShErrKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let output = match self { diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 65b9505..bed7f7c 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -7,7 +7,7 @@ use ariadne::Fmt; use crate::{ builtin::{ - alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, map, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak + alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, map, pwd::pwd, read::{self, read_builtin}, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak }, expand::{expand_aliases, glob_to_regex}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, @@ -423,13 +423,16 @@ impl Dispatcher { 'outer: for block in case_blocks { let CaseNode { pattern, body } = block; - let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim(); + let block_pattern_raw = pattern.span.as_str().strip_suffix(')').unwrap_or(pattern.span.as_str()).trim(); + log::debug!("[case] raw block pattern: {:?}", block_pattern_raw); // Split at '|' to allow for multiple patterns like `foo|bar)` let block_patterns = block_pattern_raw.split('|'); for pattern in block_patterns { let pattern_regex = glob_to_regex(pattern, false); + log::debug!("[case] testing input {:?} against pattern {:?} (regex: {:?})", pattern_raw, pattern, pattern_regex); if pattern_regex.is_match(&pattern_raw) { + log::debug!("[case] matched pattern {:?}", pattern); for node in &body { s.dispatch_node(node.clone())?; } @@ -824,6 +827,7 @@ impl Dispatcher { "type" => intro::type_builtin(cmd), "getopts" => getopts(cmd), "keymap" => keymap::keymap(cmd), + "read_key" => read::read_key(cmd), "true" | ":" => { state::set_status(0); Ok(()) @@ -836,6 +840,9 @@ impl Dispatcher { }; if let Err(e) = result { + if !e.is_flow_control() { + state::set_status(1); + } Err(e.with_context(context).with_redirs(redir_guard)) } else { Ok(()) @@ -860,7 +867,6 @@ impl Dispatcher { let no_fork = cmd.flags.contains(NdFlags::NO_FORK); if argv.is_empty() { - state::set_status(0); return Ok(()); } diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 08bb39f..8696688 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -272,6 +272,26 @@ bitflags! { } } +pub fn clean_input(input: &str) -> String { + let mut chars = input.chars().peekable(); + let mut output = String::new(); + while let Some(ch) = chars.next() { + match ch { + '\\' if chars.peek() == Some(&'\n') => { + chars.next(); + } + '\r' => { + if chars.peek() == Some(&'\n') { + chars.next(); + } + output.push('\n'); + } + _ => output.push(ch), + } + } + output +} + impl LexStream { pub fn new(source: Arc, flags: LexFlags) -> Self { let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD; @@ -825,12 +845,15 @@ impl Iterator for LexStream { self.set_next_is_cmd(true); while let Some(ch) = get_char(&self.source, self.cursor) { - if is_hard_sep(ch) { - // Combine consecutive separators into one, including whitespace - self.cursor += 1; - } else { - break; - } + match ch { + '\\' => { + self.cursor = (self.cursor + 2).min(self.source.len()); + } + _ if is_hard_sep(ch) => { + self.cursor += 1; + } + _ => break, + } } self.get_token(ch_idx..self.cursor, TkRule::Sep) } @@ -1060,6 +1083,7 @@ pub fn lookahead(pat: &str, mut chars: Chars) -> Option { pub fn case_pat_lookahead(mut chars: Peekable) -> Option { let mut pos = 0; + let mut qt_state = QuoteState::default(); while let Some(ch) = chars.next() { pos += ch.len_utf8(); match ch { @@ -1069,8 +1093,14 @@ pub fn case_pat_lookahead(mut chars: Peekable) -> Option { pos += esc.len_utf8(); } } - ')' => return Some(pos), - '(' => return None, + '\'' => { + qt_state.toggle_single(); + } + '"' => { + qt_state.toggle_double(); + } + ')' if qt_state.outside() => return Some(pos), + '(' if qt_state.outside() => return None, _ => { /* continue */ } } } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 1a6a0e6..716a7f3 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -9,9 +9,7 @@ use crate::{ libsh::{ error::{ShErr, ShErrKind, ShResult, last_color, next_color}, utils::{NodeVecUtils, TkVecUtils}, - }, - prelude::*, - procio::IoMode, + }, parse::lex::clean_input, prelude::*, procio::IoMode }; pub mod execute; @@ -52,6 +50,11 @@ pub struct ParsedSrc { impl ParsedSrc { pub fn new(src: Arc) -> Self { + let src = if src.contains("\\\n") || src.contains('\r') { + Arc::new(clean_input(&src)) + } else { + src + }; Self { src, name: "".into(), diff --git a/src/readline/keys.rs b/src/readline/keys.rs index 121414a..6c3a776 100644 --- a/src/readline/keys.rs +++ b/src/readline/keys.rs @@ -1,6 +1,8 @@ use std::sync::Arc; use unicode_segmentation::UnicodeSegmentation; +use crate::libsh::error::{ShErr, ShErrKind, ShResult}; + // Credit to Rustyline for the design ideas in this module // https://github.com/kkawakam/rustyline #[derive(Clone, PartialEq, Eq, Debug)] @@ -87,6 +89,109 @@ impl KeyEvent { } } } + pub fn as_vim_seq(&self) -> ShResult { + let mut seq = String::new(); + let KeyEvent(event, mods) = self; + let mut needs_angle_bracket = false; + + if mods.contains(ModKeys::CTRL) { + seq.push_str("C-"); + needs_angle_bracket = true; + } + if mods.contains(ModKeys::ALT) { + seq.push_str("A-"); + needs_angle_bracket = true; + } + if mods.contains(ModKeys::SHIFT) { + seq.push_str("S-"); + needs_angle_bracket = true; + } + + match event { + KeyCode::UnknownEscSeq => return Err(ShErr::simple( + ShErrKind::ParseErr, + "Cannot convert unknown escape sequence to Vim key sequence".to_string(), + )), + KeyCode::Backspace => { + seq.push_str("BS"); + needs_angle_bracket = true; + } + KeyCode::BackTab => { + seq.push_str("S-Tab"); + needs_angle_bracket = true; + } + KeyCode::BracketedPasteStart => todo!(), + KeyCode::BracketedPasteEnd => todo!(), + KeyCode::Delete => { + seq.push_str("Del"); + needs_angle_bracket = true; + } + KeyCode::Down => { + seq.push_str("Down"); + needs_angle_bracket = true; + } + KeyCode::End => { + seq.push_str("End"); + needs_angle_bracket = true; + } + KeyCode::Enter => { + seq.push_str("Enter"); + needs_angle_bracket = true; + } + KeyCode::Esc => { + seq.push_str("Esc"); + needs_angle_bracket = true; + } + + KeyCode::F(f) => { + seq.push_str(&format!("F{}", f)); + needs_angle_bracket = true; + } + KeyCode::Home => { + seq.push_str("Home"); + needs_angle_bracket = true; + } + KeyCode::Insert => { + seq.push_str("Insert"); + needs_angle_bracket = true; + } + KeyCode::Left => { + seq.push_str("Left"); + needs_angle_bracket = true; + } + KeyCode::Null => todo!(), + KeyCode::PageDown => { + seq.push_str("PgDn"); + needs_angle_bracket = true; + } + KeyCode::PageUp => { + seq.push_str("PgUp"); + needs_angle_bracket = true; + } + KeyCode::Right => { + seq.push_str("Right"); + needs_angle_bracket = true; + } + KeyCode::Tab => { + seq.push_str("Tab"); + needs_angle_bracket = true; + } + KeyCode::Up => { + seq.push_str("Up"); + needs_angle_bracket = true; + } + KeyCode::Char(ch) => { + seq.push(*ch); + } + KeyCode::Grapheme(gr) => seq.push_str(gr), + } + + if needs_angle_bracket { + Ok(format!("<{}>", seq)) + } else { + Ok(seq) + } + } } #[derive(Clone, PartialEq, Eq, Debug)] diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index 3692c43..c222da4 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -18,7 +18,7 @@ use crate::{ register::{RegisterContent, write_register}, term::RawModeGuard, }, - state::{VarFlags, VarKind, read_shopts, read_vars, write_vars}, + state::{VarFlags, VarKind, read_shopts, read_vars, write_meta, write_vars}, }; const PUNCTUATION: [&str; 3] = ["?", "!", "."]; @@ -926,6 +926,7 @@ impl LineBuf { } pub fn is_word_bound(&mut self, pos: usize, word: Word, dir: Direction) -> bool { let clamped_pos = ClampedUsize::new(pos, self.cursor.max, true); + log::debug!("clamped_pos: {}", clamped_pos.get()); let cur_char = self .grapheme_at(clamped_pos.get()) .map(|c| c.to_string()) @@ -996,7 +997,11 @@ impl LineBuf { } else { self.start_of_word_backward(self.cursor.get(), word) }; - let end = self.dispatch_word_motion(count, To::Start, word, Direction::Forward, true); + let end = if self.is_word_bound(self.cursor.get(), word, Direction::Forward) { + self.cursor.get() + } else { + self.end_of_word_forward(self.cursor.get(), word) + }; Some((start, end)) } Bound::Around => { @@ -3083,29 +3088,45 @@ impl LineBuf { | Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these Verb::ShellCmd(cmd) => { + log::debug!("Executing ex-mode command from widget: {cmd}"); let mut vars = HashSet::new(); - vars.insert("BUFFER".into()); - vars.insert("CURSOR".into()); + vars.insert("_BUFFER".into()); + vars.insert("_CURSOR".into()); + vars.insert("_ANCHOR".into()); let _guard = var_ctx_guard(vars); let mut buf = self.as_str().to_string(); let mut cursor = self.cursor.get(); + let mut anchor = self.select_range().map(|r| if r.0 != cursor { r.0 } else { r.1 }).unwrap_or(cursor); write_vars(|v| { - v.set_var("BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?; - v.set_var("CURSOR", VarKind::Str(cursor.to_string()), VarFlags::EXPORT) + v.set_var("_BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?; + v.set_var("_CURSOR", VarKind::Str(cursor.to_string()), VarFlags::EXPORT)?; + v.set_var("_ANCHOR", VarKind::Str(anchor.to_string()), VarFlags::EXPORT) })?; RawModeGuard::with_cooked_mode(|| exec_input(cmd, None, true, Some("".into())))?; - read_vars(|v| { - buf = v.get_var("BUFFER"); - cursor = v.get_var("CURSOR").parse().unwrap_or(cursor); + let keys = write_vars(|v| { + buf = v.take_var("_BUFFER"); + cursor = v.take_var("_CURSOR").parse().unwrap_or(cursor); + anchor = v.take_var("_ANCHOR").parse().unwrap_or(anchor); + v.take_var("_KEYS") }); self.set_buffer(buf); + self.update_graphemes(); self.cursor.set_max(self.buffer.graphemes(true).count()); self.cursor.set(cursor); + log::debug!("[ShellCmd] post-widget: cursor={}, anchor={}, select_range={:?}", cursor, anchor, self.select_range); + if anchor != cursor && self.select_range.is_some() { + self.select_range = Some(ordered(cursor, anchor)); + } + if !keys.is_empty() { + log::debug!("Pending widget keys from shell command: {keys}"); + write_meta(|m| m.set_pending_widget_keys(&keys)) + } + } Verb::Normal(_) | Verb::Read(_) diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 7230f26..29eb63e 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -11,11 +11,11 @@ use crate::builtin::keymap::{KeyMapFlags, KeyMapMatch}; use crate::expand::expand_prompt; use crate::libsh::sys::TTY_FILENO; use crate::parse::lex::{LexStream, QuoteState}; -use crate::prelude::*; +use crate::{prelude::*, state}; use crate::readline::complete::FuzzyCompleter; use crate::readline::term::{Pos, TermReader, calc_str_width}; use crate::readline::vimode::ViEx; -use crate::state::{ShellParam, read_logic, read_shopts}; +use crate::state::{ShellParam, read_logic, read_shopts, write_meta}; use crate::{ libsh::error::ShResult, parse::lex::{self, LexFlags, Tk, TkFlags, TkRule}, @@ -148,6 +148,9 @@ impl Prompt { let Ok(ps1_raw) = env::var("PS1") else { return Self::default(); }; + // PS1 expansion may involve running commands (e.g., for \h or \W), which can modify shell state + let saved_status = state::get_status(); + let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else { return Self::default(); }; @@ -158,6 +161,9 @@ impl Prompt { .transpose() .ok() .flatten(); + + // Restore shell state after prompt expansion, since it may have been modified by command substitutions in the prompt + state::set_status(saved_status); Self { ps1_expanded, ps1_raw, @@ -213,10 +219,12 @@ pub struct ShedVi { pub completer: Box, pub mode: Box, + pub saved_mode: Option>, pub pending_keymap: Vec, pub repeat_action: Option, pub repeat_motion: Option, pub editor: LineBuf, + pub next_is_escaped: bool, pub old_layout: Option, pub history: History, @@ -233,6 +241,8 @@ impl ShedVi { completer: Box::new(FuzzyCompleter::default()), highlighter: Highlighter::new(), mode: Box::new(ViInsert::new()), + next_is_escaped: false, + saved_mode: None, pending_keymap: Vec::new(), old_layout: None, repeat_action: None, @@ -365,8 +375,6 @@ impl ShedVi { let span_start = self.completer.token_span().0; let new_cursor = span_start + candidate.len(); let line = self.completer.get_completed_line(&candidate); - log::debug!("Completer accepted candidate: {candidate}"); - log::debug!("New line after completion: {line}"); self.editor.set_buffer(line); self.editor.cursor.set(new_cursor); // Don't reset yet — clear() needs old_layout to erase the selector. @@ -401,13 +409,10 @@ impl ShedVi { } else { let keymap_flags = self.curr_keymap_flags(); self.pending_keymap.push(key.clone()); - log::debug!("[keymap] pending={:?} flags={:?}", self.pending_keymap, keymap_flags); let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &self.pending_keymap)); - log::debug!("[keymap] {} matches found", matches.len()); if matches.is_empty() { // No matches. Drain the buffered keys and execute them. - log::debug!("[keymap] no matches, flushing {} buffered keys", self.pending_keymap.len()); for key in std::mem::take(&mut self.pending_keymap) { if let Some(event) = self.handle_key(key)? { return Ok(event); @@ -418,11 +423,8 @@ impl ShedVi { } else if matches.len() == 1 && matches[0].compare(&self.pending_keymap) == KeyMapMatch::IsExact { // We have a single exact match. Execute it. let keymap = matches[0].clone(); - log::debug!("[keymap] self.pending_keymap={:?}", self.pending_keymap); - log::debug!("[keymap] exact match: {:?} -> {:?}", keymap.keys, keymap.action); self.pending_keymap.clear(); let action = keymap.action_expanded(); - log::debug!("[keymap] expanded action: {:?}", action); for key in action { if let Some(event) = self.handle_key(key)? { return Ok(event); @@ -432,7 +434,6 @@ impl ShedVi { continue; } else { // There is ambiguity. Allow the timeout in the main loop to handle this. - log::debug!("[keymap] ambiguous: {} matches, waiting for more input", matches.len()); continue; } } @@ -512,6 +513,13 @@ impl ShedVi { return Ok(None); } + if let KeyEvent(KeyCode::Char('\\'), ModKeys::NONE) = key + && !self.next_is_escaped { + self.next_is_escaped = true; + } else { + self.next_is_escaped = false; + } + let Ok(cmd) = self.mode.handle_key_fallible(key) else { // it's an ex mode error self.mode = Box::new(ViNormal::new()) as Box; @@ -519,10 +527,8 @@ impl ShedVi { }; let Some(mut cmd) = cmd else { - log::debug!("[readline] mode.handle_key returned None"); return Ok(None); }; - log::debug!("[readline] got cmd: verb={:?} motion={:?} flags={:?}", cmd.verb, cmd.motion, cmd.flags); cmd.alter_line_motion_if_no_verb(); if self.should_grab_history(&cmd) { @@ -532,8 +538,9 @@ impl ShedVi { } if cmd.is_submit_action() - && (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) - { + && !self.next_is_escaped + && !self.editor.buffer.ends_with('\\') + && (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) { self.editor.set_hint(None); self.editor.cursor.set(self.editor.cursor_max()); self.print_line(true)?; @@ -564,6 +571,11 @@ impl ShedVi { let before = self.editor.buffer.clone(); self.exec_cmd(cmd)?; + if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) { + for key in keys { + self.handle_key(key)?; + } + } let after = self.editor.as_str(); if before != after { @@ -637,6 +649,7 @@ impl ShedVi { || (self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty() && matches!(event, KeyEvent(KeyCode::Char('l'), ModKeys::NONE))) } + ModeReport::Ex => false, _ => unimplemented!(), } } else { @@ -783,7 +796,11 @@ impl ShedVi { if cmd.is_mode_transition() { let count = cmd.verb_count(); let mut mode: Box = if let ModeReport::Ex = self.mode.report_mode() && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) { - Box::new(ViNormal::new()) + if let Some(saved) = self.saved_mode.take() { + saved + } else { + Box::new(ViNormal::new()) + } } else { match cmd.verb().unwrap().1 { Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => { @@ -791,7 +808,9 @@ impl ShedVi { Box::new(ViInsert::new().with_count(count as u16)) } - Verb::ExMode => Box::new(ViEx::new()), + Verb::ExMode => { + Box::new(ViEx::new()) + } Verb::NormalMode => Box::new(ViNormal::new()), @@ -825,6 +844,11 @@ impl ShedVi { std::mem::swap(&mut mode, &mut self.mode); + if self.mode.report_mode() == ModeReport::Ex { + self.saved_mode = Some(mode); + return Ok(()); + } + if mode.is_repeatable() { self.repeat_action = mode.as_replay(); } @@ -917,12 +941,22 @@ impl ShedVi { } } + if self.mode.report_mode() == ModeReport::Visual + && self.editor.select_range().is_none() { + self.editor.stop_selecting(); + let mut mode: Box = Box::new(ViNormal::new()); + std::mem::swap(&mut mode, &mut self.mode); + } + if cmd.is_repeatable() { if self.mode.report_mode() == ModeReport::Visual { // The motion is assigned in the line buffer execution, so we also have to // assign it here in order to be able to repeat it - let range = self.editor.select_range().unwrap(); - cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1))) + if let Some(range) = self.editor.select_range() { + cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1))) + } else { + log::warn!("You're in visual mode with no select range??"); + }; } self.repeat_action = Some(CmdReplay::Single(cmd.clone())); } @@ -939,8 +973,20 @@ impl ShedVi { std::mem::swap(&mut mode, &mut self.mode); } + if self.mode.report_mode() != ModeReport::Visual && self.editor.select_range().is_some() { + self.editor.stop_selecting(); + } + if cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) { - let mut mode: Box = Box::new(ViNormal::new()); + let mut mode: Box = if self.mode.report_mode() == ModeReport::Ex { + if let Some(saved) = self.saved_mode.take() { + saved + } else { + Box::new(ViNormal::new()) + } + } else { + Box::new(ViNormal::new()) + }; std::mem::swap(&mut mode, &mut self.mode); self.editor.set_cursor_clamp(self.mode.clamp_cursor()); } @@ -976,6 +1022,8 @@ pub fn annotate_input(input: &str) -> String { .filter(|tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI | TkRule::Null)) .collect(); + log::debug!("Annotating input with tokens: {tokens:#?}"); + for tk in tokens.into_iter().rev() { let insertions = annotate_token(tk); for (pos, marker) in insertions { @@ -1019,7 +1067,6 @@ pub fn annotate_input_recursive(input: &str) -> String { Some('>') => ">(", Some('<') => "<(", _ => { - log::error!("Unexpected character after PROC_SUB marker: expected '>' or '<'"); "<(" } }, diff --git a/src/readline/vimode/visual.rs b/src/readline/vimode/visual.rs index c3aa76e..18fca27 100644 --- a/src/readline/vimode/visual.rs +++ b/src/readline/vimode/visual.rs @@ -129,6 +129,15 @@ impl ViVisual { flags: CmdFlags::empty(), }); } + ':' => { + return Some(ViCmd { + register, + verb: Some(VerbCmd(count, Verb::ExMode)), + motion: None, + raw_seq: self.take_cmd(), + flags: CmdFlags::empty(), + }) + } 'x' => { chars = chars_clone; break 'verb_parse Some(VerbCmd(count, Verb::Delete)); diff --git a/src/state.rs b/src/state.rs index 2ece57c..d0f0677 100644 --- a/src/state.rs +++ b/src/state.rs @@ -11,22 +11,15 @@ use std::{ use nix::unistd::{User, gethostname, getppid}; use crate::{ - builtin::{BUILTINS, keymap::{KeyMap, KeyMapFlags, KeyMapMatch}, map::MapNode, trap::TrapTarget}, - exec_input, - jobs::JobTab, - libsh::{ + builtin::{BUILTINS, keymap::{KeyMap, KeyMapFlags, KeyMapMatch}, map::MapNode, trap::TrapTarget}, exec_input, expand::expand_keymap, jobs::JobTab, libsh::{ error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt, - }, - parse::{ + }, parse::{ ConjunctNode, NdRule, Node, ParsedSrc, lex::{LexFlags, LexStream, Span, Tk}, - }, - prelude::*, - readline::{ + }, prelude::*, readline::{ complete::{BashCompSpec, CompSpec}, keys::KeyEvent, markers - }, - shopt::ShOpts, + }, shopt::ShOpts }; pub struct Shed { @@ -417,6 +410,11 @@ impl ScopeStack { None } + pub fn take_var(&mut self, var_name: &str) -> String { + let var = self.get_var(var_name); + self.unset_var(var_name).ok(); + var + } pub fn get_var(&self, var_name: &str) -> String { if let Ok(param) = var_name.parse::() { return self.get_param(param); @@ -1125,6 +1123,8 @@ pub struct MetaTab { // programmable completion specs comp_specs: HashMap>, + // pending keys from widget function + pending_widget_keys: Vec } impl MetaTab { @@ -1134,6 +1134,17 @@ impl MetaTab { ..Default::default() } } + pub fn set_pending_widget_keys(&mut self, keys: &str) { + let exp = expand_keymap(keys); + self.pending_widget_keys = exp; + } + pub fn take_pending_widget_keys(&mut self) -> Option> { + if self.pending_widget_keys.is_empty() { + None + } else { + Some(std::mem::take(&mut self.pending_widget_keys)) + } + } pub fn getopts_char_offset(&self) -> usize { self.getopts_offset }