diff --git a/doc/autocmd.txt b/doc/autocmd.txt index fdb8fb4..a0a7acc 100644 --- a/doc/autocmd.txt +++ b/doc/autocmd.txt @@ -145,7 +145,20 @@ behavior without modifying the shell itself. Special variables: `$_COMP_CANDIDATE` the selected completion candidate - 2.8 Exit Event *autocmd-exit-event* + 2.8 Screensaver Events *autocmd-screensaver-events* + + `on-screensaver-exec` *autocmd-on-screensaver-exec* + + Fires when the screensaver activates after the idle timeout. + The screensaver command (see `shopt prompt.screensaver_cmd`) is + available for pattern matching. + + `on-screensaver-return` *autocmd-on-screensaver-return* + + Fires when the shell returns from the screensaver command. + The screensaver command is available for pattern matching. + + 2.9 Exit Event *autocmd-exit-event* `on-exit` *autocmd-on-exit* diff --git a/doc/help.txt b/doc/help.txt new file mode 100644 index 0000000..4bf19a9 --- /dev/null +++ b/doc/help.txt @@ -0,0 +1,44 @@ +*help* + +#SHED HELP# + +Shed is an experimental UNIX command interpreter (shell) usable as both an interactive login shell and as a shell script command preprocessor. Shed combines the functionality of `bash` with the UX and customizability of `vim`. Shed seeks to improve the interactive shell experience by providing: +* Programmable, dynamic prompts +* A hackable line editor similar to `zsh`'s `zle` +* Custom tab completion +* An event hook system +* Fuzzy history search/tab completion +* And many more features + +============================================================================== +1. Available Topics *help-topics* + + `help arith` Arithmetic expansion: `$(( ))` syntax |arith| + `help autocmd` Autocmd hooks for shell events |autocmd| + `help ex` Ex mode (colon) commands |ex| + `help glob` Pathname expansion and wildcards |glob| + `help keybinds` Vi mode keys, motions, text objects |keybinds| + `help param` Parameter expansion: `${}` operators |param| + `help prompt` Prompt configuration, PS1, PSR, echo -p |prompt| + `help redirect` Redirection, pipes, heredocs |redirect| + +============================================================================== +2. Navigating Help *help-nav* + + `help {topic}` Open a help page by name. Prefix matches work, + so `help key` opens |keybinds|. + + `help {tag}` Jump directly to a tagged section. Tags are the + words marked with `*asterisks*` in the help files. + + Examples: + `help text-objects` jumps to |text-objects| + `help prompt-escapes` jumps to |prompt-escapes| + `help heredoc` jumps to |heredoc| + + Cross-references like |prompt| are clickable tags. Search for them + with `help` followed by the reference name. + + Inside the pager, use `/` to search and `q` to quit. + +============================================================================== diff --git a/doc/keybinds.txt b/doc/keybinds.txt index e919e2b..67fa9f5 100644 --- a/doc/keybinds.txt +++ b/doc/keybinds.txt @@ -388,7 +388,7 @@ mode on startup is Insert. When the buffer is taller than the terminal, the editor displays a scrolling viewport. The viewport follows the cursor and respects the - `scrolloff` option (minimum lines visible above/below the cursor). + `line.scroll_offset` shopt (minimum lines visible above/below the cursor). `Ctrl+D` scroll down half a screen `Ctrl+U` scroll up half a screen diff --git a/nix/render_rc.nix b/nix/render_rc.nix index 21cfaeb..e7c283b 100644 --- a/nix/render_rc.nix +++ b/nix/render_rc.nix @@ -53,6 +53,9 @@ lib.concatLines [ (lib.concatLines (lib.mapAttrsToList (name: value: "export ${name}=\"${value}\"") cfg.environmentVars)) (lib.concatLines (lib.mapAttrsToList (name: value: "alias ${name}=\"${value}\"") cfg.aliases)) (lib.concatLines [ + "shopt line.viewport_height=${toString cfg.settings.viewportHeight}" + "shopt line.scroll_offset=${toString cfg.settings.scrollOffset}" + "shopt core.dotglob=${boolToString cfg.settings.dotGlob}" "shopt core.autocd=${boolToString cfg.settings.autocd}" "shopt core.hist_ignore_dupes=${boolToString cfg.settings.historyIgnoresDupes}" diff --git a/nix/shed_opts.nix b/nix/shed_opts.nix index 61f6ae4..e96326d 100644 --- a/nix/shed_opts.nix +++ b/nix/shed_opts.nix @@ -155,6 +155,18 @@ description = "Additional completion scripts to source when shed starts (e.g. for custom tools or functions)"; }; + viewportHeight = { + type = lib.types.either lib.types.int lib.types.str; + default = "50%"; + description = "Maximum viewport height for the line editor buffer"; + }; + + scrollOffset = { + type = lib.types.int; + default = "1"; + description = "The minimum number of lines to keep visible above and below the cursor when scrolling (i.e. the 'scrolloff' option in vim)"; + }; + environmentVars = lib.mkOption { type = lib.types.attrsOf lib.types.str; default = {}; diff --git a/src/libsh/utils.rs b/src/libsh/utils.rs index 345cceb..60a451a 100644 --- a/src/libsh/utils.rs +++ b/src/libsh/utils.rs @@ -8,6 +8,33 @@ use crate::parse::{Node, Redir, RedirType}; use crate::prelude::*; use crate::state::AutoCmd; +#[macro_export] +/// Defines a two-way mapping between an enum and its string representation, implementing both Display and FromStr. +macro_rules! two_way_display { + ($name:ident, $($member:ident <=> $val:expr;)*) => { + impl Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $(Self::$member => write!(f, $val),)* + } + } + } + + impl FromStr for $name { + type Err = ShErr; + fn from_str(s: &str) -> Result { + match s { + $($val => Ok(Self::$member),)* + _ => Err(ShErr::simple( + ShErrKind::ParseErr, + format!("Invalid {} kind: {}",stringify!($name),s), + )), + } + } + } + }; +} + pub trait VecDequeExt { fn to_vec(self) -> Vec; } diff --git a/src/main.rs b/src/main.rs index 8e81199..8a9eae4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -329,14 +329,21 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { Ok(0) => { // We timed out. if let Some(cmd) = exec_if_timeout { - let prepared = ReadlineEvent::Line(cmd); + let prepared = ReadlineEvent::Line(cmd.clone()); let saved_hist_opt = read_shopts(|o| o.core.auto_hist); let _guard = scopeguard::guard(saved_hist_opt, |opt| { write_shopts(|o| o.core.auto_hist = opt); }); write_shopts(|o| o.core.auto_hist = false); // don't save screensaver command to history + let pre_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnScreensaverExec)); + pre_cmds.exec_with(&cmd); - match handle_readline_event(&mut readline, Ok(prepared))? { + let res = handle_readline_event(&mut readline, Ok(prepared))?; + + let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnScreensaverReturn)); + post_cmds.exec_with(&cmd); + + match res { true => return Ok(()), false => continue, } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index f26575c..954e4f3 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -509,6 +509,12 @@ pub enum LoopKind { Until, } +crate::two_way_display!(LoopKind, + While <=> "while"; + Until <=> "until"; +); + + #[derive(Clone, Debug)] pub enum TestCase { Unary { @@ -638,29 +644,6 @@ impl TestCaseBuilder { } } -impl FromStr for LoopKind { - type Err = ShErr; - fn from_str(s: &str) -> Result { - match s { - "while" => Ok(Self::While), - "until" => Ok(Self::Until), - _ => Err(ShErr::simple( - ShErrKind::ParseErr, - format!("Invalid loop kind: {s}"), - )), - } - } -} - -impl Display for LoopKind { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - LoopKind::While => write!(f, "while"), - LoopKind::Until => write!(f, "until"), - } - } -} - #[derive(Clone, Debug)] pub enum AssignKind { Eq, diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index 2080bd1..a627648 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -925,7 +925,7 @@ impl LineBuf { } Direction::Backward => { let slice = self.line_to_cursor(); - for (i, gr) in slice.iter().rev().enumerate().skip(1) { + for (i, gr) in slice.iter().rev().enumerate() { if gr == char { match dest { Dest::On => return -(i as isize) - 1, diff --git a/src/state.rs b/src/state.rs index 9a84df8..0b17429 100644 --- a/src/state.rs +++ b/src/state.rs @@ -631,59 +631,31 @@ pub enum AutoCmdKind { OnCompletionStart, OnCompletionCancel, OnCompletionSelect, + OnScreensaverExec, + OnScreensaverReturn, OnExit, } -impl Display for AutoCmdKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::PreCmd => write!(f, "pre-cmd"), - Self::PostCmd => write!(f, "post-cmd"), - Self::PreChangeDir => write!(f, "pre-change-dir"), - Self::PostChangeDir => write!(f, "post-change-dir"), - Self::OnJobFinish => write!(f, "on-job-finish"), - Self::PrePrompt => write!(f, "pre-prompt"), - Self::PostPrompt => write!(f, "post-prompt"), - Self::PreModeChange => write!(f, "pre-mode-change"), - Self::PostModeChange => write!(f, "post-mode-change"), - Self::OnHistoryOpen => write!(f, "on-history-open"), - Self::OnHistoryClose => write!(f, "on-history-close"), - Self::OnHistorySelect => write!(f, "on-history-select"), - Self::OnCompletionStart => write!(f, "on-completion-start"), - Self::OnCompletionCancel => write!(f, "on-completion-cancel"), - Self::OnCompletionSelect => write!(f, "on-completion-select"), - Self::OnExit => write!(f, "on-exit"), - } - } -} - -impl FromStr for AutoCmdKind { - type Err = ShErr; - fn from_str(s: &str) -> Result { - match s { - "pre-cmd" => Ok(Self::PreCmd), - "post-cmd" => Ok(Self::PostCmd), - "pre-change-dir" => Ok(Self::PreChangeDir), - "post-change-dir" => Ok(Self::PostChangeDir), - "on-job-finish" => Ok(Self::OnJobFinish), - "pre-prompt" => Ok(Self::PrePrompt), - "post-prompt" => Ok(Self::PostPrompt), - "pre-mode-change" => Ok(Self::PreModeChange), - "post-mode-change" => Ok(Self::PostModeChange), - "on-history-open" => Ok(Self::OnHistoryOpen), - "on-history-close" => Ok(Self::OnHistoryClose), - "on-history-select" => Ok(Self::OnHistorySelect), - "on-completion-start" => Ok(Self::OnCompletionStart), - "on-completion-cancel" => Ok(Self::OnCompletionCancel), - "on-completion-select" => Ok(Self::OnCompletionSelect), - "on-exit" => Ok(Self::OnExit), - _ => Err(ShErr::simple( - ShErrKind::ParseErr, - format!("Invalid autocmd kind: {}", s), - )), - } - } -} +crate::two_way_display!(AutoCmdKind, + PreCmd <=> "pre-cmd"; + PostCmd <=> "post-cmd"; + PreChangeDir <=> "pre-change-dir"; + PostChangeDir <=> "post-change-dir"; + OnJobFinish <=> "on-job-finish"; + PrePrompt <=> "pre-prompt"; + PostPrompt <=> "post-prompt"; + PreModeChange <=> "pre-mode-change"; + PostModeChange <=> "post-mode-change"; + OnHistoryOpen <=> "on-history-open"; + OnHistoryClose <=> "on-history-close"; + OnHistorySelect <=> "on-history-select"; + OnCompletionStart <=> "on-completion-start"; + OnCompletionCancel <=> "on-completion-cancel"; + OnCompletionSelect <=> "on-completion-select"; + OnScreensaverExec <=> "on-screensaver-exec"; + OnScreensaverReturn <=> "on-screensaver-return"; + OnExit <=> "on-exit"; +); #[derive(Clone, Debug)] pub struct AutoCmd {