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:
@@ -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. <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" />
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -121,7 +121,11 @@ pub fn help(node: Node) -> 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 prompt_arg = file_name
|
||||
.map(|name| format!("-Ps'{name}'"))
|
||||
|
||||
@@ -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))?;
|
||||
}
|
||||
|
||||
@@ -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}")
|
||||
|
||||
17
src/main.rs
17
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<ReadlineEvent>)
|
||||
}) {
|
||||
// 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);
|
||||
|
||||
@@ -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)?,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user