From d7d3b56981ce71c0d24cb5ab51d3839f7d90b81c Mon Sep 17 00:00:00 2001 From: pagedmov Date: Wed, 4 Mar 2026 12:55:50 -0500 Subject: [PATCH] Implemented the autocmd builtin, which allows you to register hooks for certain shell events. --- Cargo.lock | 96 ++++++++++------------- nix/hm-module.nix | 47 ++++++++++- src/builtin/autocmd.rs | 91 ++++++++++++++++++++++ src/builtin/cd.rs | 2 +- src/builtin/dirstack.rs | 2 +- src/builtin/keymap.rs | 2 +- src/builtin/mod.rs | 5 +- src/builtin/read.rs | 1 - src/libsh/utils.rs | 32 ++++++++ src/main.rs | 13 +++- src/parse/execute.rs | 16 ++-- src/readline/complete.rs | 12 ++- src/readline/linebuf.rs | 18 ++++- src/readline/mod.rs | 44 ++++++++--- src/readline/vimode/mod.rs | 15 ++++ src/signal.rs | 36 ++++++--- src/state.rs | 155 ++++++++++++++++++++++++++++++++++++- 17 files changed, 485 insertions(+), 102 deletions(-) create mode 100644 src/builtin/autocmd.rs diff --git a/Cargo.lock b/Cargo.lock index 0813c44..bd9c756 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,9 +85,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "cfg-if" @@ -114,9 +114,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.55" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -124,9 +124,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.55" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -148,9 +148,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" @@ -250,21 +250,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", @@ -321,9 +309,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.46.1" +version = "1.46.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248b42847813a1550dafd15296fd9748c651d0c32194559dbc05d804d54b21e8" +checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" dependencies = [ "console", "once_cell", @@ -354,9 +342,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.20" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "log", @@ -367,9 +355,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.20" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -384,15 +372,15 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" @@ -402,9 +390,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "nix" @@ -476,18 +464,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" @@ -496,7 +484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "chacha20", - "getrandom 0.4.1", + "getrandom", "rand_core", ] @@ -508,9 +496,9 @@ checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -520,9 +508,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -531,15 +519,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -641,9 +629,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -652,12 +640,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom", "once_cell", "rustix", "windows-sys 0.61.2", @@ -665,9 +653,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" diff --git a/nix/hm-module.nix b/nix/hm-module.nix index d12ba9f..8558993 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -5,9 +5,17 @@ let boolToString = b: if b then "true" else "false"; - mkFunctionDef = name: body: '' + mkAutoCmd = cfg: + lib.concatLines (map (hook: "autocmd ${hook} ${lib.optionalString (cfg.pattern != null) "-p \"${cfg.pattern}\""} '${cfg.command}'") cfg.hooks); + + + mkFunctionDef = name: body: + let + indented = "\t" + lib.concatStringsSep "\n\t" (lib.splitString "\n" body); + in + '' ${name}() { -${body} +${indented} }''; mkKeymapCmd = cfg: let @@ -58,6 +66,39 @@ in description = "Shell functions to set when shed starts"; }; + autocmds = lib.mkOption { + type = lib.types.listOf (lib.types.submodule { + options = { + hooks = lib.mkOption { + type = lib.types.addCheck (lib.types.listOf (lib.types.enum [ + "pre-cmd" + "post-cmd" + "pre-change-dir" + "post-change-dir" + "on-job-finish" + "pre-prompt" + "post-prompt" + "pre-mode-change" + "post-mode-change" + ])) (list: list != []); + description = "The events that trigger this autocmd"; + }; + pattern = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "A regex pattern to use in the hook to determine whether it runs or not. What it's compared to differs by hook, for instance 'pre-change-dir' compares it to the new directory, pre-cmd compares it to the command, etc"; + }; + command = lib.mkOption { + type = lib.types.addCheck lib.types.str (cmd: cmd != ""); + description = "The shell command to execute when the hook is triggered and the pattern (if provided) matches"; + }; + }; + + }); + default = []; + description = "Custom autocmds to set when shed starts"; + }; + keymaps = lib.mkOption { type = lib.types.listOf (lib.types.submodule { options = { @@ -243,6 +284,7 @@ in completeLines = lib.concatLines (lib.mapAttrsToList mkCompleteCmd cfg.extraCompletion); keymapLines = lib.concatLines (map mkKeymapCmd cfg.keymaps); functionLines = lib.concatLines (lib.mapAttrsToList mkFunctionDef cfg.functions); + autocmdLines = lib.concatLines (map mkAutoCmd cfg.autocmds); in lib.mkIf cfg.enable { home.packages = [ cfg.package ]; @@ -269,6 +311,7 @@ in functionLines completeLines keymapLines + autocmdLines ]) cfg.settings.extraPostConfig ]; diff --git a/src/builtin/autocmd.rs b/src/builtin/autocmd.rs new file mode 100644 index 0000000..62b5c10 --- /dev/null +++ b/src/builtin/autocmd.rs @@ -0,0 +1,91 @@ +use regex::Regex; + +use crate::{ + getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, state::{self, AutoCmd, AutoCmdKind, write_logic} +}; + +pub struct AutoCmdOpts { + pattern: Option, + clear: bool +} +fn autocmd_optspec() -> [OptSpec;2] { + [ + OptSpec { + opt: Opt::Short('p'), + takes_arg: true + }, + OptSpec { + opt: Opt::Short('c'), + takes_arg: false + } + ] +} + +pub fn get_autocmd_opts(opts: &[Opt]) -> ShResult { + let mut autocmd_opts = AutoCmdOpts { + pattern: None, + clear: false + }; + + let mut opts = opts.iter(); + while let Some(arg) = opts.next() { + match arg { + Opt::ShortWithArg('p', arg) => { + autocmd_opts.pattern = Some(Regex::new(arg).map_err(|e| ShErr::simple(ShErrKind::ExecFail, format!("invalid regex for -p: {}", e)))?); + } + Opt::Short('c') => { + autocmd_opts.clear = true; + } + _ => { + return Err(ShErr::simple(ShErrKind::ExecFail, format!("unexpected option: {}", arg))); + } + } + } + + Ok(autocmd_opts) +} + +pub fn autocmd(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, &autocmd_optspec()).promote_err(span.clone())?; + let autocmd_opts = get_autocmd_opts(&opts).promote_err(span.clone())?; + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } + let mut args = argv.iter(); + + let Some(autocmd_kind) = args.next() else { + return Err(ShErr::at(ShErrKind::ExecFail, span, "expected an autocmd kind".to_string())); + }; + + let Ok(autocmd_kind) = autocmd_kind.0.parse::() else { + return Err(ShErr::at(ShErrKind::ExecFail, autocmd_kind.1.clone(), format!("invalid autocmd kind: {}", autocmd_kind.0))); + }; + + if autocmd_opts.clear { + write_logic(|l| l.clear_autocmds(autocmd_kind)); + state::set_status(0); + return Ok(()); + } + + let Some(autocmd_cmd) = args.next() else { + return Err(ShErr::at(ShErrKind::ExecFail, span, "expected an autocmd command".to_string())); + }; + + let autocmd = AutoCmd { + pattern: autocmd_opts.pattern, + command: autocmd_cmd.0.clone(), + }; + + write_logic(|l| l.insert_autocmd(autocmd_kind, autocmd)); + + state::set_status(0); + Ok(()) +} diff --git a/src/builtin/cd.rs b/src/builtin/cd.rs index 3c164e8..23c49e7 100644 --- a/src/builtin/cd.rs +++ b/src/builtin/cd.rs @@ -44,7 +44,7 @@ pub fn cd(node: Node) -> ShResult<()> { .labeled(cd_span.clone(), format!("cd: Not a directory '{}'", new_dir.display().fg(next_color())))); } - if let Err(e) = env::set_current_dir(new_dir) { + if let Err(e) = state::change_dir(new_dir) { return Err(ShErr::new(ShErrKind::ExecFail, span.clone()) .labeled(cd_span.clone(), format!("cd: Failed to change directory: '{}'", e.fg(Color::Red)))); } diff --git a/src/builtin/dirstack.rs b/src/builtin/dirstack.rs index a5e8fb7..187bf2f 100644 --- a/src/builtin/dirstack.rs +++ b/src/builtin/dirstack.rs @@ -52,7 +52,7 @@ fn change_directory(target: &PathBuf, blame: Span) -> ShResult<()> { ); } - if let Err(e) = env::set_current_dir(target) { + if let Err(e) = state::change_dir(target) { return Err( ShErr::at(ShErrKind::ExecFail, blame, format!("Failed to change directory: '{}'", e.fg(Color::Red))) ); diff --git a/src/builtin/keymap.rs b/src/builtin/keymap.rs index 3d8b8c8..a5ddb35 100644 --- a/src/builtin/keymap.rs +++ b/src/builtin/keymap.rs @@ -1,5 +1,5 @@ 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} + 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, state::{self, write_logic} }; bitflags! { diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index b5674c5..c35cc5c 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -26,13 +26,14 @@ pub mod arrops; pub mod intro; pub mod getopts; pub mod keymap; +pub mod autocmd; -pub const BUILTINS: [&str; 46] = [ +pub const BUILTINS: [&str; 47] = [ "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", "read_key" + "getopts", "keymap", "read_key", "autocmd" ]; pub fn true_builtin() -> ShResult<()> { diff --git a/src/builtin/read.rs b/src/builtin/read.rs index ee2a6c6..956ac61 100644 --- a/src/builtin/read.rs +++ b/src/builtin/read.rs @@ -4,7 +4,6 @@ use nix::{ libc::{STDIN_FILENO, STDOUT_FILENO}, unistd::{isatty, read, write}, }; -use yansi::Paint; use crate::{ 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} diff --git a/src/libsh/utils.rs b/src/libsh/utils.rs index 6ad45ab..3141f98 100644 --- a/src/libsh/utils.rs +++ b/src/libsh/utils.rs @@ -2,9 +2,11 @@ use std::collections::VecDeque; use ariadne::Span as AriadneSpan; +use crate::parse::execute::exec_input; use crate::parse::lex::{Span, Tk, TkRule}; use crate::parse::{Node, Redir, RedirType}; use crate::prelude::*; +use crate::state::AutoCmd; pub trait VecDequeExt { fn to_vec(self) -> Vec; @@ -22,6 +24,11 @@ pub trait TkVecUtils { fn split_at_separators(&self) -> Vec>; } +pub trait AutoCmdVecUtils { + fn exec(&self); + fn exec_with(&self, pattern: &str); +} + pub trait RedirVecUtils { /// Splits the vector of redirections into two vectors /// @@ -33,6 +40,31 @@ pub trait NodeVecUtils { fn get_span(&self) -> Option; } +impl AutoCmdVecUtils for Vec { + fn exec(&self) { + for cmd in self { + let AutoCmd { pattern: _, command } = cmd; + if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) { + e.print_error(); + } + } + } + fn exec_with(&self, other_pattern: &str) { + for cmd in self { + let AutoCmd { pattern, command } = cmd; + if let Some(pat) = pattern + && !pat.is_match(other_pattern) { + log::trace!("autocmd pattern '{}' did not match '{}', skipping", pat, other_pattern); + continue; + } + + if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) { + e.print_error(); + } + } + } +} + impl VecDequeExt for VecDeque { fn to_vec(self) -> Vec { self.into_iter().collect::>() diff --git a/src/main.rs b/src/main.rs index 8735090..3853e02 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,12 +31,13 @@ use crate::builtin::keymap::KeyMapMatch; use crate::builtin::trap::TrapTarget; use crate::libsh::error::{self, ShErr, ShErrKind, ShResult}; use crate::libsh::sys::TTY_FILENO; +use crate::libsh::utils::AutoCmdVecUtils; use crate::parse::execute::exec_input; use crate::prelude::*; use crate::readline::term::{LineWriter, RawModeGuard, raw_mode}; use crate::readline::{Prompt, ReadlineEvent, ShedVi}; use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending}; -use crate::state::{read_logic, source_rc, write_jobs, write_meta}; +use crate::state::{AutoCmdKind, read_logic, source_rc, write_jobs, write_meta}; use clap::Parser; use state::{read_vars, write_vars}; @@ -357,9 +358,14 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { // Process any available input match readline.process_input() { Ok(ReadlineEvent::Line(input)) => { + let pre_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)); + let post_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)); + + pre_exec.exec_with(&input); + let start = Instant::now(); write_meta(|m| m.start_timer()); - if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true, Some("".into()))) { + if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input.clone(), None, true, Some("".into()))) { match e.kind() { ShErrKind::CleanExit(code) => { QUIT_CODE.store(*code, Ordering::SeqCst); @@ -371,6 +377,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { let command_run_time = start.elapsed(); log::info!("Command executed in {:.2?}", command_run_time); write_meta(|m| m.stop_timer()); + + post_exec.exec_with(&input); + readline.fix_column()?; readline.writer.flush_write("\n\r")?; diff --git a/src/parse/execute.rs b/src/parse/execute.rs index bed7f7c..7d6bfcf 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -7,9 +7,9 @@ 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::{self, 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}, autocmd::autocmd, 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}, + expand::{Expander, expand_aliases, expand_raw, glob_to_regex}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, libsh::{ error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, @@ -19,7 +19,7 @@ use crate::{ prelude::*, procio::{IoMode, IoStack}, state::{ - self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars, + self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars }, }; @@ -429,10 +429,13 @@ impl Dispatcher { 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); + log::debug!("[case] testing pattern {:?} against input {:?}", pattern, pattern_raw); + let pattern_exp = Expander::from_raw(pattern)?.expand()?.join(" "); + log::debug!("[case] expanded pattern: {:?}", pattern_exp); + let pattern_regex = glob_to_regex(&pattern_exp, false); + log::debug!("[case] testing input {:?} against pattern {:?} (regex: {:?})", pattern_raw, pattern_exp, pattern_regex); if pattern_regex.is_match(&pattern_raw) { - log::debug!("[case] matched pattern {:?}", pattern); + log::debug!("[case] matched pattern {:?}", pattern_exp); for node in &body { s.dispatch_node(node.clone())?; } @@ -828,6 +831,7 @@ impl Dispatcher { "getopts" => getopts(cmd), "keymap" => keymap::keymap(cmd), "read_key" => read::read_key(cmd), + "autocmd" => autocmd(cmd), "true" | ":" => { state::set_status(0); Ok(()) diff --git a/src/readline/complete.rs b/src/readline/complete.rs index 317f11a..5a7d23e 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -3,7 +3,6 @@ use std::{ }; use nix::sys::signal::Signal; -use unicode_width::UnicodeWidthStr; use crate::{ builtin::complete::{CompFlags, CompOptFlags, CompOpts}, @@ -823,13 +822,13 @@ impl Completer for FuzzyCompleter { } K(C::Tab, M::SHIFT) | K(C::Up, M::NONE) => { - self.cursor.sub(1); + self.cursor.wrap_sub(1); self.update_scroll_offset(); Ok(CompResponse::Consumed) } K(C::Tab, M::NONE) | K(C::Down, M::NONE) => { - self.cursor.add(1); + self.cursor.wrap_add(1); self.update_scroll_offset(); Ok(CompResponse::Consumed) } @@ -848,7 +847,7 @@ impl Completer for FuzzyCompleter { // soft wraps and re-wraps as a flat buffer. let total_cells = layout.rows as u32 * layout.cols as u32; let physical_rows = if new_cols > 0 { - ((total_cells + new_cols as u32 - 1) / new_cols as u32) as u16 + total_cells.div_ceil(new_cols as u32) as u16 } else { layout.rows }; @@ -864,8 +863,7 @@ impl Completer for FuzzyCompleter { // filling to t_cols). Compute how many extra rows that adds // between the prompt cursor and the fuzzy content. let gap_extra = if new_cols > 0 && layout.preceding_line_width > new_cols { - let wrap_rows = ((layout.preceding_line_width as u32 + new_cols as u32 - 1) - / new_cols as u32) as u16; + let wrap_rows = (layout.preceding_line_width as u32).div_ceil(new_cols as u32) as u16; let cursor_wrap_row = layout.preceding_cursor_col / new_cols; wrap_rows.saturating_sub(cursor_wrap_row + 1) } else { @@ -975,7 +973,7 @@ impl Completer for FuzzyCompleter { let new_layout = FuzzyLayout { rows, - cols: cols as u16, + cols, cursor_col, preceding_line_width: self.prompt_line_width, preceding_cursor_col: self.prompt_cursor_col, diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index c222da4..12d215f 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_meta, write_vars}, + state::{VarFlags, VarKind, read_shopts, write_meta, write_vars}, }; const PUNCTUATION: [&str; 3] = ["?", "!", "."]; @@ -297,6 +297,20 @@ impl ClampedUsize { pub fn sub(&mut self, value: usize) { self.value = self.value.saturating_sub(value) } + pub fn wrap_add(&mut self, value: usize) { + self.value = self.ret_wrap_add(value); + } + pub fn wrap_sub(&mut self, value: usize) { + self.value = self.ret_wrap_sub(value); + } + pub fn ret_wrap_add(&self, value: usize) -> usize { + let max = self.upper_bound(); + (self.value + value) % (max + 1) + } + pub fn ret_wrap_sub(&self, value: usize) -> usize { + let max = self.upper_bound(); + (self.value + (max + 1) - (value % (max + 1))) % (max + 1) + } /// Add a value to the wrapped usize, return the result /// /// Returns the result instead of mutating the inner value @@ -2774,7 +2788,7 @@ impl LineBuf { match content { RegisterContent::Span(ref text) => { let insert_idx = match anchor { - Anchor::After => self.cursor.ret_add(1), + Anchor::After => self.cursor.get().saturating_add(1).min(self.grapheme_indices().len()), Anchor::Before => self.cursor.get(), }; self.insert_str_at(insert_idx, text); diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 29eb63e..a368f09 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -10,12 +10,13 @@ use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVis use crate::builtin::keymap::{KeyMapFlags, KeyMapMatch}; use crate::expand::expand_prompt; use crate::libsh::sys::TTY_FILENO; +use crate::libsh::utils::AutoCmdVecUtils; use crate::parse::lex::{LexStream, QuoteState}; 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, write_meta}; +use crate::state::{AutoCmdKind, ShellParam, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta, write_vars}; use crate::{ libsh::error::ShResult, parse::lex::{self, LexFlags, Tk, TkFlags, TkRule}, @@ -251,6 +252,8 @@ impl ShedVi { history: History::new()?, needs_redraw: true, }; + write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(new.mode.report_mode().to_string()), VarFlags::NONE))?; + new.prompt.refresh()?; new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline new.print_line(false)?; Ok(new) @@ -716,6 +719,9 @@ impl ShedVi { self.writer.clear_rows(layout)?; } + let pre_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PrePrompt)); + pre_prompt.exec(); + self .writer .redraw(self.prompt.get_ps1(), &line, &new_layout)?; @@ -786,15 +792,28 @@ impl ShedVi { self.old_layout = Some(new_layout); self.needs_redraw = false; - // Save physical cursor row so SIGWINCH can restore it + + let post_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PostPrompt)); + post_prompt.exec(); + Ok(()) } + pub fn swap_mode(&mut self, mode: &mut Box) { + std::mem::swap(&mut self.mode, mode); + self.editor.set_cursor_clamp(self.mode.clamp_cursor()); + write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok(); + self.prompt.refresh().ok(); + } + pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> { let mut select_mode = None; let mut is_insert_mode = false; if cmd.is_mode_transition() { let count = cmd.verb_count(); + let pre_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PreModeChange)); + pre_mode_change.exec(); + let mut mode: Box = if let ModeReport::Ex = self.mode.report_mode() && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) { if let Some(saved) = self.saved_mode.take() { saved @@ -823,8 +842,7 @@ impl ShedVi { .start_selecting(SelectMode::Char(SelectAnchor::End)); } let mut mode: Box = Box::new(ViVisual::new()); - std::mem::swap(&mut mode, &mut self.mode); - self.editor.set_cursor_clamp(self.mode.clamp_cursor()); + self.swap_mode(&mut mode); return self.editor.exec_cmd(cmd); } @@ -842,10 +860,12 @@ impl ShedVi { }; - std::mem::swap(&mut mode, &mut self.mode); + self.swap_mode(&mut mode); if self.mode.report_mode() == ModeReport::Ex { self.saved_mode = Some(mode); + write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE))?; + self.prompt.refresh()?; return Ok(()); } @@ -868,6 +888,13 @@ impl ShedVi { } else { self.editor.clear_insert_mode_start_pos(); } + + write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE))?; + self.prompt.refresh()?; + + let post_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PostModeChange)); + post_mode_change.exec(); + return Ok(()); } else if cmd.is_cmd_repeat() { let Some(replay) = self.repeat_action.clone() else { @@ -945,7 +972,7 @@ impl ShedVi { && 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); + self.swap_mode(&mut mode); } if cmd.is_repeatable() { @@ -970,7 +997,7 @@ impl ShedVi { if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) { self.editor.stop_selecting(); let mut mode: Box = Box::new(ViNormal::new()); - std::mem::swap(&mut mode, &mut self.mode); + self.swap_mode(&mut mode); } if self.mode.report_mode() != ModeReport::Visual && self.editor.select_range().is_some() { @@ -987,8 +1014,7 @@ impl ShedVi { } else { Box::new(ViNormal::new()) }; - std::mem::swap(&mut mode, &mut self.mode); - self.editor.set_cursor_clamp(self.mode.clamp_cursor()); + self.swap_mode(&mut mode); } Ok(()) diff --git a/src/readline/vimode/mod.rs b/src/readline/vimode/mod.rs index 49767c5..959a31b 100644 --- a/src/readline/vimode/mod.rs +++ b/src/readline/vimode/mod.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use unicode_segmentation::UnicodeSegmentation; use crate::libsh::error::ShResult; @@ -28,6 +30,19 @@ pub enum ModeReport { Unknown, } +impl Display for ModeReport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ModeReport::Insert => write!(f, "INSERT"), + ModeReport::Normal => write!(f, "NORMAL"), + ModeReport::Ex => write!(f, "COMMAND"), + ModeReport::Visual => write!(f, "VISUAL"), + ModeReport::Replace => write!(f, "REPLACE"), + ModeReport::Unknown => write!(f, "UNKNOWN"), + } + } +} + #[derive(Debug, Clone)] pub enum CmdReplay { ModeReplay { cmds: Vec, repeat: u16 }, diff --git a/src/signal.rs b/src/signal.rs index 0f469fd..9ff2f9c 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -8,7 +8,7 @@ use crate::{ libsh::error::{ShErr, ShErrKind, ShResult}, parse::execute::exec_input, prelude::*, - state::{read_jobs, read_logic, write_jobs, write_meta}, + state::{AutoCmd, AutoCmdKind, read_jobs, read_logic, write_jobs, write_meta}, }; static SIGNALS: AtomicU64 = AtomicU64::new(0); @@ -150,7 +150,7 @@ pub fn sig_setup(is_login: bool) { if is_login { - setpgid(Pid::from_raw(0), Pid::from_raw(0)); + let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0)); take_term().ok(); } } @@ -307,15 +307,29 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> { { if is_fg { take_term()?; - } else { - JOB_DONE.store(true, Ordering::SeqCst); - let job_order = read_jobs(|j| j.order().to_vec()); - let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); - if let Some(job) = result { - let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string(); - write_meta(|m| m.post_system_message(job_complete_msg)) - } - } + } else { + JOB_DONE.store(true, Ordering::SeqCst); + let job_order = read_jobs(|j| j.order().to_vec()); + let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); + if let Some(job) = result { + let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string(); + + let post_job_hooks = read_logic(|l| l.get_autocmds(AutoCmdKind::OnJobFinish)); + for cmd in post_job_hooks { + let AutoCmd { pattern, command } = cmd; + if let Some(pat) = pattern + && job.get_cmds().iter().all(|p| !pat.is_match(p)) { + continue; + } + + if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) { + e.print_error(); + } + } + + write_meta(|m| m.post_system_message(job_complete_msg)) + } + } } Ok(()) } diff --git a/src/state.rs b/src/state.rs index d0f0677..298e2af 100644 --- a/src/state.rs +++ b/src/state.rs @@ -9,11 +9,11 @@ use std::{ }; use nix::unistd::{User, gethostname, getppid}; +use regex::Regex; use crate::{ builtin::{BUILTINS, keymap::{KeyMap, KeyMapFlags, KeyMapMatch}, map::MapNode, trap::TrapTarget}, exec_input, expand::expand_keymap, jobs::JobTab, libsh::{ - error::{ShErr, ShErrKind, ShResult}, - utils::VecDequeExt, + error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt }, parse::{ ConjunctNode, NdRule, Node, ParsedSrc, lex::{LexFlags, LexStream, Span, Tk}, @@ -522,6 +522,62 @@ impl ShFunc { } } +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum AutoCmdKind { + PreCmd, + PostCmd, + PreChangeDir, + PostChangeDir, + OnJobFinish, + PrePrompt, + PostPrompt, + PreModeChange, + PostModeChange +} + +impl Display for AutoCmdKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PreCmd => write!(f, "pre-cmd"), + Self::PostCmd => write!(f, "post-cmd"), + Self::PreChangeDir => write!(f, "pre-change-dir"), + Self::PostChangeDir => write!(f, "post-change-dir"), + Self::OnJobFinish => write!(f, "on-job-finish"), + Self::PrePrompt => write!(f, "pre-prompt"), + Self::PostPrompt => write!(f, "post-prompt"), + Self::PreModeChange => write!(f, "pre-mode-change"), + Self::PostModeChange => write!(f, "post-mode-change"), + } + } +} + +impl FromStr for AutoCmdKind { + type Err = ShErr; + fn from_str(s: &str) -> Result { + match s { + "pre-cmd" => Ok(Self::PreCmd), + "post-cmd" => Ok(Self::PostCmd), + "pre-change-dir" => Ok(Self::PreChangeDir), + "post-change-dir" => Ok(Self::PostChangeDir), + "on-job-finish" => Ok(Self::OnJobFinish), + "pre-prompt" => Ok(Self::PrePrompt), + "post-prompt" => Ok(Self::PostPrompt), + "pre-mode-change" => Ok(Self::PreModeChange), + "post-mode-change" => Ok(Self::PostModeChange), + _ => Err(ShErr::simple( + ShErrKind::ParseErr, + format!("Invalid autocmd kind: {}", s), + )), + } + } +} + +#[derive(Clone, Debug)] +pub struct AutoCmd { + pub pattern: Option, + pub command: String, +} + /// The logic table for the shell /// /// Contains aliases and functions @@ -530,13 +586,35 @@ pub struct LogTab { functions: HashMap, aliases: HashMap, traps: HashMap, - keymaps: Vec + keymaps: Vec, + autocmds: HashMap> } impl LogTab { pub fn new() -> Self { Self::default() } + pub fn autocmds(&self) -> &HashMap> { + &self.autocmds + } + pub fn autocmds_mut(&mut self) -> &mut HashMap> { + &mut self.autocmds + } + pub fn insert_autocmd(&mut self, kind: AutoCmdKind, cmd: AutoCmd) { + self.autocmds.entry(kind).or_default().push(cmd); + } + pub fn get_autocmds(&self, kind: AutoCmdKind) -> Vec { + self.autocmds.get(&kind).cloned().unwrap_or_default() + } + pub fn clear_autocmds(&mut self, kind: AutoCmdKind) { + self.autocmds.remove(&kind); + } + pub fn keymaps(&self) -> &Vec { + &self.keymaps + } + pub fn keymaps_mut(&mut self) -> &mut Vec { + &mut self.keymaps + } pub fn insert_keymap(&mut self, keymap: KeyMap) { let mut found_dup = false; for map in self.keymaps.iter_mut() { @@ -797,6 +875,18 @@ impl Display for Var { } } +impl From for Var { + fn from(value: String) -> Self { + Self::new(VarKind::Str(value), VarFlags::NONE) + } +} + +impl From<&str> for Var { + fn from(value: &str) -> Self { + Self::new(VarKind::Str(value.into()), VarFlags::NONE) + } +} + #[derive(Default, Clone, Debug)] pub struct VarTab { vars: HashMap, @@ -1496,6 +1586,65 @@ pub fn get_shopt(path: &str) -> String { read_shopts(|s| s.get(path)).unwrap().unwrap() } +pub fn with_vars(vars: H, f: F) -> T +where + F: FnOnce() -> T, + H: Into>, + V: Into { + + let snapshot = read_vars(|v| v.clone()); + let vars = vars.into(); + for (name, val) in vars { + let val = val.into(); + write_vars(|v| v.set_var(&name, val.kind, val.flags).unwrap()); + } + let _guard = scopeguard::guard(snapshot, |snap| { + write_vars(|v| *v = snap); + }); + f() +} + +pub fn change_dir>(dir: P) -> ShResult<()> { + let dir = dir.as_ref(); + let dir_raw = &dir.display().to_string(); + let pre_cd = read_logic(|l| l.get_autocmds(AutoCmdKind::PreChangeDir)); + let post_cd = read_logic(|l| l.get_autocmds(AutoCmdKind::PostChangeDir)); + + let current_dir = env::current_dir()?.display().to_string(); + with_vars([("_NEW_DIR".into(), dir_raw.as_str()), ("_OLD_DIR".into(), current_dir.as_str())], || { + for cmd in pre_cd { + let AutoCmd { command, pattern } = cmd; + if let Some(pat) = pattern + && !pat.is_match(dir_raw) { + continue; + } + + if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd (pre-changedir)".to_string())) { + e.print_error(); + }; + } + }); + + + env::set_current_dir(dir)?; + + with_vars([("_NEW_DIR".into(), dir_raw.as_str()), ("_OLD_DIR".into(), current_dir.as_str())], || { + for cmd in post_cd { + let AutoCmd { command, pattern } = cmd; + if let Some(pat) = pattern + && !pat.is_match(dir_raw) { + continue; + } + + if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd (post-changedir)".to_string())) { + e.print_error(); + }; + } + }); + + Ok(()) +} + pub fn get_status() -> i32 { read_vars(|v| v.get_param(ShellParam::Status)) .parse::()