Skip to content

[feat] auto-complete history plugins #140

Closed
Closed

Description

@LESdylan

Here’s a stronger version of the prompt, framed as an engineering task/specification rather than just a feature request. I also make the distinction between shell core behavior and plugin behavior explicit.

Objective

Implement an intelligent interactive command-autocompletion plugin for Hellish, located in the hellishrc_plugins project rather than in the POSIX shell core.

The goal is to reproduce the useful parts of the Zsh-style interactive completion/history experience, while keeping the Hellish core minimal and POSIX-oriented.

The feature should work directly inside the current command line while the user is typing, rather than requiring a separate history-search command.


Core behavior

The plugin should provide intelligent suggestions based on the user's current input.

For example, when the user types:

make

the shell could detect available Make targets and provide interactive completions such as:

make all
make clean
make fclean
make re
make test
make debug

The user should be able to navigate/select a suggestion and insert it into the current command line, or ignore it and continue typing.

The completion mechanism should also work for normal shell commands, arguments, paths, options, and other useful completion sources where possible.


History-aware suggestions

The plugin should not behave like a simple static autocomplete dictionary.

It should take into account the user's recent command history and usage patterns.

For example, if the history contains:

make re
make test
make re
make clean
make re

then entering:

make

should prioritize frequently/recently used commands.

The system should ideally combine several signals:

current input
      +
recent history
      +
frequency
      +
recency
      +
context
      +
available completions
      ↓
ranked suggestions

The exact ranking algorithm should be designed and documented rather than hard-coded arbitrarily.


Inline suggestion behavior

The most important UX requirement is that suggestions should appear while the user is editing the current command line.

For example:

❯ make re█
        └── suggested completion

The suggestion should ideally be displayed as an inline placeholder/ghost text, similar to modern shells and terminal tools.

The user should be able to:

  • accept the suggestion;
  • partially accept it if technically feasible;
  • cycle through alternatives;
  • ignore it;
  • continue editing normally;
  • invoke explicit completion when desired.

For example:

❯ make re
       └── ghost suggestion: `build`

Pressing an appropriate key could transform it into:

❯ make rebuild

without requiring the user to manually type the remaining characters.

The interaction must remain compatible with Hellish's existing line editor and should not interfere with normal cursor movement, deletion, history navigation, or Ctrl+C/Ctrl+D behavior.


Make target completion

Make targets are an important initial use case.

When the current command is:

make <TAB>

the plugin should inspect the relevant Makefile(s) and discover available targets.

Potential sources include:

Makefile
makefile
GNUmakefile
included Makefiles

where practical.

The plugin should avoid treating every arbitrary token as a target. It should understand the structure sufficiently to provide meaningful candidates.

For example:

❯ make c█

could suggest:

clean

while:

❯ make f█

could suggest:

fclean

The implementation should also consider whether the current working directory contains a Makefile before attempting Make-specific completion.


Intelligent contextual completion

The architecture should be extensible so that Make targets are only the first completion provider.

Ideally, the plugin should eventually support providers such as:

Command completion
        │
        ├── Executables
        ├── Shell builtins
        ├── Aliases
        ├── Functions
        ├── Paths/files
        ├── Make targets
        ├── Git branches
        ├── Git commands
        ├── Options/flags
        └── History-based suggestions

The completion engine should therefore be designed around a provider/ranking architecture rather than a large collection of hard-coded if statements.

For example:

Input
  │
  ▼
Context parser
  │
  ▼
Completion providers
  ├── History
  ├── Make
  ├── Filesystem
  ├── Commands
  └── Git
  │
  ▼
Candidate aggregation
  │
  ▼
Ranking
  │
  ▼
Interactive suggestion

Zsh-inspired behavior

The objective is not to clone Zsh internally.

Instead, study the relevant behavior of Zsh and similar modern shells and reproduce the useful UX while keeping the implementation appropriate for Hellish.

In particular, investigate:

  • inline autosuggestions;
  • history-based prediction;
  • completion candidate ranking;
  • contextual completion;
  • keyboard interaction;
  • completion caching;
  • performance considerations.

Avoid copying implementation details that are unnecessarily coupled to Zsh's architecture.


Architecture constraints

This functionality should live in:

hellishrc_plugins

and not become part of the Hellish POSIX shell core, unless a minimal core interface is genuinely required.

The separation should be approximately:

                    HELLISH
                       │
                stable plugin API
                       │
                       ▼
              hellishrc_plugins
                       │
        ┌──────────────┼──────────────┐
        ▼              ▼              ▼
   completion       history        providers
     engine          engine
        │
        ▼
   inline editor

The plugin should consume existing Hellish interfaces wherever possible instead of modifying the shell parser/executor unnecessarily.

