Implemented the autocmd builtin, which allows you to register hooks for certain shell events.

This commit is contained in:
2026-03-04 12:55:50 -05:00
parent 210b57b992
commit 11ff256815
17 changed files with 482 additions and 102 deletions

96
Cargo.lock generated
View File

@@ -85,9 +85,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.10.0" version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@@ -114,9 +114,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.55" version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785" checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -124,9 +124,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.55" version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61" checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -148,9 +148,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.7.7" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
@@ -250,21 +250,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.4" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
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"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@@ -321,9 +309,9 @@ dependencies = [
[[package]] [[package]]
name = "insta" name = "insta"
version = "1.46.1" version = "1.46.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248b42847813a1550dafd15296fd9748c651d0c32194559dbc05d804d54b21e8" checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4"
dependencies = [ dependencies = [
"console", "console",
"once_cell", "once_cell",
@@ -354,9 +342,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]] [[package]]
name = "jiff" name = "jiff"
version = "0.2.20" version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
dependencies = [ dependencies = [
"jiff-static", "jiff-static",
"log", "log",
@@ -367,9 +355,9 @@ dependencies = [
[[package]] [[package]]
name = "jiff-static" name = "jiff-static"
version = "0.2.20" version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -384,15 +372,15 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.180" version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.11.0" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "log" name = "log"
@@ -402,9 +390,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.6" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]] [[package]]
name = "nix" name = "nix"
@@ -476,18 +464,18 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.44" version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]] [[package]]
name = "r-efi" name = "r-efi"
version = "5.3.0" version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]] [[package]]
name = "rand" name = "rand"
@@ -496,7 +484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
dependencies = [ dependencies = [
"chacha20", "chacha20",
"getrandom 0.4.1", "getrandom",
"rand_core", "rand_core",
] ]
@@ -508,9 +496,9 @@ checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.12.2" version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -520,9 +508,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.13" version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -531,15 +519,15 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.8" version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.1.3" version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
@@ -641,9 +629,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.114" version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -652,12 +640,12 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.24.0" version = "3.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.3.4", "getrandom",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@@ -665,9 +653,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.22" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"

View File

@@ -5,9 +5,17 @@ let
boolToString = b: boolToString = b:
if b then "true" else "false"; if b then "true" else "false";
mkFunctionDef = name: body: '' mkAutoCmd = cfg:
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}() { ${name}() {
${body} ${indented}
}''; }'';
mkKeymapCmd = cfg: let mkKeymapCmd = cfg: let
@@ -58,6 +66,36 @@ in
description = "Shell functions to set when shed starts"; 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";
};
};
});
};
keymaps = lib.mkOption { keymaps = lib.mkOption {
type = lib.types.listOf (lib.types.submodule { type = lib.types.listOf (lib.types.submodule {
options = { options = {
@@ -243,6 +281,7 @@ in
completeLines = lib.concatLines (lib.mapAttrsToList mkCompleteCmd cfg.extraCompletion); completeLines = lib.concatLines (lib.mapAttrsToList mkCompleteCmd cfg.extraCompletion);
keymapLines = lib.concatLines (map mkKeymapCmd cfg.keymaps); keymapLines = lib.concatLines (map mkKeymapCmd cfg.keymaps);
functionLines = lib.concatLines (lib.mapAttrsToList mkFunctionDef cfg.functions); functionLines = lib.concatLines (lib.mapAttrsToList mkFunctionDef cfg.functions);
autocmdLines = lib.concatLines (map mkAutoCmd cfg.autocmds);
in in
lib.mkIf cfg.enable { lib.mkIf cfg.enable {
home.packages = [ cfg.package ]; home.packages = [ cfg.package ];
@@ -269,6 +308,7 @@ in
functionLines functionLines
completeLines completeLines
keymapLines keymapLines
autocmdLines
]) ])
cfg.settings.extraPostConfig cfg.settings.extraPostConfig
]; ];

91
src/builtin/autocmd.rs Normal file
View File

@@ -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<Regex>,
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<AutoCmdOpts> {
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::<AutoCmdKind>() 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(())
}

View File

@@ -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())))); .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()) return Err(ShErr::new(ShErrKind::ExecFail, span.clone())
.labeled(cd_span.clone(), format!("cd: Failed to change directory: '{}'", e.fg(Color::Red)))); .labeled(cd_span.clone(), format!("cd: Failed to change directory: '{}'", e.fg(Color::Red))));
} }

