Added ex mode to line editor, a 'keymap' builtin, and a zsh-like widget system using ':!<shellcmd>' ex mode commands

This commit is contained in:
2026-03-03 03:19:02 -05:00
parent a28446329e
commit 9d13565176
23 changed files with 3141 additions and 2016 deletions

149
src/builtin/keymap.rs Normal file
View File

@@ -0,0 +1,149 @@
use crate::{
expand::expand_keymap, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, prelude::*, readline::{keys::KeyEvent, vimode::ModeReport}, state::{self, write_logic}
};
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyMapFlags: u32 {
const NORMAL = 0b0000001;
const INSERT = 0b0000010;
const VISUAL = 0b0000100;
const EX = 0b0001000;
const OP_PENDING = 0b0010000;
const REPLACE = 0b0100000;
}
}
pub struct KeyMapOpts {
remove: Option<String>,
flags: KeyMapFlags,
}
impl KeyMapOpts {
pub fn from_opts(opts: &[Opt]) -> ShResult<Self> {
let mut flags = KeyMapFlags::empty();
let mut remove = None;
for opt in opts {
match opt {
Opt::Short('n') => flags |= KeyMapFlags::NORMAL,
Opt::Short('i') => flags |= KeyMapFlags::INSERT,
Opt::Short('v') => flags |= KeyMapFlags::VISUAL,
Opt::Short('x') => flags |= KeyMapFlags::EX,
Opt::Short('o') => flags |= KeyMapFlags::OP_PENDING,
Opt::Short('r') => flags |= KeyMapFlags::REPLACE,
Opt::LongWithArg(name, arg) if name == "remove" => {
if remove.is_some() {
return Err(ShErr::simple(ShErrKind::ExecFail, "Duplicate --remove option for keymap".to_string()));
}
remove = Some(arg.clone());
},
_ => return Err(ShErr::simple(ShErrKind::ExecFail, format!("Invalid option for keymap: {:?}", opt))),
}
}
if flags.is_empty() {
return Err(ShErr::simple(ShErrKind::ExecFail, "At least one mode option must be specified for keymap".to_string()).with_note("Use -n for normal mode, -i for insert mode, -v for visual mode, -x for ex mode, and -o for operator-pending mode".to_string()));
}
Ok(Self { remove, flags })
}
pub fn keymap_opts() -> [OptSpec;6] {
[
OptSpec {
opt: Opt::Short('n'), // normal mode
takes_arg: false
},
OptSpec {
opt: Opt::Short('i'), // insert mode
takes_arg: false
},
OptSpec {
opt: Opt::Short('v'), // visual mode
takes_arg: false
},
OptSpec {
opt: Opt::Short('x'), // ex mode
takes_arg: false
},
OptSpec {
opt: Opt::Short('o'), // operator-pending mode
takes_arg: false
},
OptSpec {
opt: Opt::Short('r'), // replace mode
takes_arg: false
},
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyMapMatch {
NoMatch,
IsPrefix,
IsExact
}
#[derive(Debug, Clone)]
pub struct KeyMap {
pub flags: KeyMapFlags,
pub keys: String,
pub action: String
}
impl KeyMap {
pub fn keys_expanded(&self) -> Vec<KeyEvent> {
expand_keymap(&self.keys)
}
pub fn action_expanded(&self) -> Vec<KeyEvent> {
expand_keymap(&self.action)
}
pub fn compare(&self, other: &[KeyEvent]) -> KeyMapMatch {
let ours = self.keys_expanded();
if other == ours {
KeyMapMatch::IsExact
} else if ours.starts_with(other) {
KeyMapMatch::IsPrefix
} else {
KeyMapMatch::NoMatch
}
}
}
pub fn keymap(node: Node) -> ShResult<()> {
let span = node.get_span();
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let (argv, opts) = get_opts_from_tokens(argv, &KeyMapOpts::keymap_opts())?;
let opts = KeyMapOpts::from_opts(&opts).promote_err(span.clone())?;
if let Some(to_rm) = opts.remove {
write_logic(|l| l.remove_keymap(&to_rm));
state::set_status(0);
return Ok(());
}
let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); }
let Some((keys,_)) = argv.first() else {
return Err(ShErr::at(ShErrKind::ExecFail, span, "missing keys argument".to_string()));
};
let Some((action,_)) = argv.get(1) else {
return Err(ShErr::at(ShErrKind::ExecFail, span, "missing action argument".to_string()));
};
let keymap = KeyMap {
flags: opts.flags,
keys: keys.clone(),
action: action.clone(),
};
write_logic(|l| l.insert_keymap(keymap));
state::set_status(0);
Ok(())
}

View File

@@ -25,13 +25,14 @@ pub mod map;
pub mod arrops;
pub mod intro;
pub mod getopts;
pub mod keymap;
pub const BUILTINS: [&str; 44] = [
pub const BUILTINS: [&str; 45] = [
"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"
"getopts", "keymap"
];
pub fn true_builtin() -> ShResult<()> {