If a change to Hellish core is unavoidable, clearly document:

  1. why it is required;
  2. the smallest required interface;
  3. why the same functionality cannot be implemented entirely inside the plugin.

Performance requirements

Completion happens during interactive typing, so latency is critical.

The plugin must not perform expensive work synchronously for every keystroke.

For example, avoid repeatedly:

parse entire Makefile
scan entire filesystem
search entire history
spawn multiple processes

for every character typed.

Investigate appropriate strategies such as:

  • caching;
  • incremental updates;
  • asynchronous completion generation;
  • invalidation when relevant files change;
  • bounded history searches;
  • candidate ranking without unnecessary process spawning.

The interactive shell must remain responsive even in large repositories.


Configuration

The plugin should expose configuration through the existing hellishrc plugin mechanism.

Potential configuration should include:

enable/disable autosuggestions
enable/disable Make completion
history suggestion limit
minimum suggestion length
ranking behavior
key bindings
maximum completion latency

Use the existing project conventions instead of inventing a second configuration system.


Testing

Add tests covering at minimum:

Make completion

Makefile exists
Makefile does not exist
multiple targets
partial target
target with special characters
included Makefiles
large Makefile

History

recent command
frequent command
duplicate commands
partial history match
no matching history
large history

Interactive behavior

accept suggestion
reject suggestion
cycle suggestions
cursor movement
backspace
Ctrl+C
Ctrl+D
history navigation
empty input

Regression

Ensure existing Hellish command-line behavior remains unchanged when the plugin is disabled.


Important engineering questions

Before implementing, investigate the existing Hellish line-editor architecture and determine:

  1. Where is interactive input currently handled?
  2. How are key sequences interpreted?
  3. How is the current editable buffer represented?
  4. How does Hellish currently implement history?
  5. Can the plugin observe changes to the input buffer?
  6. Can the plugin render ghost text without modifying the core renderer?
  7. What plugin API already exists?
  8. What minimal API additions would be necessary?
  9. Can completion providers run asynchronously?
  10. How can the plugin avoid blocking the shell while generating suggestions?

Do not immediately implement the feature.

First inspect the existing architecture and propose an implementation design that respects the current Hellish/plugin separation.


Expected deliverables

Produce:

  1. an architectural analysis of the existing Hellish line editor/plugin system;
  2. a proposed completion architecture;
  3. the minimal required Hellish/plugin API changes;
  4. the implementation;
  5. Make target completion;
  6. history-aware inline suggestions;
  7. keyboard interaction;
  8. tests;
  9. documentation and configuration examples;
  10. performance considerations and known limitations.

The final result should feel like a native Hellish interactive feature, while remaining cleanly isolated as a hellishrc_plugins capability.

The key principle is:

Hellish provides the shell; hellishrc_plugins provides the intelligent interactive UX.