View File

@@ -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( return Err(
ShErr::at(ShErrKind::ExecFail, blame, format!("Failed to change directory: '{}'", e.fg(Color::Red))) ShErr::at(ShErrKind::ExecFail, blame, format!("Failed to change directory: '{}'", e.fg(Color::Red)))
); );

View File

@@ -1,5 +1,5 @@
use crate::{ 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! { bitflags! {

View File

@@ -26,13 +26,14 @@ pub mod arrops;
pub mod intro; pub mod intro;
pub mod getopts; pub mod getopts;
pub mod keymap; 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", "echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown",
"alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin",
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type", "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<()> { pub fn true_builtin() -> ShResult<()> {

View File

@@ -4,7 +4,6 @@ use nix::{
libc::{STDIN_FILENO, STDOUT_FILENO}, libc::{STDIN_FILENO, STDOUT_FILENO},
unistd::{isatty, read, write}, unistd::{isatty, read, write},
}; };
use yansi::Paint;
use crate::{ 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} 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}

View File

@@ -2,9 +2,11 @@ use std::collections::VecDeque;
use ariadne::Span as AriadneSpan; use ariadne::Span as AriadneSpan;
use crate::parse::execute::exec_input;
use crate::parse::lex::{Span, Tk, TkRule}; use crate::parse::lex::{Span, Tk, TkRule};
use crate::parse::{Node, Redir, RedirType}; use crate::parse::{Node, Redir, RedirType};
use crate::prelude::*; use crate::prelude::*;
use crate::state::AutoCmd;
pub trait VecDequeExt<T> { pub trait VecDequeExt<T> {
fn to_vec(self) -> Vec<T>; fn to_vec(self) -> Vec<T>;
@@ -22,6 +24,11 @@ pub trait TkVecUtils<Tk> {
fn split_at_separators(&self) -> Vec<Vec<Tk>>; fn split_at_separators(&self) -> Vec<Vec<Tk>>;
} }
pub trait AutoCmdVecUtils {
fn exec(&self);
fn exec_with(&self, pattern: &str);
}
pub trait RedirVecUtils<Redir> { pub trait RedirVecUtils<Redir> {
/// Splits the vector of redirections into two vectors /// Splits the vector of redirections into two vectors
/// ///
@@ -33,6 +40,31 @@ pub trait NodeVecUtils<Node> {
fn get_span(&self) -> Option<Span>; fn get_span(&self) -> Option<Span>;
} }
impl AutoCmdVecUtils for Vec<AutoCmd> {
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<T> VecDequeExt<T> for VecDeque<T> { impl<T> VecDequeExt<T> for VecDeque<T> {
fn to_vec(self) -> Vec<T> { fn to_vec(self) -> Vec<T> {
self.into_iter().collect::<Vec<T>>() self.into_iter().collect::<Vec<T>>()

View File

@@ -31,12 +31,13 @@ use crate::builtin::keymap::KeyMapMatch;
use crate::builtin::trap::TrapTarget; use crate::builtin::trap::TrapTarget;
use crate::libsh::error::{self, ShErr, ShErrKind, ShResult}; use crate::libsh::error::{self, ShErr, ShErrKind, ShResult};
use crate::libsh::sys::TTY_FILENO; use crate::libsh::sys::TTY_FILENO;
use crate::libsh::utils::AutoCmdVecUtils;
use crate::parse::execute::exec_input; use crate::parse::execute::exec_input;
use crate::prelude::*; use crate::prelude::*;
use crate::readline::term::{LineWriter, RawModeGuard, raw_mode}; use crate::readline::term::{LineWriter, RawModeGuard, raw_mode};
use crate::readline::{Prompt, ReadlineEvent, ShedVi}; use crate::readline::{Prompt, ReadlineEvent, ShedVi};
use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending}; 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 clap::Parser;
use state::{read_vars, write_vars}; use state::{read_vars, write_vars};
@@ -357,9 +358,14 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
// Process any available input // Process any available input
match readline.process_input() { match readline.process_input() {
Ok(ReadlineEvent::Line(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(); let start = Instant::now();
write_meta(|m| m.start_timer()); write_meta(|m| m.start_timer());
if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true, Some("<stdin>".into()))) { if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input.clone(), None, true, Some("<stdin>".into()))) {
match e.kind() { match e.kind() {
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
@@ -371,6 +377,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
let command_run_time = start.elapsed(); let command_run_time = start.elapsed();
log::info!("Command executed in {:.2?}", command_run_time); log::info!("Command executed in {:.2?}", command_run_time);
write_meta(|m| m.stop_timer()); write_meta(|m| m.stop_timer());
post_exec.exec_with(&input);
readline.fix_column()?; readline.fix_column()?;
readline.writer.flush_write("\n\r")?; readline.writer.flush_write("\n\r")?;

View File

@@ -7,9 +7,9 @@ use ariadne::Fmt;
use crate::{ use crate::{
builtin::{ 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}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
libsh::{ libsh::{
error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color},
@@ -19,7 +19,7 @@ use crate::{
prelude::*, prelude::*,
procio::{IoMode, IoStack}, procio::{IoMode, IoStack},
state::{ 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('|'); let block_patterns = block_pattern_raw.split('|');
for pattern in block_patterns { for pattern in block_patterns {
let pattern_regex = glob_to_regex(pattern, false); log::debug!("[case] testing pattern {:?} against input {:?}", pattern, pattern_raw);
log::debug!("[case] testing input {:?} against pattern {:?} (regex: {:?})", pattern_raw, pattern, pattern_regex); 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) { if pattern_regex.is_match(&pattern_raw) {
log::debug!("[case] matched pattern {:?}", pattern); log::debug!("[case] matched pattern {:?}", pattern_exp);
for node in &body { for node in &body {
s.dispatch_node(node.clone())?; s.dispatch_node(node.clone())?;
} }
@@ -828,6 +831,7 @@ impl Dispatcher {
"getopts" => getopts(cmd), "getopts" => getopts(cmd),
"keymap" => keymap::keymap(cmd), "keymap" => keymap::keymap(cmd),
"read_key" => read::read_key(cmd), "read_key" => read::read_key(cmd),
"autocmd" => autocmd(cmd),
"true" | ":" => { "true" | ":" => {
state::set_status(0); state::set_status(0);
Ok(()) Ok(())

View File

@@ -3,7 +3,6 @@ use std::{
}; };
use nix::sys::signal::Signal; use nix::sys::signal::Signal;
use unicode_width::UnicodeWidthStr;
use crate::{ use crate::{
builtin::complete::{CompFlags, CompOptFlags, CompOpts}, builtin::complete::{CompFlags, CompOptFlags, CompOpts},
@@ -823,13 +822,13 @@ impl Completer for FuzzyCompleter {
} }
K(C::Tab, M::SHIFT) | K(C::Tab, M::SHIFT) |
K(C::Up, M::NONE) => { K(C::Up, M::NONE) => {
self.cursor.sub(1); self.cursor.wrap_sub(1);
self.update_scroll_offset(); self.update_scroll_offset();
Ok(CompResponse::Consumed) Ok(CompResponse::Consumed)
} }
K(C::Tab, M::NONE) | K(C::Tab, M::NONE) |
K(C::Down, M::NONE) => { K(C::Down, M::NONE) => {
self.cursor.add(1); self.cursor.wrap_add(1);
self.update_scroll_offset(); self.update_scroll_offset();
Ok(CompResponse::Consumed) Ok(CompResponse::Consumed)
} }
@@ -848,7 +847,7 @@ impl Completer for FuzzyCompleter {
// soft wraps and re-wraps as a flat buffer. // soft wraps and re-wraps as a flat buffer.
let total_cells = layout.rows as u32 * layout.cols as u32; let total_cells = layout.rows as u32 * layout.cols as u32;
let physical_rows = if new_cols > 0 { 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 { } else {
layout.rows layout.rows
}; };
@@ -864,8 +863,7 @@ impl Completer for FuzzyCompleter {
// filling to t_cols). Compute how many extra rows that adds // filling to t_cols). Compute how many extra rows that adds
// between the prompt cursor and the fuzzy content. // between the prompt cursor and the fuzzy content.
let gap_extra = if new_cols > 0 && layout.preceding_line_width > new_cols { 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) let wrap_rows = (layout.preceding_line_width as u32).div_ceil(new_cols as u32) as u16;
/ new_cols as u32) as u16;
let cursor_wrap_row = layout.preceding_cursor_col / new_cols; let cursor_wrap_row = layout.preceding_cursor_col / new_cols;
wrap_rows.saturating_sub(cursor_wrap_row + 1) wrap_rows.saturating_sub(cursor_wrap_row + 1)
} else { } else {
@@ -975,7 +973,7 @@ impl Completer for FuzzyCompleter {
let new_layout = FuzzyLayout { let new_layout = FuzzyLayout {
rows, rows,
cols: cols as u16, cols,
cursor_col, cursor_col,
preceding_line_width: self.prompt_line_width, preceding_line_width: self.prompt_line_width,
preceding_cursor_col: self.prompt_cursor_col, preceding_cursor_col: self.prompt_cursor_col,

View File

@@ -18,7 +18,7 @@ use crate::{
register::{RegisterContent, write_register}, register::{RegisterContent, write_register},
term::RawModeGuard, 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] = ["?", "!", "."]; const PUNCTUATION: [&str; 3] = ["?", "!", "."];
@@ -297,6 +297,20 @@ impl ClampedUsize {
pub fn sub(&mut self, value: usize) { pub fn sub(&mut self, value: usize) {
self.value = self.value.saturating_sub(value) 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 /// Add a value to the wrapped usize, return the result
/// ///
/// Returns the result instead of mutating the inner value /// Returns the result instead of mutating the inner value
@@ -2774,7 +2788,7 @@ impl LineBuf {
match content { match content {
RegisterContent::Span(ref text) => { RegisterContent::Span(ref text) => {
let insert_idx = match anchor { 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(), Anchor::Before => self.cursor.get(),
}; };
self.insert_str_at(insert_idx, text); self.insert_str_at(insert_idx, text);

View File

@@ -10,12 +10,13 @@ use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVis
use crate::builtin::keymap::{KeyMapFlags, KeyMapMatch}; use crate::builtin::keymap::{KeyMapFlags, KeyMapMatch};
use crate::expand::expand_prompt; use crate::expand::expand_prompt;
use crate::libsh::sys::TTY_FILENO; use crate::libsh::sys::TTY_FILENO;
use crate::libsh::utils::AutoCmdVecUtils;
use crate::parse::lex::{LexStream, QuoteState}; use crate::parse::lex::{LexStream, QuoteState};
use crate::{prelude::*, state}; use crate::{prelude::*, state};
use crate::readline::complete::FuzzyCompleter; use crate::readline::complete::FuzzyCompleter;
use crate::readline::term::{Pos, TermReader, calc_str_width}; use crate::readline::term::{Pos, TermReader, calc_str_width};
use crate::readline::vimode::ViEx; 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::{ use crate::{
libsh::error::ShResult, libsh::error::ShResult,
parse::lex::{self, LexFlags, Tk, TkFlags, TkRule}, parse::lex::{self, LexFlags, Tk, TkFlags, TkRule},
@@ -251,6 +252,8 @@ impl ShedVi {
history: History::new()?, history: History::new()?,
needs_redraw: true, 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.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)?; new.print_line(false)?;
Ok(new) Ok(new)
@@ -716,6 +719,9 @@ impl ShedVi {
self.writer.clear_rows(layout)?; self.writer.clear_rows(layout)?;
} }
let pre_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PrePrompt));
pre_prompt.exec();
self self
.writer .writer
.redraw(self.prompt.get_ps1(), &line, &new_layout)?; .redraw(self.prompt.get_ps1(), &line, &new_layout)?;
@@ -786,15 +792,28 @@ impl ShedVi {
self.old_layout = Some(new_layout); self.old_layout = Some(new_layout);
self.needs_redraw = false; 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(()) Ok(())
} }
pub fn swap_mode(&mut self, mode: &mut Box<dyn ViMode>) {
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<()> { pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
let mut select_mode = None; let mut select_mode = None;
let mut is_insert_mode = false; let mut is_insert_mode = false;
if cmd.is_mode_transition() { if cmd.is_mode_transition() {
let count = cmd.verb_count(); 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<dyn ViMode> = if let ModeReport::Ex = self.mode.report_mode() && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) { let mut mode: Box<dyn ViMode> = if let ModeReport::Ex = self.mode.report_mode() && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
if let Some(saved) = self.saved_mode.take() { if let Some(saved) = self.saved_mode.take() {
saved saved
@@ -823,8 +842,7 @@ impl ShedVi {
.start_selecting(SelectMode::Char(SelectAnchor::End)); .start_selecting(SelectMode::Char(SelectAnchor::End));
} }
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new()); let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
std::mem::swap(&mut mode, &mut self.mode); self.swap_mode(&mut mode);
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
return self.editor.exec_cmd(cmd); 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 { if self.mode.report_mode() == ModeReport::Ex {
self.saved_mode = Some(mode); 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(()); return Ok(());
} }
@@ -868,6 +888,13 @@ impl ShedVi {
} else { } else {
self.editor.clear_insert_mode_start_pos(); 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(()); return Ok(());
} else if cmd.is_cmd_repeat() { } else if cmd.is_cmd_repeat() {
let Some(replay) = self.repeat_action.clone() else { let Some(replay) = self.repeat_action.clone() else {
@@ -945,7 +972,7 @@ impl ShedVi {
&& self.editor.select_range().is_none() { && self.editor.select_range().is_none() {
self.editor.stop_selecting(); self.editor.stop_selecting();
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new()); let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
std::mem::swap(&mut mode, &mut self.mode); self.swap_mode(&mut mode);
} }
if cmd.is_repeatable() { 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()) { if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) {
self.editor.stop_selecting(); self.editor.stop_selecting();
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new()); let mut mode: Box<dyn ViMode> = 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() { if self.mode.report_mode() != ModeReport::Visual && self.editor.select_range().is_some() {
@@ -987,8 +1014,7 @@ impl ShedVi {
} else { } else {
Box::new(ViNormal::new()) Box::new(ViNormal::new())
}; };
std::mem::swap(&mut mode, &mut self.mode); self.swap_mode(&mut mode);
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
} }
Ok(()) Ok(())

View File

@@ -1,3 +1,5 @@
use std::fmt::Display;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use crate::libsh::error::ShResult; use crate::libsh::error::ShResult;
@@ -28,6 +30,19 @@ pub enum ModeReport {
Unknown, 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)] #[derive(Debug, Clone)]
pub enum CmdReplay { pub enum CmdReplay {
ModeReplay { cmds: Vec<ViCmd>, repeat: u16 }, ModeReplay { cmds: Vec<ViCmd>, repeat: u16 },

View File

@@ -8,7 +8,7 @@ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::execute::exec_input, parse::execute::exec_input,
prelude::*, 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); static SIGNALS: AtomicU64 = AtomicU64::new(0);
@@ -150,7 +150,7 @@ pub fn sig_setup(is_login: bool) {
if is_login { 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(); take_term().ok();
} }
} }
@@ -307,15 +307,29 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
{ {
if is_fg { if is_fg {
take_term()?; take_term()?;
} else { } else {
JOB_DONE.store(true, Ordering::SeqCst); JOB_DONE.store(true, Ordering::SeqCst);
let job_order = read_jobs(|j| j.order().to_vec()); let job_order = read_jobs(|j| j.order().to_vec());
let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned());
if let Some(job) = result { if let Some(job) = result {
let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string(); let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string();
write_meta(|m| m.post_system_message(job_complete_msg))
} 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(()) Ok(())
} }

View File

@@ -9,11 +9,11 @@ use std::{
}; };
use nix::unistd::{User, gethostname, getppid}; use nix::unistd::{User, gethostname, getppid};
use regex::Regex;
use crate::{ use crate::{
builtin::{BUILTINS, keymap::{KeyMap, KeyMapFlags, KeyMapMatch}, map::MapNode, trap::TrapTarget}, exec_input, expand::expand_keymap, 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}, error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt
utils::VecDequeExt,
}, parse::{ }, parse::{
ConjunctNode, NdRule, Node, ParsedSrc, ConjunctNode, NdRule, Node, ParsedSrc,
lex::{LexFlags, LexStream, Span, Tk}, 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<Self, Self::Err> {
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<Regex>,
pub command: String,
}
/// The logic table for the shell /// The logic table for the shell
/// ///
/// Contains aliases and functions /// Contains aliases and functions
@@ -530,13 +586,35 @@ pub struct LogTab {
functions: HashMap<String, ShFunc>, functions: HashMap<String, ShFunc>,
aliases: HashMap<String, ShAlias>, aliases: HashMap<String, ShAlias>,
traps: HashMap<TrapTarget, String>, traps: HashMap<TrapTarget, String>,
keymaps: Vec<KeyMap> keymaps: Vec<KeyMap>,
autocmds: HashMap<AutoCmdKind, Vec<AutoCmd>>
} }
impl LogTab { impl LogTab {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
pub fn autocmds(&self) -> &HashMap<AutoCmdKind, Vec<AutoCmd>> {
&self.autocmds
}
pub fn autocmds_mut(&mut self) -> &mut HashMap<AutoCmdKind, Vec<AutoCmd>> {
&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<AutoCmd> {
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<KeyMap> {
&self.keymaps
}
pub fn keymaps_mut(&mut self) -> &mut Vec<KeyMap> {
&mut self.keymaps
}
pub fn insert_keymap(&mut self, keymap: KeyMap) { pub fn insert_keymap(&mut self, keymap: KeyMap) {
let mut found_dup = false; let mut found_dup = false;
for map in self.keymaps.iter_mut() { for map in self.keymaps.iter_mut() {
@@ -797,6 +875,18 @@ impl Display for Var {
} }
} }
impl From<String> 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)] #[derive(Default, Clone, Debug)]
pub struct VarTab { pub struct VarTab {
vars: HashMap<String, Var>, vars: HashMap<String, Var>,
@@ -1496,6 +1586,65 @@ pub fn get_shopt(path: &str) -> String {
read_shopts(|s| s.get(path)).unwrap().unwrap() read_shopts(|s| s.get(path)).unwrap().unwrap()
} }
pub fn with_vars<F,H,V,T>(vars: H, f: F) -> T
where
F: FnOnce() -> T,
H: Into<HashMap<String,V>>,
V: Into<Var> {
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<P: AsRef<Path>>(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 { pub fn get_status() -> i32 {
read_vars(|v| v.get_param(ShellParam::Status)) read_vars(|v| v.get_param(ShellParam::Status))
.parse::<i32>() .parse::<i32>()