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

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

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,13 +586,35 @@ 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;
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)]
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>()