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.
@@ -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()