Propagate SIGINT from foreground jobs to interrupt shell loops, add SIGUSR1 for async prompt refresh, and support SHED_HPAGER override

This commit is contained in:
2026-03-16 19:00:33 -04:00
parent 958dad9942
commit db3f1b5108
7 changed files with 64 additions and 23 deletions

View File

@@ -1,6 +1,6 @@
# shed # 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. <small>Btw if you don't use `vim` this probably isn't your shell.</small>
<img width="506" height="407" alt="shed" src="https://github.com/user-attachments/assets/3945f663-a361-4418-bf20-0c4eaa2a36d2" /> <img width="506" height="407" alt="shed" src="https://github.com/user-attachments/assets/3945f663-a361-4418-bf20-0c4eaa2a36d2" />
@@ -40,6 +40,8 @@ gitbranch() { git branch --show-current 2>/dev/null; }
export PS1='\u@\h \W \@gitbranch \$ ' 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. Additionally, `echo` now has a `-p` flag that expands prompt escape sequences, similar to how the `-e` flag expands conventional escape sequences.
--- ---

View File

@@ -121,7 +121,11 @@ pub fn help(node: Node) -> ShResult<()> {
} }
pub fn open_help(content: &str, line: Option<usize>, file_name: Option<String>) -> ShResult<()> { pub fn open_help(content: &str, line: Option<usize>, file_name: Option<String>) -> 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 line_arg = line.map(|ln| format!("+{ln}")).unwrap_or_default();
let prompt_arg = file_name let prompt_arg = file_name
.map(|name| format!("-Ps'{name}'")) .map(|name| format!("-Ps'{name}'"))

View File

@@ -1,6 +1,7 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use ariadne::Fmt; use ariadne::Fmt;
use nix::unistd::getpid;
use scopeguard::defer; use scopeguard::defer;
use yansi::Color; use yansi::Color;
@@ -872,7 +873,12 @@ pub fn wait_fg(job: Job, interactive: bool) -> ShResult<()> {
write_jobs(|j| j.fg_to_bg(*status))?; write_jobs(|j| j.fg_to_bg(*status))?;
} }
WtStat::Signaled(_, sig, _) => { 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; was_stopped = true;
write_jobs(|j| j.fg_to_bg(*status))?; write_jobs(|j| j.fg_to_bg(*status))?;
} }

View File

@@ -459,7 +459,7 @@ pub enum ShErrKind {
FuncReturn(i32), FuncReturn(i32),
LoopContinue(i32), LoopContinue(i32),
LoopBreak(i32), LoopBreak(i32),
ClearReadline, Interrupt,
Null, Null,
} }
@@ -471,7 +471,7 @@ impl ShErrKind {
| Self::FuncReturn(_) | Self::FuncReturn(_)
| Self::LoopContinue(_) | Self::LoopContinue(_)
| Self::LoopBreak(_) | Self::LoopBreak(_)
| Self::ClearReadline | Self::Interrupt
) )
} }
} }
@@ -496,7 +496,7 @@ impl Display for ShErrKind {
Self::LoopBreak(_) => "Syntax Error", Self::LoopBreak(_) => "Syntax Error",
Self::ReadlineErr => "Readline Error", Self::ReadlineErr => "Readline Error",
Self::ExCommand => "Ex Command Error", Self::ExCommand => "Ex Command Error",
Self::ClearReadline => "", Self::Interrupt => "",
Self::Null => "", Self::Null => "",
}; };
write!(f, "{output}") write!(f, "{output}")

View File

