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
|
# 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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}'"))
|
||||||
|
|||||||
@@ -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))?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
17
src/main.rs
17
src/main.rs
@@ -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);
|
||||||
|
|||||||
@@ -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)?,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user