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 ba2301fd38
commit fbadbebf8c
17 changed files with 486 additions and 103 deletions

96
Cargo.lock generated
View File

@@ -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"

View File

@@ -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
];

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()))));
}
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))));
}

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

View File

@@ -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! {

View File

@@ -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<()> {

View File

@@ -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}

View File

@@ -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<T> {
fn to_vec(self) -> Vec<T>;
@@ -22,6 +24,11 @@ pub trait TkVecUtils<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> {
/// Splits the vector of redirections into two vectors
///
@@ -33,6 +40,31 @@ pub trait NodeVecUtils<Node> {
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> {
fn to_vec(self) -> 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::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("<stdin>".into()))) {
if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input.clone(), None, true, Some("<stdin>".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")?;

View File

@@ -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(())

View File

@@ -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,

View File

@@ -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);

View File

@@ -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)
@@ -294,7 +297,7 @@ impl ShedVi {
// so print_line can call clear_rows with the full multi-line layout
self.prompt = Prompt::new();
self.editor = Default::default();
self.mode = Box::new(ViInsert::new());
self.swap_mode(&mut (Box::new(ViInsert::new()) as Box<dyn ViMode>));
self.needs_redraw = true;
if full_redraw {
self.old_layout = None;
@@ -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<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<()> {
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<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() {
saved
@@ -823,8 +842,7 @@ impl ShedVi {
.start_selecting(SelectMode::Char(SelectAnchor::End));
}
let mut mode: Box<dyn ViMode> = 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<dyn ViMode> = 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<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() {
@@ -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(())

View File

@@ -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<ViCmd>, repeat: u16 },

View File

@@ -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();
}
}
@@ -313,6 +313,20 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
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))
}
}

View File

@@ -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<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
///
/// Contains aliases and functions
@@ -530,12 +586,34 @@ pub struct LogTab {
functions: HashMap<String, ShFunc>,
aliases: HashMap<String, ShAlias>,
traps: HashMap<TrapTarget, String>,
keymaps: Vec<KeyMap>
keymaps: Vec<KeyMap>,
autocmds: HashMap<AutoCmdKind, Vec<AutoCmd>>
}
impl LogTab {
pub fn new() -> Self {
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) {
let mut found_dup = false;
@@ -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)]
pub struct VarTab {
vars: HashMap<String, Var>,
@@ -1496,6 +1586,65 @@ pub fn get_shopt(path: &str) -> String {
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 {
read_vars(|v| v.get_param(ShellParam::Status))
.parse::<i32>()