diff --git a/README.md b/README.md index ecffc29..203ce93 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # shed -A Linux shell written in Rust. The name is a nod to the original Unix utilities `sh` and `ed`. It's a shell with a heavy emphasis on smooth line editing. +A Linux shell written in Rust. The name is a nod to the original Unix utilities `sh` and `ed`. It's a shell with a heavy emphasis on smooth line editing and general interactive UX improvements over existing shells. Btw if you don't use `vim` this probably isn't your shell. shed @@ -40,6 +40,8 @@ gitbranch() { git branch --show-current 2>/dev/null; } export PS1='\u@\h \W \@gitbranch \$ ' ``` +If `shed` receives `SIGUSR1` while in interactive mode, it will refresh and redraw the prompt. This can be used to create asynchronous, dynamic prompt content. + Additionally, `echo` now has a `-p` flag that expands prompt escape sequences, similar to how the `-e` flag expands conventional escape sequences. --- diff --git a/src/builtin/help.rs b/src/builtin/help.rs index 4bc282d..4789fac 100644 --- a/src/builtin/help.rs +++ b/src/builtin/help.rs @@ -121,7 +121,11 @@ pub fn help(node: Node) -> ShResult<()> { } pub fn open_help(content: &str, line: Option, file_name: Option) -> ShResult<()> { - let pager = env::var("PAGER").unwrap_or("less -R".into()); + let pager = env::var("SHED_HPAGER") + .unwrap_or( + env::var("PAGER") + .unwrap_or("less -R".into()), + ); let line_arg = line.map(|ln| format!("+{ln}")).unwrap_or_default(); let prompt_arg = file_name .map(|name| format!("-Ps'{name}'")) diff --git a/src/jobs.rs b/src/jobs.rs index cca4252..743eea8 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -1,6 +1,7 @@ use std::collections::VecDeque; use ariadne::Fmt; +use nix::unistd::getpid; use scopeguard::defer; use yansi::Color; @@ -872,7 +873,12 @@ pub fn wait_fg(job: Job, interactive: bool) -> ShResult<()> { write_jobs(|j| j.fg_to_bg(*status))?; } WtStat::Signaled(_, sig, _) => { - if *sig == Signal::SIGTSTP { + if *sig == Signal::SIGINT { + // interrupt propagates to the shell + // necessary for interrupting stuff like + // while/for loops + kill(getpid(), Signal::SIGINT)?; + } else if *sig == Signal::SIGTSTP { was_stopped = true; write_jobs(|j| j.fg_to_bg(*status))?; } diff --git a/src/libsh/error.rs b/src/libsh/error.rs index 59f3989..250e9b2 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -459,7 +459,7 @@ pub enum ShErrKind { FuncReturn(i32), LoopContinue(i32), LoopBreak(i32), - ClearReadline, + Interrupt, Null, } @@ -471,7 +471,7 @@ impl ShErrKind { | Self::FuncReturn(_) | Self::LoopContinue(_) | Self::LoopBreak(_) - | Self::ClearReadline + | Self::Interrupt ) } } @@ -496,7 +496,7 @@ impl Display for ShErrKind { Self::LoopBreak(_) => "Syntax Error", Self::ReadlineErr => "Readline Error", Self::ExCommand => "Ex Command Error", - Self::ClearReadline => "", + Self::Interrupt => "", Self::Null => "", }; write!(f, "{output}") diff --git a/src/main.rs b/src/main.rs index fb84b2e..cc0e362 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,7 +38,7 @@ use crate::prelude::*; use crate::procio::borrow_fd; 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::signal::{GOT_SIGUSR1, GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending}; use crate::state::{ AutoCmdKind, read_logic, read_shopts, source_env, source_login, source_rc, write_jobs, write_meta, write_shopts, @@ -259,7 +259,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { while signals_pending() { if let Err(e) = check_signals() { match e.kind() { - ShErrKind::ClearReadline => { + ShErrKind::Interrupt => { // We got Ctrl+C - clear current input and redraw readline.reset_active_widget(false)?; } @@ -285,9 +285,16 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { readline.prompt_mut().refresh(); } + if GOT_SIGUSR1.swap(false, Ordering::SeqCst) { + log::info!("SIGUSR1 received: refreshing readline state"); + readline.mark_dirty(); + readline.prompt_mut().refresh(); + } + readline.print_line(false)?; - // Poll for stdin input + // Poll for + // stdin input let mut fds = [PollFd::new( unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) }, PollFlags::POLLIN, @@ -435,6 +442,10 @@ fn handle_readline_event(readline: &mut ShedVi, event: ShResult) }) { // CleanExit signals an intentional shell exit; any other error is printed. match e.kind() { + ShErrKind::Interrupt => { + // We got Ctrl+C during command execution + // Just fall through here + } ShErrKind::CleanExit(code) => { QUIT_CODE.store(*code, Ordering::SeqCst); return Ok(true); diff --git a/src/parse/execute.rs b/src/parse/execute.rs index d006248..de4a1a3 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -32,19 +32,13 @@ use crate::{ test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, - }, - expand::{expand_aliases, expand_case_pattern, glob_to_regex}, - jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, - libsh::{ + }, expand::{expand_aliases, expand_case_pattern, glob_to_regex}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, libsh::{ error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, guards::{scope_guard, var_ctx_guard}, utils::RedirVecUtils, - }, - prelude::*, - procio::{IoMode, IoStack, PipeGenerator}, - state::{ + }, prelude::*, procio::{IoMode, IoStack, PipeGenerator}, signal::{check_signals, signals_pending}, state::{ self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars, - }, + } }; use super::{ @@ -273,6 +267,13 @@ impl Dispatcher { Ok(()) } pub fn dispatch_node(&mut self, node: Node) -> ShResult<()> { + while signals_pending() { + // If we have received SIGINT, + // this will stop the execution here + // and propagate back to the functions in main.rs + check_signals()?; + } + match node.class { NdRule::Conjunction { .. } => self.exec_conjunction(node)?, NdRule::Pipeline { .. } => self.exec_pipeline(node)?, diff --git a/src/signal.rs b/src/signal.rs index 398ec21..524d9d9 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -3,7 +3,7 @@ use std::{ sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering}, }; -use nix::sys::signal::{SaFlags, SigAction, sigaction}; +use nix::{sys::signal::{SaFlags, SigAction, sigaction}, unistd::getpid}; use crate::{ builtin::trap::TrapTarget, @@ -21,17 +21,22 @@ static SIGNALS: AtomicU64 = AtomicU64::new(0); pub static REAPING_ENABLED: AtomicBool = AtomicBool::new(true); pub static SHOULD_QUIT: AtomicBool = AtomicBool::new(false); -pub static GOT_SIGWINCH: AtomicBool = AtomicBool::new(false); pub static JOB_DONE: AtomicBool = AtomicBool::new(false); pub static QUIT_CODE: AtomicI32 = AtomicI32::new(0); -const MISC_SIGNALS: [Signal; 22] = [ +/// Window size change signal +pub static GOT_SIGWINCH: AtomicBool = AtomicBool::new(false); + +/// SIGUSR1 tells the prompt that it needs to fully refresh. +/// Useful for dynamic prompt content and asynchronous refreshing +pub static GOT_SIGUSR1: AtomicBool = AtomicBool::new(false); + +const MISC_SIGNALS: [Signal; 21] = [ Signal::SIGILL, Signal::SIGTRAP, Signal::SIGABRT, Signal::SIGBUS, Signal::SIGFPE, - Signal::SIGUSR1, Signal::SIGSEGV, Signal::SIGUSR2, Signal::SIGPIPE, @@ -71,7 +76,7 @@ pub fn check_signals() -> ShResult<()> { if got_signal(Signal::SIGINT) { interrupt()?; run_trap(Signal::SIGINT)?; - return Err(ShErr::simple(ShErrKind::ClearReadline, "")); + return Err(ShErr::simple(ShErrKind::Interrupt, "")); } if got_signal(Signal::SIGHUP) { run_trap(Signal::SIGHUP)?; @@ -93,6 +98,10 @@ pub fn check_signals() -> ShResult<()> { GOT_SIGWINCH.store(true, Ordering::SeqCst); run_trap(Signal::SIGWINCH)?; } + if got_signal(Signal::SIGUSR1) { + GOT_SIGUSR1.store(true, Ordering::SeqCst); + run_trap(Signal::SIGUSR1)?; + } for sig in MISC_SIGNALS { if got_signal(sig) { @@ -324,6 +333,14 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> { let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string(); let statuses = job.get_stats(); + for status in &statuses { + if let WtStat::Signaled(_, sig, _) = status + && *sig == Signal::SIGINT { + // Necessary to interrupt stuff like shell loops + kill(getpid(), Signal::SIGINT).ok(); + } + } + if let Some(pipe_status) = Job::pipe_status(&statuses) { let pipe_status = pipe_status .into_iter()