Activity

  1. self-assigned this
    on Sep 24, 2026
  2. added theissue type on Sep 24, 2026
  3. LESdylan commented on Sep 26, 2026

    @LESdylan
    MemberAuthor

    The issue asks for the architecture analysis and a design before any implementation, so this comment is that first step. File references are to develop.

    What the line editor is today

    Input. hellish links the system GNU readline (8.0 or later reads in-process). There is no editor of its own; src/editing/ is a thin zsh-style layer on top of readline.

    • Call path: repl_shell → readline_cmd → get_more_input_readline → rl_read_inproc → readline().
    • The line lives in readline's rl_line_buffer / rl_point, and the shell keeps no copy between keys.

    Keys (all merged in #152):

    • bind works like bash's, including bind -x with READLINE_LINE / READLINE_POINT.
    • zle -N plus bindkey run a shell widget with BUFFER / LBUFFER / RBUFFER / CURSOR in scope. Assigning LBUFFER moves the cursor; assigning CURSOR is not read back.
    • bindkey KEY readline-command binds readline's own commands, and zle .forward-char calls them from a widget.

    History.

    • The shell's hist_cmds vector is the source of truth, mirrored into readline's list.
    • HISTFILE, HISTSIZE, HISTCONTROL and HISTIGNORE are honoured.
    • Prefix search ([feat] Smart history #136) is readline's history-search-backward / -forward. hellishrc_plugins binds Up/Down to them wherever bind exists (branch feat/history-prefix-search).

    Hooks.

    • Around each command: HELLISH_PRECMD_FUNCS, HELLISH_PREEXEC_FUNCS, precmd / preexec, chpwd_functions.
    • While a line is being edited, nothing runs shell code: no zsh special widget is honoured (zle-line-init, zle-line-pre-redraw, …).
    • rp_redisplay (src/platform/posix/rl_rprompt.c) already runs after every key, because it is readline's redisplay function. It paints RPROMPT, but it runs no shell code.

    Text that is not input. Nothing exists for this today: no POSTDISPLAY, no region_highlight. RPROMPT is the only thing painted beside the line. It is written by the redisplay hook in one write (save cursor, clear to end of line, text, restore cursor), and readline never learns it is there. Ghost text needs exactly that mechanism.

    Completion.

    • complete / compgen with -W, -F and the actions. fix/compgen-actions fixes -e -g -j -s -u, which listed files.
    • -F functions run in-process with COMP_* set, once shopt -s progcomp is on (rc.d/70-completion.hsh turns it on).
    • There is no make-target completion in either repository.

    Asynchronous work.

    • zle -F is not supported.
    • The reader waits on the terminal plus one extra descriptor: the background git status scan for the prompt.
    • A synchronous hook between keys is easy to add. Asynchronous suggestions would need that single descriptor turned into a list.

    Proposed split

    In hellish (core), the smallest interface that cannot be written in shell:

    1. zle-line-pre-redraw, the zsh name for "the buffer changed". It is a widget that hellish calls after an editing command changes rl_line_buffer / rl_point, with BUFFER / CURSOR in scope and edits collected, like any other widget.
      • Where: rl_getc_hook, before it waits for the next key.
      • It is skipped during incremental search, completion listings, numeric arguments and multi-key sequences, and while more input is already pending. Pasting 1000 characters therefore runs it once, not 1000 times.
      • Cost: no widget, no cost beyond one lookup.
    2. POSTDISPLAY, the zsh variable. It holds text painted after the cursor in a dim style and never becomes part of the line: accept-line, history and fc never see it.
      • Painting: by the same redisplay hook, and in the same single write, as RPROMPT.
      • Painted only when the cursor is at the end of the line on the primary prompt, and truncated so it never wraps.
      • Cleared on accept and on Ctrl-C, and unset at the start and end of each read.
    3. A capability hx_cap postdisplay can detect, the same way hooks and rprompt are detected.

    Everything else goes in hellishrc_plugins, as plugins/suggest/:

    • Providers: history, make targets, then the existing complete specs. Each is a function that appends candidates for the current buffer, so a new provider is a new function and not another if. Make comes first, as asked:
      • only when Makefile, makefile or GNUmakefile is present;
      • targets come from rule lines (^[A-Za-z0-9_./-]+: and not :=), plus included files one level deep;
      • special targets (.PHONY …) and pattern rules are excluded;
      • the result is cached per file and invalidated on mtime and size.
    • Ranking: one documented score per candidate: prefix match first, then frecency (count × recency decay over the last N history lines), then source order.
    • Hot path:
      • the history snapshot is taken once per prompt, from a precmd hook, into an array;
      • each key scans that array only up to the first match, with a bounded N;
      • no process is started per key;
      • Makefile parsing happens on a cache miss only.
    • Keys:
      • Right / End / Ctrl-F accept the whole suggestion;
      • Alt-F / Ctrl-Right accept one word;
      • Alt-N / Alt-P cycle through the alternatives;
      • every key keeps its ordinary meaning when no suggestion is shown;
      • Tab stays completion.
    • Configuration, through conf / hellish.conf:
      • HX_SUGGEST on|off;
      • HX_SUGGEST_MAKE on|off;
      • HX_SUGGEST_HISTORY_MAX;
      • HX_SUGGEST_MIN_CHARS;
      • HX_SUGGEST_STYLE.
    • When it cannot run: on a hellish without the capability the plugin reports the gap in conf doctor and loads nothing. Disabling it changes no key.

    Tests

    • Core:
      • a pty test with pyte that types, checks the dim text after the cursor, and checks that the cursor column has not moved;
      • then Right: the text becomes input;
      • Enter: the accepted command does not include the ghost text;
      • Ctrl-C, Ctrl-D, Backspace and history navigation;
      • a paste of 4 KB runs the hook once.
    • Plugin:
      • test/run.hsh cases for each provider: no Makefile, several targets, partial target, special characters, include, a 5000-rule file;
      • ranking: recent, frequent, duplicates, no match, 10 000 history lines;
      • plus the plugin-off regression: key bindings identical to a shell without it.

    This touches the core in two places (rl_getc.c and rl_rprompt.c) and adds one widget name and one variable, both with zsh's meaning. If this split is what you want, I'll do it as two PRs: the core interface with its pty tests first, then the plugin.


    Generated by Claude Code

  4. added a commit that references this issue on Oct 17, 2026
  5. qwO-Owp commented on Oct 17, 2026

    @qwO-Owp
    Member

    Progress update. Most of this follows @LESdylan's design above; one change is explained below.

    Done: #168, make target completion + the provider architecture

    Merged into develop at 0dacfc9, with every check green: CI, Platforms including macOS and WSL, and Docker distros.

    • make cl<TAB> completes clean. Targets come from GNUmakefile, makefile or Makefile (GNU make's order), parsed in-process: make is never run and nothing forks.
    • A provider table {command, function}, so a new per-command completion is one row plus one function. make and gmake are the first rows, and they also work behind sudo, env X=1, && and (.
    • What the parser skips: recipe lines, comments, define/endef, every kind of assignment, directives, and %, $ and .SPECIAL targets.
    • What the parser handles: a b c:, a::, backslash continuations (as GNU make reads them) and CRLF. Includes are followed one level deep.
    • The parsed list is cached per Makefile (dev, ino, mtime, size). No globals.
    • Fallbacks:
      • A user complete spec still wins.
      • Plain filename completion is used when there is no Makefile, on -C/-f, or when no target matches.
    • Tests: tests/complete_make_test.py has 24 case groups, in-process and forked, and all 10 mutants are killed.

    Next: inline suggestions

    • PR3a (in progress) shows the most recent matching history line as dim text after the cursor. Right/End/Ctrl-F accept it.
      • Off by default, turned on with HELLISH_AUTOSUGGEST=1. HELLISH_SUGGEST_MAX bounds how far back it scans.
      • Never suggested: multi-line entries, and entries containing control characters, so a history line cannot inject terminal escapes.
      • Your own bindings still win over the accept keys.
      • No extra cost per key: it stays within the existing per-keystroke syscall budget, and the ghost is drawn in the same single write as RPROMPT.
    • POSTDISPLAY comes from the core interface you proposed. It is exposed to zle widgets with zsh's meaning, so plugins in hellishrc_plugins can still paint their own text after the cursor.
    • The change: the suggestions themselves are computed in C rather than by a shell function. Running shell code on every keystroke would break the per-key budget that frontend_budget_test.py enforces (at most 9 syscalls and 3 writes per key).
    • PR3b: accept one word at a time (Alt-F / Ctrl-Right), and cycle through alternatives.
    • PR3c: frecency ranking (frequency × recency), kept accurate even with erasedups.

    This issue stays open until PR3b/3c land. #136 (prefix search on Up/Down) is done in #167.

  6. added 2 commits that reference this issue on Oct 17, 2026
  7. added a commit that references this issue on Oct 18, 2026
  8. qwO-Owp commented on Oct 18, 2026

    @qwO-Owp
    Member

    Done: every part of this issue is merged into develop

    Spec item Where
    make <TAB> targets, parsed in-process, cache invalidated on change #168
    provider architecture ({command, function} table) #168
    inline ghost suggestions, accept (Right/End/C-f), ignore, keep editing #169
    partial accept (Alt-F, Ctrl-Right), cycling (Alt-N/Alt-P) #175
    ranking by frequency and recency ("make prefers make re") #188
    ranked TAB candidates (make: targets you run come first) #188
    bounded history search (HELLISH_SUGGEST_MAX) #169
    caching and incremental updates (one scan; typing forward reuses it) #169, #188
    no process spawning, per-key cost (at most 9 syscalls and 3 writes per key, enforced in CI) frontend_budget_test.py
    terminal-injection safety (no control bytes painted) #169
    tests: make, history, interactive, regression (both readers, both allocators, mutation-checked) each PR

    Deliberate departures from the spec, and why

    • Native C, not a hellishrc_plugins plugin. Ghost text has to paint during readline's redisplay, and Alt-N/Alt-P have to bind keys without breaking user bindings. Neither is reachable from shell code without a new per-key callback API, which would cost a fork or an eval per key. The smallest interface for plugins is exposed instead: POSTDISPLAY (zsh's meaning) is readable and settable from zle widgets (feat(editor): inline history suggestions as dim ghost text (#140) #169).
    • No asynchronous generation. Nothing here blocks. Make targets are parsed in-process and cached, and the history scan is bounded and ranked by a sort in a reused buffer. The worst case is measured in CI rather than hidden behind a thread.
    • Configuration uses HELLISH_* variables, not conf/hellish.conf (no such builtin exists). HELLISH_AUTOSUGGEST, HELLISH_SUGGEST_MAX and HELLISH_SUGGEST_STRATEGY=recent are read at every prompt, so export/unset takes effect on the next line. HELLISH_HISTORY_PREFIX belongs to [feat] Smart history #136.

    Known limits (Caveat-marked in the code)

    • Recency counts history entries, not time (half-life 64 entries).
    • make ranking reads words: make a && make b credits both, and sudo make x isn't counted.

    The ranking is not released yet. It is in develop, and the notes are drafted in #189 for the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions