Implemented the autocmd builtin, which allows you to register hooks for certain shell events.
This commit is contained in:
91
src/builtin/autocmd.rs
Normal file
91
src/builtin/autocmd.rs
Normal 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(())
|
||||
}
|
||||
@@ -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))));
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
);
|
||||
|
||||
@@ -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! {
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>>()
|
||||
|
||||
13
src/main.rs
13
src/main.rs
@@ -31,12 +31,13 @@ use crate::builtin::keymap::KeyMapMatch;
|
||||
use crate::builtin::trap::TrapTarget;
|
||||
use crate::libsh::error::{self, ShErr, ShErrKind, ShResult};
|
||||
use crate::libsh::sys::TTY_FILENO;
|
||||
use crate::libsh::utils::AutoCmdVecUtils;
|
||||
use crate::parse::execute::exec_input;
|
||||
use crate::prelude::*;
|
||||
use crate::readline::term::{LineWriter, RawModeGuard, raw_mode};
|
||||
use crate::readline::{Prompt, ReadlineEvent, ShedVi};
|
||||
use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending};
|
||||
use crate::state::{read_logic, source_rc, write_jobs, write_meta};
|
||||
use crate::state::{AutoCmdKind, read_logic, source_rc, write_jobs, write_meta};
|
||||
use clap::Parser;
|
||||
use state::{read_vars, write_vars};
|
||||
|
||||
@@ -357,9 +358,14 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
|
||||
// Process any available input
|
||||
match readline.process_input() {
|
||||
Ok(ReadlineEvent::Line(input)) => {
|
||||
let pre_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd));
|
||||
let post_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd));
|
||||
|
||||
pre_exec.exec_with(&input);
|
||||
|
||||
let start = Instant::now();
|
||||
write_meta(|m| m.start_timer());
|
||||
if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true, Some("<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")?;
|
||||
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
155
src/state.rs
155
src/state.rs
@@ -9,11 +9,11 @@ use std::{
|
||||
};
|
||||
|
||||
use nix::unistd::{User, gethostname, getppid};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::{
|
||||
builtin::{BUILTINS, keymap::{KeyMap, KeyMapFlags, KeyMapMatch}, map::MapNode, trap::TrapTarget}, exec_input, expand::expand_keymap, jobs::JobTab, libsh::{
|
||||
error::{ShErr, ShErrKind, ShResult},
|
||||
utils::VecDequeExt,
|
||||
error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt
|
||||
}, parse::{
|
||||
ConjunctNode, NdRule, Node, ParsedSrc,
|
||||
lex::{LexFlags, LexStream, Span, Tk},
|
||||
@@ -522,6 +522,62 @@ impl ShFunc {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum AutoCmdKind {
|
||||
PreCmd,
|
||||
PostCmd,
|
||||
PreChangeDir,
|
||||
PostChangeDir,
|
||||
OnJobFinish,
|
||||
PrePrompt,
|
||||
PostPrompt,
|
||||
PreModeChange,
|
||||
PostModeChange
|
||||
}
|
||||
|
||||
impl Display for AutoCmdKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::PreCmd => write!(f, "pre-cmd"),
|
||||
Self::PostCmd => write!(f, "post-cmd"),
|
||||
Self::PreChangeDir => write!(f, "pre-change-dir"),
|
||||
Self::PostChangeDir => write!(f, "post-change-dir"),
|
||||
Self::OnJobFinish => write!(f, "on-job-finish"),
|
||||
Self::PrePrompt => write!(f, "pre-prompt"),
|
||||
Self::PostPrompt => write!(f, "post-prompt"),
|
||||
Self::PreModeChange => write!(f, "pre-mode-change"),
|
||||
Self::PostModeChange => write!(f, "post-mode-change"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AutoCmdKind {
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<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>()
|
||||
|
||||
Reference in New Issue
Block a user