@@ -38,7 +38,7 @@ use crate::prelude::*;
use crate::procio::borrow_fd; use crate::procio::borrow_fd;
use crate::readline::term::{LineWriter, RawModeGuard, raw_mode}; use crate::readline::term::{LineWriter, RawModeGuard, raw_mode};
use crate::readline::{Prompt, ReadlineEvent, ShedVi}; use crate::readline::{Prompt, ReadlineEvent, ShedVi};
use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending}; use crate::signal::{GOT_SIGUSR1, GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending};
use crate::state::{ use crate::state::{
AutoCmdKind, read_logic, read_shopts, source_env, source_login, source_rc, write_jobs, AutoCmdKind, read_logic, read_shopts, source_env, source_login, source_rc, write_jobs,
write_meta, write_shopts, write_meta, write_shopts,
@@ -259,7 +259,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
while signals_pending() { while signals_pending() {
if let Err(e) = check_signals() { if let Err(e) = check_signals() {
match e.kind() { match e.kind() {
ShErrKind::ClearReadline => { ShErrKind::Interrupt => {
// We got Ctrl+C - clear current input and redraw // We got Ctrl+C - clear current input and redraw
readline.reset_active_widget(false)?; readline.reset_active_widget(false)?;
} }
@@ -285,9 +285,16 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
readline.prompt_mut().refresh(); 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)?; readline.print_line(false)?;
// Poll for stdin input // Poll for
// stdin input
let mut fds = [PollFd::new( let mut fds = [PollFd::new(
unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) }, unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) },
PollFlags::POLLIN, PollFlags::POLLIN,
@@ -435,6 +442,10 @@ fn handle_readline_event(readline: &mut ShedVi, event: ShResult<ReadlineEvent>)
}) { }) {
// CleanExit signals an intentional shell exit; any other error is printed. // CleanExit signals an intentional shell exit; any other error is printed.
match e.kind() { match e.kind() {
ShErrKind::Interrupt => {
// We got Ctrl+C during command execution
// Just fall through here
}
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(true); return Ok(true);

View File

@@ -32,19 +32,13 @@ use crate::{
test::double_bracket_test, test::double_bracket_test,
trap::{TrapTarget, trap}, trap::{TrapTarget, trap},
varcmds::{export, local, readonly, unset}, 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}, error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color},
guards::{scope_guard, var_ctx_guard}, guards::{scope_guard, var_ctx_guard},
utils::RedirVecUtils, utils::RedirVecUtils,
}, }, prelude::*, procio::{IoMode, IoStack, PipeGenerator}, signal::{check_signals, signals_pending}, state::{
prelude::*,
procio::{IoMode, IoStack, PipeGenerator},
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,
}, }
}; };
use super::{ use super::{
@@ -273,6 +267,13 @@ impl Dispatcher {
Ok(()) Ok(())
} }
pub fn dispatch_node(&mut self, node: Node) -> ShResult<()> { 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 { match node.class {
NdRule::Conjunction { .. } => self.exec_conjunction(node)?, NdRule::Conjunction { .. } => self.exec_conjunction(node)?,
NdRule::Pipeline { .. } => self.exec_pipeline(node)?, NdRule::Pipeline { .. } => self.exec_pipeline(node)?,

View File

@@ -3,7 +3,7 @@ use std::{
sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering}, sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering},
}; };
use nix::sys::signal::{SaFlags, SigAction, sigaction}; use nix::{sys::signal::{SaFlags, SigAction, sigaction}, unistd::getpid};
use crate::{ use crate::{
builtin::trap::TrapTarget, builtin::trap::TrapTarget,
@@ -21,17 +21,22 @@ static SIGNALS: AtomicU64 = AtomicU64::new(0);
pub static REAPING_ENABLED: AtomicBool = AtomicBool::new(true); pub static REAPING_ENABLED: AtomicBool = AtomicBool::new(true);
pub static SHOULD_QUIT: AtomicBool = AtomicBool::new(false); 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 JOB_DONE: AtomicBool = AtomicBool::new(false);
pub static QUIT_CODE: AtomicI32 = AtomicI32::new(0); 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::SIGILL,
Signal::SIGTRAP, Signal::SIGTRAP,
Signal::SIGABRT, Signal::SIGABRT,
Signal::SIGBUS, Signal::SIGBUS,
Signal::SIGFPE, Signal::SIGFPE,
Signal::SIGUSR1,
Signal::SIGSEGV, Signal::SIGSEGV,
Signal::SIGUSR2, Signal::SIGUSR2,
Signal::SIGPIPE, Signal::SIGPIPE,
@@ -71,7 +76,7 @@ pub fn check_signals() -> ShResult<()> {
if got_signal(Signal::SIGINT) { if got_signal(Signal::SIGINT) {
interrupt()?; interrupt()?;
run_trap(Signal::SIGINT)?; run_trap(Signal::SIGINT)?;
return Err(ShErr::simple(ShErrKind::ClearReadline, "")); return Err(ShErr::simple(ShErrKind::Interrupt, ""));
} }
if got_signal(Signal::SIGHUP) { if got_signal(Signal::SIGHUP) {
run_trap(Signal::SIGHUP)?; run_trap(Signal::SIGHUP)?;
@@ -93,6 +98,10 @@ pub fn check_signals() -> ShResult<()> {
GOT_SIGWINCH.store(true, Ordering::SeqCst); GOT_SIGWINCH.store(true, Ordering::SeqCst);
run_trap(Signal::SIGWINCH)?; run_trap(Signal::SIGWINCH)?;
} }
if got_signal(Signal::SIGUSR1) {
GOT_SIGUSR1.store(true, Ordering::SeqCst);
run_trap(Signal::SIGUSR1)?;
}
for sig in MISC_SIGNALS { for sig in MISC_SIGNALS {
if got_signal(sig) { 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 job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string();
let statuses = job.get_stats(); 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) { if let Some(pipe_status) = Job::pipe_status(&statuses) {
let pipe_status = pipe_status let pipe_status = pipe_status
.into_iter() .into_iter()