From 42647ffda89096fae027eb659c0e7da2b85b1a0c Mon Sep 17 00:00:00 2001 From: pagedmov Date: Sat, 21 Mar 2026 02:14:47 -0400 Subject: [PATCH] Refactor: extract history search, completion, and keymap handling into separate methods; support prefix matching for help topics --- doc/autocmd.txt | 197 +++++++++++++++++++ doc/ex.txt | 109 +++++++++++ doc/keybinds.txt | 461 ++++++++++++++++++++++++++++++++++++++++++++ doc/prompt.txt | 229 ++++++++++++++++++++++ src/builtin/help.rs | 31 +-- src/readline/mod.rs | 342 ++++++++++++++++---------------- 6 files changed, 1190 insertions(+), 179 deletions(-) create mode 100644 doc/autocmd.txt create mode 100644 doc/ex.txt create mode 100644 doc/keybinds.txt create mode 100644 doc/prompt.txt diff --git a/doc/autocmd.txt b/doc/autocmd.txt new file mode 100644 index 0000000..fdb8fb4 --- /dev/null +++ b/doc/autocmd.txt @@ -0,0 +1,197 @@ +*autocmd* *autocmds* *hooks* + +#AUTOCMDS# + +Autocmds (automatic commands) execute shell commands in response to +specific events. They provide a hook system for customizing shell +behavior without modifying the shell itself. + +============================================================================== +1. Registration *autocmd-register* + + `autocmd [OPTIONS] {kind} {command}` + + Register {command} to run whenever the event {kind} fires. + + Options: + + `-p {pattern}` *autocmd-pattern* + + Only run this autocmd when {pattern} (a regex) matches the + event's context string. The context varies by event type: + for |autocmd-pre-cmd| it is the command being executed, for + |autocmd-post-change-dir| it is the target directory, etc. + + `-c` *autocmd-clear* + + Clear all autocmds of the specified {kind}. No command is + needed. + + Examples: + + `autocmd post-cmd 'echo "exit: $?"'` + `autocmd -p '^git' pre-cmd 'echo running git...'` + `autocmd -c pre-cmd` + +============================================================================== +2. Event Kinds *autocmd-kinds* + + 2.1 Command Execution *autocmd-cmd-events* + + `pre-cmd` *autocmd-pre-cmd* + + Fires before a command is executed. The command string is + available for pattern matching. + + `post-cmd` *autocmd-post-cmd* + + Fires after a command finishes. The command string is available + for pattern matching. + + 2.2 Directory Changes *autocmd-dir-events* + + `pre-change-dir` *autocmd-pre-change-dir* + + Fires before `cd` changes the working directory. The target + directory is available for pattern matching. + + Special variables: + `$_NEW_DIR` the directory being changed to + `$_OLD_DIR` the current directory (before the change) + + `post-change-dir` *autocmd-post-change-dir* + + Fires after a successful directory change. + + Special variables: + `$_NEW_DIR` the new working directory + `$_OLD_DIR` the previous directory + + 2.3 Job Events *autocmd-job-events* + + `on-job-finish` *autocmd-on-job-finish* + + Fires when a background job completes. The job's command string + is available for pattern matching. + + 2.4 Prompt Events *autocmd-prompt-events* + + `pre-prompt` *autocmd-pre-prompt* + + Fires before the prompt is rendered. Useful for updating prompt + state. + + `post-prompt` *autocmd-post-prompt* + + Fires after the prompt is rendered. + + 2.5 Mode Change Events *autocmd-mode-events* + + `pre-mode-change` *autocmd-pre-mode-change* + + Fires before the vi editing mode changes. The `$SHED_VI_MODE` + variable still holds the old mode. + + `post-mode-change` *autocmd-post-mode-change* + + Fires after the vi editing mode changes. `$SHED_VI_MODE` reflects + the new mode. + + 2.6 History Events *autocmd-hist-events* + + `on-history-open` *autocmd-on-history-open* + + Fires when the fuzzy history search window opens. + + Special variables: + `$_ENTRIES` array of all history entries + `$_NUM_ENTRIES` count of all entries + `$_MATCHES` array of currently matching entries + `$_NUM_MATCHES` count of matching entries + `$_SEARCH_STR` the current search string + + `on-history-close` *autocmd-on-history-close* + + Fires when the history search is dismissed without selecting. + + `on-history-select` *autocmd-on-history-select* + + Fires when a history entry is selected. The entry text is + available for pattern matching. + + Special variables: + `$_HIST_ENTRY` the selected history entry + + 2.7 Completion Events *autocmd-comp-events* + + `on-completion-start` *autocmd-on-completion-start* + + Fires when the completion menu becomes visible. + + Special variables: + `$_MATCHES` array of completion candidates + `$_NUM_MATCHES` count of candidates + `$_SEARCH_STR` the token being completed + + `on-completion-cancel` *autocmd-on-completion-cancel* + + Fires when the completion menu is dismissed without selecting. + + `on-completion-select` *autocmd-on-completion-select* + + Fires when a completion candidate is accepted. The candidate + is available for pattern matching. + + Special variables: + `$_COMP_CANDIDATE` the selected completion candidate + + 2.8 Exit Event *autocmd-exit-event* + + `on-exit` *autocmd-on-exit* + + Fires when the shell is about to exit. + +============================================================================== +3. Behavior *autocmd-behavior* + + - Multiple autocmds can be registered for the same event kind. They + execute in registration order. + + - If an autocmd command fails, the error is printed but subsequent + autocmds for the same event still run. + + - Autocmds do not affect the shell's exit status (`$?`). The exit + status is saved before autocmd execution and restored afterward. + + - Pattern matching uses Rust regex syntax. If an autocmd has no + pattern, it always fires for its event kind. + + - Special variables (e.g. `$_NEW_DIR`) are only available within the + scope of the autocmd execution. They are not set globally. + +============================================================================== +4. Examples *autocmd-examples* + + Notify on directory change: + + `autocmd post-change-dir 'echo "moved to $_NEW_DIR"'` + + Run a linter only on git commands: + + `autocmd -p '^git commit' post-cmd 'lint-check'` + + Refresh prompt on mode change (for mode indicator): + + `autocmd post-mode-change 'kill -USR1 $$'` + (SIGUSR1 can be used to remotely refresh the prompt. See |prompt|) + + Log completed jobs: + + `autocmd on-job-finish 'echo "job done" >> /tmp/jobs.log'` + + Clean up on exit: + + `autocmd on-exit 'rm -f /tmp/my-shell-*.tmp'` + +============================================================================== +See also: |keybinds| |prompt| |ex| diff --git a/doc/ex.txt b/doc/ex.txt new file mode 100644 index 0000000..4b4b9b6 --- /dev/null +++ b/doc/ex.txt @@ -0,0 +1,109 @@ +*ex* *ex-mode* *ex-commands* *colon-commands* + +#EX MODE# + +Ex mode provides colon commands for operations that go beyond single-key +normal mode actions. Enter ex mode by pressing `:` in normal mode. + +The command line supports full editing via insert mode, and has its own +command history navigable with `Up` and `Down`. + +============================================================================== +1. Shell Commands *ex-shell* + + `:!{cmd}` *ex-bang* + + Execute {cmd} in the shell. The following special variables are + set during execution and can be read or modified: + + `$_BUFFER` the current editor buffer contents + `$_CURSOR` the cursor position (flat byte index) + `$_ANCHOR` the visual selection anchor position + + If the command modifies these variables, the editor state is + updated accordingly. This allows ex commands to programmatically + edit the buffer. + + If the command sets `$_KEYS`, the value is fed back into the + editor as a key sequence. + + Example: + `:!echo "$_BUFFER" | tr a-z A-Z > /tmp/out` + `:!_BUFFER=$(echo "$_BUFFER" | sort)` + +============================================================================== +2. File Operations *ex-file* + + `:r {file}` *ex-read* + + Read the contents of {file} and insert them into the buffer at + the cursor position. + + `:r !{cmd}` *ex-read-cmd* + + Execute {cmd} and insert its output into the buffer. + + `:w {file}` *ex-write* + + Write the buffer contents to {file}. Creates the file if it does + not exist, or truncates it if it does. + + `:w >> {file}` *ex-write-append* + + Append the buffer contents to {file}. + + `:w !{cmd}` *ex-write-cmd* + + Pipe the buffer contents to {cmd} as stdin. + + `:e {file}` *ex-edit* + + Open {file} in the editor defined by `$EDITOR`. Requires the + `EDITOR` environment variable to be set. + + Example: + `:e ~/.config/shed/shedrc` + +============================================================================== +3. Buffer Operations *ex-buffer* + + `:d` *ex-delete* + + Delete the entire buffer. + + `:y` *ex-yank* + + Yank the entire buffer into the default register. + + `:pu` *ex-put* + + Put (paste) from the default register after the cursor. + +============================================================================== +4. Other Commands *ex-other* + + `:q` *ex-quit* + + Quit the editor / exit the shell. + + `:help {topic}` *ex-help* + + Display help for {topic}. Runs the `help` builtin. + +============================================================================== +5. Path Expansion *ex-paths* + + File paths in ex commands are subject to variable expansion. You can + use environment variables in paths: + + `:e $HOME/.config/shed/shedrc` + `:w ${TMPDIR}/output.txt` + +============================================================================== +6. Ex Command History *ex-history* + + Ex mode maintains its own command history, separate from the main + shell history. Navigate with `Up` and `Down` while in ex mode. + +============================================================================== +See also: |keybinds| |autocmd| |prompt| diff --git a/doc/keybinds.txt b/doc/keybinds.txt new file mode 100644 index 0000000..e919e2b --- /dev/null +++ b/doc/keybinds.txt @@ -0,0 +1,461 @@ +*keybinds* *keys* *vi-mode* *keybindings* + +#VI MODE KEYBINDINGS# + +The line editor uses a vi-style modal editing system with six modes: +Normal, Insert, Visual, Replace, Ex (command), and Verbatim. The default +mode on startup is Insert. + +============================================================================== +1. Normal Mode *normal-mode* + + Normal mode is for navigating and manipulating text. Press `Esc` from + any other mode to return here. + + 1.1 Movement *normal-movement* + + `h` `l` *key-h* *key-l* + + Move left / right by one character. + + `j` `k` *key-j* *key-k* + + Move down / up by one line. + + `w` `W` *key-w* *key-W* + + Move to the start of the next word. `w` stops at punctuation + boundaries, `W` only stops at whitespace. + + `b` `B` *key-b* *key-B* + + Move to the start of the previous word. + + `e` `E` *key-e* *key-E* + + Move to the end of the next word. + + `ge` `gE` *key-ge* *key-gE* + + Move to the end of the previous word. + + `0` *key-0* + + Move to the start of the line. + + `^` *key-caret* + + Move to the first non-whitespace character on the line. + + `$` *key-dollar* + + Move to the end of the line. + + `g_` *key-g_* + + Move to the last non-whitespace character on the line. + + `gg` *key-gg* + + Move to the first line of the buffer. + + `G` *key-G* + + Move to the last line of the buffer. + + `|` *key-bar* + + Move to a specific column. `10|` moves to column 10. + + `%` *key-percent* + + Jump to the matching bracket: `()`, `[]`, `{}`. + + `](` `[(` *key-paren-nav* + + Jump to the next / previous unmatched parenthesis. + + `]}` `[{` *key-brace-nav* + + Jump to the next / previous unmatched brace. + + 1.2 Character Search *char-search* + + `f{char}` *key-f* + + Move forward to the next occurrence of {char} on the current line. + + `F{char}` *key-F* + + Move backward to the previous occurrence of {char}. + + `t{char}` *key-t* + + Move forward to just before {char}. + + `T{char}` *key-T* + + Move backward to just after {char}. + + `;` *key-semicolon* + + Repeat the last `f`, `F`, `t`, or `T` in the same direction. + + `,` *key-comma* + + Repeat the last `f`, `F`, `t`, or `T` in the reverse direction. + + 1.3 Scrolling *scrolling* + + `Ctrl+D` *key-ctrl-d* + + In normal mode, scroll down half a screen. See |viewport|. + In insert mode, `Ctrl+D` clears the buffer if there is any content, and exits the shell if there is not. + + `Ctrl+U` *key-ctrl-u* + + Scroll up half a screen. + + `Ctrl+G` *key-ctrl-g* + + Print current cursor position (line, column, total lines). + + 1.4 Operators *operators* + + Operators take a {motion} or |text-object| to define the range they + act on. Double an operator to act on the whole line (e.g. `dd`, `>>`, + `==`). + + `d{motion}` *key-d* + + Delete the text covered by {motion}. + + `dd` delete the whole line + `D` delete to end of line (same as `d$`) + `x` delete character under cursor (same as `dl`) + `X` delete character before cursor (same as `dh`) + + `c{motion}` *key-c* + + Delete the text covered by {motion} and enter insert mode. + + `cc` change the whole line + `C` change to end of line (same as `c$`) + `s` change the character under cursor (same as `cl`) + `S` change the whole line (same as `cc`) + + `y{motion}` *key-y* + + Yank (copy) the text covered by {motion} into a register. + + `yy` yank the whole line + `Y` yank the whole line + + `>{motion}` *key-indent* + + Indent the lines covered by {motion}. + + `>>` indent the current line + + `<{motion}` *key-dedent* + + Dedent the lines covered by {motion}. + + `<<` dedent the current line + + `={motion}` *key-equalize* + + Auto-indent the lines covered by {motion}. Uses the minimum + nesting depth of each line's start and end to determine the + correct indentation level. + + `==` equalize the current line + + `g~{motion}` *key-toggle-case* + + Toggle the case of the text covered by {motion}. + + `gu{motion}` *key-gu* + + Convert the text covered by {motion} to lowercase. + + `gU{motion}` *key-gU* + + Convert the text covered by {motion} to uppercase. + + 1.5 Single-Key Actions *normal-actions* + + `p` *key-p* + + Paste from the register after the cursor. + + `P` *key-P* + + Paste from the register before the cursor. + + `r{char}` *key-r* + + Replace the character under the cursor with {char}. With a count, + replaces that many characters. + + `~` *key-tilde* + + Toggle the case of the character under the cursor and advance. + Accepts a count. + + `J` *key-J* + + Join the current line with the next line. + + `u` *key-u* + + Undo the last change. + + `Ctrl+R` *key-ctrl-r* + + Redo the last undone change. + + `.` *key-dot* + + Repeat the last editing command. + + `Ctrl+A` *key-ctrl-a* + + Increment the number under the cursor. Recognizes decimal, + hexadecimal (`0x`), binary (`0b`), and octal (`0o`) formats. + Preserves leading zeros and prefix. + + `Ctrl+X` *key-ctrl-x* + + Decrement the number under the cursor. + + 1.6 Entering Other Modes *mode-entry* + + `i` insert before cursor *key-i* + `a` insert after cursor *key-a* + `I` insert at first non-whitespace *key-I* + `A` insert at end of line *key-A* + `o` open a new line below *key-o* + `O` open a new line above *key-O* + `R` enter replace mode *key-R* + `v` enter visual mode (character-wise) *key-v* + `V` enter visual mode (line-wise) *key-V-visual* + `gv` reselect last visual region *key-gv* + `:` enter ex mode *key-colon* + + 1.7 Registers *registers* + + `"{reg}` *key-register* + + Use register {reg} for the next delete, yank, or put. Registers + `a`-`z` store text; uppercase `A`-`Z` appends to the corresponding + lowercase register. + +============================================================================== +2. Insert Mode *insert-mode* + + Insert mode is for typing text. Characters are inserted at the cursor. + + `Esc` return to normal mode + `Backspace` delete character before cursor + `Ctrl+H` same as Backspace + `Ctrl+W` delete word before cursor + `Delete` delete character under cursor + `Tab` trigger completion (see |completion|) + `Shift+Tab` trigger completion (reverse direction) + `Ctrl+R` open history search (see |history-search|) + `Ctrl+V` enter verbatim mode (insert literal key sequence) + `Enter` submit line or insert newline if input is incomplete + + Arrow keys, Home, and End work as expected for navigation. + +============================================================================== +3. Visual Mode *visual-mode* + + Visual mode selects a region of text. Enter with `v` (character-wise) + or `V` (line-wise) from normal mode. + + All normal-mode motions work to extend the selection. Operators act + on the selected region without needing a motion: + + `d` `x` delete selection + `c` `s` `S` change selection (delete and enter insert mode) + `y` yank selection + `p` `P` paste, replacing selection + `>` `<` indent / dedent selection + `=` equalize selection + `~` toggle case of selection + `u` lowercase selection + `U` uppercase selection + `r{char}` replace every character in selection with {char} + `J` join selected lines + `o` `O` swap cursor and selection anchor + + Press `Esc` to return to normal mode without acting. + +============================================================================== +4. Replace Mode *replace-mode* + + Replace mode overwrites existing characters as you type. Enter with + `R` from normal mode. + + `Esc` return to normal mode + `Backspace` undo the last replacement + + All other keys replace the character under the cursor and advance. + +============================================================================== +5. Ex Mode *ex-mode-keys* + + Ex mode accepts colon commands. Enter with `:` from normal mode. + See |ex| for available commands. + + `Enter` execute the command + `Esc` cancel and return to normal mode + `Ctrl+C` clear the command line + `Up` `Down` navigate ex command history + +============================================================================== +6. Text Objects *text-objects* + + Text objects define a range of text based on structure. They are used + with operators: `d`, `c`, `y`, etc. Each has an "inner" (`i`) and + "around" (`a`) variant. + + `iw` `aw` *obj-word* + + Word (punctuation-delimited). `aw` includes trailing whitespace. + + `iW` `aW` *obj-WORD* + + WORD (whitespace-delimited). `aW` includes trailing whitespace. + + `i"` `a"` *obj-dquote* + + Double-quoted string. + + `i'` `a'` *obj-squote* + + Single-quoted string. + + `` i` `` `` a` `` *obj-backtick* + + Backtick-quoted string. + + `i)` `a)` `ib` `ab` *obj-paren* + + Parenthesized block. + + `i]` `a]` *obj-bracket* + + Square-bracketed block. + + `i}` `a}` `iB` `aB` *obj-brace* + + Brace-delimited block. + + `i<` `a<` *obj-angle* + + Angle-bracketed block. + + `it` `at` *obj-tag* + + XML/HTML tag block. + + `is` `as` *obj-sentence* + + Sentence. + + `ip` `ap` *obj-paragraph* + + Paragraph (separated by blank lines). + +============================================================================== +7. Counts *counts* + + Most motions, operators, and actions accept a numeric count prefix: + + `3j` move down 3 lines + `2dw` delete 2 words + `5>>` indent 5 lines + `10l` move 10 characters right + + When both the operator and the motion have counts, they are + multiplied: `2d3w` deletes 6 words. + +============================================================================== +8. Viewport *viewport* + + 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). + + `Ctrl+D` scroll down half a screen + `Ctrl+U` scroll up half a screen + `Ctrl+G` display current position in the buffer + + Line numbers in the left margin reflect actual buffer positions, not + viewport-relative indices. + +============================================================================== +9. User-Defined Keymaps *keymaps* *keymap* + + Custom key bindings are created with the `keymap` command: + + `keymap [flags] {keys} {action}` + + Flags select the mode(s) the binding applies to: + + `-n` normal mode + `-i` insert mode + `-v` visual mode + `-x` ex mode + `-o` operator-pending mode + `-r` replace mode + + Keys and actions use angle-bracket notation for special keys: + + `` Enter `` Escape + `` Tab `` Backspace + `` Delete `` Space + `` `` `` `` Arrow keys + `` `` Home / End + `` - `` Function keys + `` Enter ex mode + `` Leader key (set via `shopt prompt.leader`) + + Modifier prefixes: + + `C-` Control `A-` `M-` Alt / Meta + `S-` Shift + + Examples: + + `keymap -n w ':w'` Leader+w writes to file + `keymap -i jk ''` jk exits insert mode + `keymap -n ':!mycmd'` Ctrl+n runs a shell command + + To remove a binding: + + `keymap --remove {keys}` + +============================================================================== +10. Completion *completion* + + `Tab` start or cycle through completion candidates (forward) + `Shift+Tab` cycle backward + + When the completion menu is visible, `Tab` and arrow keys navigate + candidates. `Enter` accepts the selected candidate. `Esc` dismisses + the menu. + +============================================================================== +11. History Search *history-search* + + `Ctrl+R` open fuzzy history search (from insert or ex mode) + + Type to filter history entries. `Enter` accepts the selected entry. + `Esc` dismisses the search. + +============================================================================== +See also: |ex| |autocmd| |prompt| diff --git a/doc/prompt.txt b/doc/prompt.txt new file mode 100644 index 0000000..c8e8793 --- /dev/null +++ b/doc/prompt.txt @@ -0,0 +1,229 @@ +*prompt* *ps1* *psr* *prompt-expansion* + +#PROMPT# + +The shell displays a configurable prompt before each command. Two prompt +strings are available: PS1 (the main prompt) and PSR (an optional +right-aligned prompt). + +============================================================================== +1. Setting the Prompt *prompt-set* + + The prompt is controlled by the `PS1` and `PSR` environment variables. + Set them in your shell configuration: + + `PS1='\u@\h:\W\$ '` + `PSR='$SHED_VI_MODE'` + + Prompts are re-expanded before each display, so command substitutions + and functions are evaluated every time. + +============================================================================== +2. Escape Sequences *prompt-escapes* + + The following backslash escapes are recognized in PS1 and PSR: + + `\u` *prompt-user* + + The current username (from $USER). + + `\h` *prompt-host* + + The hostname (from $HOST). + + `\w` *prompt-pwd* + + The current working directory, with $HOME replaced by `~`. + + Example: + `/home/user/projects` -> `~/projects` + + `\W` *prompt-pwd-short* + + Truncated working directory. Shows only the last N path segments, + where N is controlled by `shopt prompt.trunc_prompt_path` + (default: 4). + + Example (with trunc_prompt_path=2): + `/home/user/projects/myapp` -> `projects/myapp` + + `\s` *prompt-shell* + + The shell name: `shed`. + + `\$` *prompt-symbol* + + `#` if the effective UID is 0 (root), `$` otherwise. + + `\j` *prompt-jobs* + + The number of background jobs currently managed by the shell. + + `\t` *prompt-runtime-ms* + + The runtime of the last command in milliseconds. + + `\T` *prompt-runtime-fmt* + + The runtime of the last command in human-readable format + (e.g. `1m 23s 456ms`). + + `\n` *prompt-newline* + + A literal newline. Use this to create multi-line prompts. + + `\r` *prompt-return* + + A literal carriage return. + + `\\` *prompt-backslash* + + A literal backslash. + + `\e[...` *prompt-ansi* + + An ANSI escape sequence. The sequence starts with `\e[` and ends + at the first letter character. Used for colors and formatting. + + Common codes: + `\e[0m` reset all attributes + `\e[1m` bold + `\e[3m` italic + `\e[4m` underline + `\e[31m` red foreground + `\e[32m` green foreground + `\e[33m` yellow foreground + `\e[34m` blue foreground + `\e[35m` magenta foreground + `\e[36m` cyan foreground + `\e[1;32m` bold green + + Example: + `PS1='\e[1;32m\u\e[0m@\e[34m\h\e[0m:\w\$ '` + +============================================================================== +3. Functions in Prompts *prompt-functions* + + `\@{funcname}` *prompt-func* + `\@funcname` + + Call a shell function and insert its output. The function must + be defined before the prompt is expanded. If the function does + not exist, the literal sequence is displayed. + + Example: + `git_branch() { git branch --show-current 2>/dev/null; }` + `PS1='\u@\h (\@git_branch)\$ '` + + This allows dynamic prompt content that updates on every command. + +============================================================================== +4. Right Prompt (PSR) *prompt-right* *psr* + + The PSR variable defines an optional right-aligned prompt displayed + on the last line of PS1. It supports the same escape sequences as PS1. + + PSR is only displayed when it fits: if the combined width of the + input line and PSR exceeds the terminal width, PSR is hidden. + + PSR is restricted to a single line. If it contains newlines, only the + first line is used. + + `PSR='$SHED_VI_MODE'` + `PSR='\T'` # show last command runtime on the right + +============================================================================== +5. Multi-Line Prompts *prompt-multiline* + + PS1 may span multiple lines using `\n`. The editor tracks line + positions for proper cursor movement and redrawing. + + `PS1='\e[1m\u@\h\e[0m\n\W\$ '` + + This displays the username and hostname on the first line, and the + working directory and prompt symbol on the second. + +============================================================================== +6. echo -p *echo-prompt* + + The `echo` builtin accepts a `-p` flag that enables prompt-style + expansion on its arguments. All prompt escape sequences listed above + are recognized. + + `echo -p '\u'` # prints the current username + `echo -p '\W'` # prints the truncated working directory + `echo -p '\e[31mred\e[0m'` # prints "red" in red + + The `-p` flag can be combined with `-n` (no trailing newline) and + `-e` (interpret escape sequences like `\n` and `\t`). When both `-e` + and `-p` are used, prompt expansion runs first, then escape sequence + interpretation. + +============================================================================== +7. Prompt Options (shopt) *prompt-options* + + The following options under `shopt prompt.*` affect prompt behavior: + + `prompt.trunc_prompt_path` *opt-trunc-path* + + Maximum number of path segments shown by `\W`. Default: 4. + + `prompt.highlight` *opt-highlight* + + Enable syntax highlighting in the input line. Default: true. + + `prompt.auto_indent` *opt-auto-indent* + + Automatically indent new lines to match the current nesting + depth. Default: true. + + `prompt.linebreak_on_incomplete` *opt-linebreak* + + Insert a newline when Enter is pressed on an incomplete command + (e.g. unclosed quotes or pipes). Default: true. + + `prompt.line_numbers` *opt-line-numbers* + + Display line numbers in the left margin for multi-line buffers. + Default: true. + + `prompt.leader` *opt-leader* + + The leader key sequence for |keymaps|. Default: `" "` (space). + + `prompt.comp_limit` *opt-comp-limit* + + Maximum number of completion candidates to display. Default: 100. + + `prompt.completion_ignore_case` *opt-comp-case* + + Case-insensitive tab completion. Default: false. + +============================================================================== +8. Special Variables *prompt-variables* + + `SHED_VI_MODE` *var-vi-mode* + + Set automatically before each prompt to the current vi mode name: + `NORMAL`, `INSERT`, `VISUAL`, `COMMAND`, `REPLACE`, `SEARCH`, or + `COMPLETE`. Useful in PSR or prompt functions. + + Example: + `PSR='$SHED_VI_MODE'` + +============================================================================== +9. Remote Refresh (SIGUSR1) *prompt-sigusr1* + + Sending `SIGUSR1` to the shell process causes it to re-expand and + redraw the prompt. This is useful for updating the prompt from + external processes (e.g. a background script that detects a state + change). + + `kill -USR1 $$` + + Combined with prompt functions (see |prompt-func|), this allows the + prompt to reflect changes that happen outside the shell's normal + command cycle. + +============================================================================== +See also: |keybinds| |autocmd| |ex| diff --git a/src/builtin/help.rs b/src/builtin/help.rs index 68643fe..6387d34 100644 --- a/src/builtin/help.rs +++ b/src/builtin/help.rs @@ -61,23 +61,32 @@ pub fn help(node: Node) -> ShResult<()> { let hpath = env::var("SHED_HPATH").unwrap_or_default(); + // search for prefixes of help doc filenames for path in hpath.split(':') { - let path = Path::new(&path).join(&topic); - if path.is_file() { - let Ok(contents) = std::fs::read_to_string(&path) else { + let dir = Path::new(path); + let Ok(entries) = dir.read_dir() else { continue }; + for entry in entries { + let Ok(entry) = entry else { continue }; + let path = entry.path(); + if !path.is_file() { continue; - }; - let filename = path.file_stem().unwrap().to_string_lossy().to_string(); + } + let stem = path.file_stem().unwrap().to_string_lossy(); + if stem.starts_with(&topic) { + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; - let unescaped = unescape_help(&contents); - let expanded = expand_help(&unescaped); - open_help(&expanded, None, Some(filename))?; - state::set_status(0); - return Ok(()); + let unescaped = unescape_help(&contents); + let expanded = expand_help(&unescaped); + open_help(&expanded, None, Some(stem.into_owned()))?; + state::set_status(0); + return Ok(()); + } } } - // didn't find an exact filename match, its probably a tag search + // didn't find a filename match, its probably a tag search for path in hpath.split(':') { let path = Path::new(path); if let Ok(entries) = path.read_dir() { diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 35c8c32..c5e1d7a 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -444,6 +444,173 @@ impl ShedVi { Ok(is_complete && is_top_level) } + fn handle_hist_search_key(&mut self, key: KeyEvent) -> ShResult<()> { + self.print_line(false)?; + match self.focused_history().fuzzy_finder.handle_key(key)? { + SelectorResponse::Accept(cmd) => { + let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect)); + + { + let editor = self.focused_editor(); + editor.set_buffer(cmd.to_string()); + editor.move_cursor_to_end(); + } + + self + .history + .update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat())); + self.editor.set_hint(None); + { + let mut writer = std::mem::take(&mut self.writer); + self.focused_history().fuzzy_finder.clear(&mut writer)?; + self.writer = writer; + } + self.focused_history().fuzzy_finder.reset(); + + with_vars([("_HIST_ENTRY".into(), cmd.clone())], || { + post_cmds.exec_with(&cmd); + }); + + write_vars(|v| { + v.set_var( + "SHED_VI_MODE", + VarKind::Str(self.mode.report_mode().to_string()), + VarFlags::NONE, + ) + }) + .ok(); + self.prompt.refresh(); + self.needs_redraw = true; + } + SelectorResponse::Dismiss => { + let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistoryClose)); + post_cmds.exec(); + + self.editor.set_hint(None); + { + let mut writer = std::mem::take(&mut self.writer); + self.focused_history().fuzzy_finder.clear(&mut writer)?; + self.writer = writer; + } + write_vars(|v| { + v.set_var( + "SHED_VI_MODE", + VarKind::Str(self.mode.report_mode().to_string()), + VarFlags::NONE, + ) + }) + .ok(); + self.prompt.refresh(); + self.needs_redraw = true; + } + SelectorResponse::Consumed => { + self.needs_redraw = true; + } + } + Ok(()) + } + + fn handle_completion_key(&mut self, key: &KeyEvent) -> ShResult { + self.print_line(false)?; + match self.completer.handle_key(key.clone())? { + CompResponse::Accept(candidate) => { + let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionSelect)); + + let span_start = self.completer.token_span().0; + let new_cursor = span_start + candidate.len(); + let line = self.completer.get_completed_line(&candidate); + self.focused_editor().set_buffer(line); + self.focused_editor().set_cursor_from_flat(new_cursor); + // Don't reset yet — clear() needs old_layout to erase the selector. + + if !self.history.at_pending() { + self.history.reset_to_pending(); + } + self + .history + .update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat())); + let hint = self.history.get_hint(); + self.editor.set_hint(hint); + self.completer.clear(&mut self.writer)?; + self.needs_redraw = true; + self.completer.reset(); + + write_vars(|v| { + v.set_var( + "SHED_VI_MODE", + VarKind::Str(self.mode.report_mode().to_string()), + VarFlags::NONE, + ) + }) + .ok(); + self.prompt.refresh(); + + with_vars([("_COMP_CANDIDATE".into(), candidate.clone())], || { + post_cmds.exec_with(&candidate); + }); + + Ok(true) + } + CompResponse::Dismiss => { + let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionCancel)); + post_cmds.exec(); + + let hint = self.history.get_hint(); + self.editor.set_hint(hint); + self.completer.clear(&mut self.writer)?; + write_vars(|v| { + v.set_var( + "SHED_VI_MODE", + VarKind::Str(self.mode.report_mode().to_string()), + VarFlags::NONE, + ) + }) + .ok(); + self.prompt.refresh(); + self.completer.reset(); + Ok(true) + } + CompResponse::Consumed => { + /* just redraw */ + self.needs_redraw = true; + Ok(true) + } + CompResponse::Passthrough => Ok(false) + } + } + + fn handle_keymap(&mut self, key: KeyEvent) -> ShResult> { + let keymap_flags = self.curr_keymap_flags(); + self.pending_keymap.push(key.clone()); + + let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &self.pending_keymap)); + if matches.is_empty() { + // No matches. Drain the buffered keys and execute them. + for key in std::mem::take(&mut self.pending_keymap) { + if let Some(event) = self.handle_key(key)? { + return Ok(Some(event)); + } + } + self.needs_redraw = true; + } else if matches.len() == 1 + && matches[0].compare(&self.pending_keymap) == KeyMapMatch::IsExact + { + // We have a single exact match. Execute it. + let keymap = matches[0].clone(); + self.pending_keymap.clear(); + let action = keymap.action_expanded(); + for key in action { + if let Some(event) = self.handle_key(key)? { + return Ok(Some(event)); + } + } + self.needs_redraw = true; + } + + // There is ambiguity. Allow the timeout in the main loop to handle this. + Ok(None) + } + /// Process any available input and return readline event /// This is non-blocking - returns Pending if no complete line yet pub fn process_input(&mut self) -> ShResult { @@ -457,138 +624,11 @@ impl ShedVi { while let Some(key) = self.reader.read_key()? { // If completer or history search are active, delegate input to it if self.focused_history().fuzzy_finder.is_active() { - self.print_line(false)?; - match self.focused_history().fuzzy_finder.handle_key(key)? { - SelectorResponse::Accept(cmd) => { - let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect)); - - { - let editor = self.focused_editor(); - editor.set_buffer(cmd.to_string()); - editor.move_cursor_to_end(); - } - - self - .history - .update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat())); - self.editor.set_hint(None); - { - let mut writer = std::mem::take(&mut self.writer); - self.focused_history().fuzzy_finder.clear(&mut writer)?; - self.writer = writer; - } - self.focused_history().fuzzy_finder.reset(); - - with_vars([("_HIST_ENTRY".into(), cmd.clone())], || { - post_cmds.exec_with(&cmd); - }); - - write_vars(|v| { - v.set_var( - "SHED_VI_MODE", - VarKind::Str(self.mode.report_mode().to_string()), - VarFlags::NONE, - ) - }) - .ok(); - self.prompt.refresh(); - self.needs_redraw = true; - continue; - } - SelectorResponse::Dismiss => { - let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistoryClose)); - post_cmds.exec(); - - self.editor.set_hint(None); - { - let mut writer = std::mem::take(&mut self.writer); - self.focused_history().fuzzy_finder.clear(&mut writer)?; - self.writer = writer; - } - write_vars(|v| { - v.set_var( - "SHED_VI_MODE", - VarKind::Str(self.mode.report_mode().to_string()), - VarFlags::NONE, - ) - }) - .ok(); - self.prompt.refresh(); - self.needs_redraw = true; - continue; - } - SelectorResponse::Consumed => { - self.needs_redraw = true; - continue; - } - } - } else if self.completer.is_active() { - self.print_line(false)?; - match self.completer.handle_key(key.clone())? { - CompResponse::Accept(candidate) => { - let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionSelect)); - - let span_start = self.completer.token_span().0; - let new_cursor = span_start + candidate.len(); - let line = self.completer.get_completed_line(&candidate); - self.focused_editor().set_buffer(line); - self.focused_editor().set_cursor_from_flat(new_cursor); - // Don't reset yet — clear() needs old_layout to erase the selector. - - if !self.history.at_pending() { - self.history.reset_to_pending(); - } - self - .history - .update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat())); - let hint = self.history.get_hint(); - self.editor.set_hint(hint); - self.completer.clear(&mut self.writer)?; - self.needs_redraw = true; - self.completer.reset(); - - write_vars(|v| { - v.set_var( - "SHED_VI_MODE", - VarKind::Str(self.mode.report_mode().to_string()), - VarFlags::NONE, - ) - }) - .ok(); - self.prompt.refresh(); - - with_vars([("_COMP_CANDIDATE".into(), candidate.clone())], || { - post_cmds.exec_with(&candidate); - }); - - continue; - } - CompResponse::Dismiss => { - let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionCancel)); - post_cmds.exec(); - - let hint = self.history.get_hint(); - self.editor.set_hint(hint); - self.completer.clear(&mut self.writer)?; - write_vars(|v| { - v.set_var( - "SHED_VI_MODE", - VarKind::Str(self.mode.report_mode().to_string()), - VarFlags::NONE, - ) - }) - .ok(); - self.prompt.refresh(); - self.completer.reset(); - continue; - } - CompResponse::Consumed => { - /* just redraw */ - self.needs_redraw = true; - continue; - } - CompResponse::Passthrough => { /* fall through to normal handling below */ } - } + self.handle_hist_search_key(key)?; + continue; + } else if self.completer.is_active() && self.handle_completion_key(&key)? { + // self.handle_completion_key() returns true if we need to continue the loop + continue; } else if self.mode.pending_seq().is_some_and(|seq| !seq.is_empty()) { // Vi mode is waiting for more input (e.g. after 'f', 'd', etc.) // Bypass keymap matching and send directly to the mode handler @@ -597,42 +637,8 @@ impl ShedVi { } self.needs_redraw = true; continue; - } else { - let keymap_flags = self.curr_keymap_flags(); - self.pending_keymap.push(key.clone()); - - let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &self.pending_keymap)); - if matches.is_empty() { - // No matches. Drain the buffered keys and execute them. - for key in std::mem::take(&mut self.pending_keymap) { - if let Some(event) = self.handle_key(key)? { - return Ok(event); - } - } - self.needs_redraw = true; - continue; - } else if matches.len() == 1 - && matches[0].compare(&self.pending_keymap) == KeyMapMatch::IsExact - { - // We have a single exact match. Execute it. - let keymap = matches[0].clone(); - self.pending_keymap.clear(); - let action = keymap.action_expanded(); - for key in action { - if let Some(event) = self.handle_key(key)? { - return Ok(event); - } - } - self.needs_redraw = true; - continue; - } else { - // There is ambiguity. Allow the timeout in the main loop to handle this. - continue; - } - } - - if let Some(event) = self.handle_key(key)? { - return Ok(event); + } else if let Some(event) = self.handle_keymap(key)? { + return Ok(event); } } if !self.completer.is_active() && !self.history.fuzzy_finder.is_active() {