# zsh-smart-complete > A modern smart completion & suggestion layer for Zsh. > Engineered as the frontend of a future independent shell. > > **v2.5.0** — ↑ can now open your history as a table instead of walking it one entry per press: `SMART_MENU_HISTORY_PICKER` picks `fzf`, `fzf-popup`, `native` (no external program needed at all) or `native-popup`, off by default. The installer now states each companion tool's overlap *before* asking to install it, and enforces a single command-line highlighter. Alongside: ↑/↓ paging after a recall, and the ghost left behind by ^W/^U/^K/^D/^H, are fixed. [English](./README.md) · [简体中文](./README.zh-CN.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) ## Status | Channel | Status | | ------- | ------ | | Build & test (CI) | [![CI](https://github.com/imonior/zsh-smart-complete/actions/workflows/ci.yml/badge.svg)](https://github.com/imonior/zsh-smart-complete/actions/workflows/ci.yml) | | Release | [![Release](https://github.com/imonior/zsh-smart-complete/actions/workflows/release.yml/badge.svg)](https://github.com/imonior/zsh-smart-complete/actions/workflows/release.yml) | | Version | 2.5.0 | ## Why Replaces both `zsh-autocomplete` and `zsh-autosuggestions` in a single plugin with a clean modular architecture, designed to evolve into a standalone shell. - **Two halves, one engine (v2.2.0)** — while you type, the candidate list pops up *immediately* (the `zsh-autocomplete` behaviour) while the inline grey suggestion stays; `→` accepts it all, `Alt+→` accepts one word (the `zsh-autosuggestions` behaviour). One plugin, one keymap, two channels — the real fix for "the two plugins conflict". - **Zero external dependencies** — the core plugin is self-contained; Atuin is optional. - **Every arrow-key encoding is bound** — both `ESC [ C` and `ESC O C` (application cursor-keys mode, what `TERM=xterm-256color` actually sends) are bound, so you never get "grey text shows but the arrow does nothing". - **Syntax-highlighting friendly** — claims at most one `region_highlight` entry and removes only that one, so other highlighters are never clobbered. On zsh 5.9+ that entry is tagged `memo=zsh-smart-complete:suggestion`; on older builds the tag is left off, because there writing it costs the entry its colour. ## Architecture ``` zsh-smart-complete.plugin.zsh │ ┌─────────────────┼─────────────────┐ │ │ │ config state event/zle │ │ │ └─────────────────┼─────────────────┘ │ ┌───────────┴───────────┐ │ │ engine/suggest engine/native │ │ history/history (user compinit) │ engine/menu (type-to-popup list) │ zsh fc │ atuin (opt) │ smart-engine (future) └───────────────────┴───────────────────┘ │ display/ region_highlight ``` ## Quick start ### Prerequisites Make sure Zsh itself has `compinit`: ```zsh export HISTFILE="$HOME/.zsh_history" export HISTSIZE=1000000 export SAVEHIST=1000000 setopt appendhistory sharehistory histignorealldups autoload -Uz compinit compinit ``` ### Install > ⚠️ This plugin replaces **both** `zsh-autocomplete` and `zsh-autosuggestions`. #### Method A — one-line installer (recommended) ```zsh curl -fsSL https://raw.githubusercontent.com/imonior/zsh-smart-complete/main/install.sh | bash ``` Prompts are read from `/dev/tty`, so the interactive menus still work even though stdin is the script itself. #### Method B — Zinit ```zsh zinit light imonior/zsh-smart-complete ``` #### Method C — manual clone ```zsh git clone https://github.com/imonior/zsh-smart-complete.git ~/.zsh-smart-complete echo 'source ~/.zsh-smart-complete/zsh-smart-complete.plugin.zsh' >> ~/.zshrc ``` #### Mainland-China mirror The installer auto-detects your public-IP region and reports it. The region decides **which candidates are worth showing**: mainland China / not detected show every candidate and speed-test every candidate including direct (whether direct is really faster should be measured, not guessed from geography); **outside mainland China hides every preset mirror** and keeps only direct — those ghproxy / gitclone channels are mainland-only and are often slower than direct out there. Even outside mainland China, though, **direct is still speed-tested**, and both manual entries are always available: a **mirror source** (rewrites GitHub URLs) or a **full proxy** (exported as `HTTP_PROXY`/`HTTPS_PROXY` so curl/git/wget route everything through it, e.g. `http://127.0.0.1:7890`). Preset mirrors are labelled *China mainland only*. The snippets below are only needed for non-interactive installs. A mirror you enter by hand — or pass as `SMART_INSTALL_GH_MIRROR` — has to be an `https://` URL: that is the address the fetched scripts get executed through, so plaintext `http://` is refused rather than trusted. ```zsh curl -fsSL https://ghproxy.net/https://raw.githubusercontent.com/imonior/zsh-smart-complete/main/install.sh | SMART_INSTALL_GH_MIRROR=https://ghproxy.net/ bash ``` ## Configuration Set these variables **before** the plugin loads: ```zsh # Master switch : ${SMART_ENABLED:=true} # Engines : ${SMART_SUGGEST:=true} : ${SMART_COMPLETE:=true} : ${SMART_SUGGEST_STRATEGY:=history,completion} # history,completion | history (combined default: completion fills gaps when history has no match) # History backend: zsh | atuin | smart-engine (future) : ${SMART_HISTORY_BACKEND:=zsh} # UI : ${SMART_INLINE:=true} : ${SMART_SUGGEST_COLOR:=auto} # "auto" = fg=110 on 256-colour terms, fg=8 otherwise # Type-to-popup candidate list (the zsh-autocomplete half) : ${SMART_MENU:=true} : ${SMART_MENU_MIN_PREFIX_CMD:=2} # min chars in the COMMAND word before listing : ${SMART_MENU_MIN_PREFIX:=2} # min chars in an ARGUMENT word, counted after # the last "/" (0 = also right after a space) : ${SMART_MENU_MIN_MATCHES:=1} # min candidates before the live popup (1 = a single match also pops, like autocomplete) : ${SMART_MENU_MAX_MATCHES:=100} # more candidates than this -> no list (keeps huge dirs, and zsh's "see all N possibilities" prompt, away) : ${SMART_MENU_MAX_PREFIX:=64} : ${SMART_MENU_HISTORY_KEYS:=false} # true = up/down prefix-search history while the line is non-empty : ${SMART_MENU_HISTORY_PICKER:=off} # native = up opens your history as a table, nothing external; native-popup = that same table floating in tmux; fzf / fzf-popup = the same through fzf : ${SMART_MENU_HISTORY_PICKER_ROWS:=8} # native/native-popup only: rows on screen (a taller box would scroll out of reach) : ${SMART_MENU_HISTORY_PICKER_MAX:=1000} # native/native-popup only: newest entries the table reads : ${SMART_MENU_SINGLE_COLUMN:=false} # true = ONE candidate per line (opt-in: loses descriptions/colours/fuzzy); false = zsh's grid : ${SMART_MENU_LISTER:=builtin} # WHO draws the list: builtin = this plugin; fzf-tab = stop drawing, let an external picker own it # Throttle: OFF by default. Measured cost is only 10-30ms per listing, so there is # nothing to throttle; this knob is for a *persistently* expensive completion. When # on, a listing >= SLOW_MS buys COOLDOWN_KEYS skipped edits. Note: a skipped edit is # not repainted, so the on-screen list vanishes for that keystroke — which is exactly # why the default is 0. : ${SMART_MENU_SLOW_MS:=250} : ${SMART_MENU_COOLDOWN_KEYS:=0} # Debugging: set to a file path and every tick decision (gate refused / cooldown # swallowed / match count / measured cost) is appended there. "The popup didn't # appear" is otherwise indistinguishable from "one match, so the ghost took over". : ${SMART_MENU_DEBUG:=} # Recent directories: while completing `cd`, offer the directories you have # actually been in, and list them immediately on the EMPTY word after `cd ` # (the one place where an empty word is worth listing). Read-only — it consumes # zsh's own recent-dirs database and never records anything itself. : ${SMART_RECENT_PATHS:=true} : ${SMART_RECENT_PATHS_MAX:=20} ``` Named widgets are exposed too, so you can rebind them the way you would with `zsh-autosuggestions`: `smart-accept-suggestion` (accept the whole suggestion, bound to the right arrow), `smart-accept-word` (accept one word, bound to Alt+right-arrow), `smart-execute-suggestion` (accept, then run the line) and `smart-suggestion-toggle` (turn the grey ghost on/off). With `SMART_MENU_HISTORY_KEYS=true`, up/down prefix-search your history while the line is non-empty — off by default, because those keys carry strong muscle memory. Either way up/down are wrapped, in every keymap this plugin binds: the wrapper clears the grey suggestion the previous line left behind before the recalled entry is drawn. Without it a `git sta` + ↑ renders as `git log --onelinetus`, because the ghost text stays in `POSTDISPLAY`. Navigation itself is not changed by the wrapper — with the knob off, the original widget still runs. ## Optional extras ### Fuzzy matching (done by zsh, not by us) The live popup runs *your* completion system, so any matcher you configure applies to it automatically. To let `fb` match `foobar.txt`: ```zsh zstyle ':completion:*' matcher-list 'r:|[._-]=* r:|=*' 'l:|=* r:|=*' ``` There is nothing to switch on here — and no fuzzy-matching code on our side, which would only fight the completion system. ### Single-column popup (opt-in) `SMART_MENU_SINGLE_COLUMN=true` draws the type-to-popup as **one candidate per line** instead of zsh's native multi-column grid. It is **off by default, and deliberately so** — worth reading before you turn it on: - A vertical list can only be drawn by **generating** the candidates (there is no reliable way to capture compsys' own: shadowing `compadd` with a function makes several zsh builds stop adding matches entirely — measured). So this mode **bypasses `_main_complete`**, and for the contexts it covers you lose candidate **descriptions**, `list-colors` colouring, grouping, and your `zstyle ':completion:*' matcher-list` — the documented fuzzy matching does **not** apply to generated candidates. - Only commands / functions / aliases / builtins, filesystem paths and `cd` recent directories are generated. Commands are generated wherever the shell is still choosing one — the first word, and the word after `|`, `&&`, `;` or a bare wrapper like `sudo`; the recent directories answer a typed prefix, not only an empty word. Everything else (git subcommands, ssh hosts, `--options`, `~user`, and anything after a wrapper that already took a command, like `sudo git`) produces nothing here and falls through to the native grid, so **the popup changes shape while you type** — easily mistaken for a second list appearing. - A candidate wider than the terminal is clipped to one line (no ellipsis). The mechanism is arithmetic: every *display* string is padded — or clipped — to exactly `COLUMNS` wide, so exactly one column fits. The typed word is escaped before it becomes a glob or a prefix pattern, so a `[` in a filename cannot break the popup and a `#` cannot widen it into a pattern (a leading `~/` stays unescaped, so `~/…` candidates keep working). ### History picker on ↑ (opt-in) ```zsh export SMART_MENU_HISTORY_PICKER=native # the table below, no external program export SMART_MENU_HISTORY_PICKER=native-popup # the same table, floating in a tmux window export SMART_MENU_HISTORY_PICKER=fzf # or: fzf inline, fzf-popup floating (tmux/Zellij) export SMART_MENU_HISTORY_PICKER_ROWS=8 # native/native-popup only: rows on screen at once (1-40) export SMART_MENU_HISTORY_PICKER_MAX=1000 # native/native-popup only: newest entries it reads ``` ↑ then opens your history as a table instead of walking it one entry per press: newest first, and when you have typed something, only the entries that start with it. ↑/↓ move the selection, **Enter puts the chosen row on your line without running it** — the shell's Enter is what executes. One row per *line* of history, not per event: `fc` prints a multi-line entry over several lines and there is no way to tell that apart from two separate events without parsing timestamps, so every finder of this shape (fzf included) shows them as several rows. `native` is the mode with nothing to install: the rows are drawn by zsh itself, through the same list area its own completions use, so the box appears under the line you are typing and disappears as soon as you navigate or pick. It needs `autoload -Uz compinit && compinit` (no completion system → ↑ keeps doing plain history navigation), and it is bounded by design — `…_ROWS` rows on screen, read from the newest `…_MAX` entries — because a box taller than the space under the prompt scrolls into rows nothing can take back. To close it without picking anything, press ↓ past the newest row: the line stays exactly as you left it, because the table never edits it. (Esc stays zsh's own key — this mode borrows ↑ and ↓ only.) `fzf` and `fzf-popup` hand the same list to fzf, and there Esc is the cancel. `fzf` needs `fzf` on PATH; with fzf missing, or when nothing matches the prefix, ↑ keeps doing plain history navigation. `native-popup` is `native`'s table drawn in a window tmux composites **over** the pane — the floating form, with nothing to install. It needs `tmux` and a client attached to it (a plain terminal, or Zellij's tmux compatibility layer, is not that), so it resolves to `native` the moment there is no `TMUX` to answer, and a tmux that refuses at the press — `display-popup` unknown, no server, a popup that would not hand the choice back — puts the identical rows under the line instead. ↑ never hangs on either answer, and the press-time probe is not repeated forever: after two refusals in a row the mode is remembered as refused, and every later ↑ draws the inline table without asking tmux again. `smart-status` prints that reason next to the knob, and pointing `SMART_MENU_HISTORY_PICKER` at another mode and back re-arms the probe. Floating is tmux's window specifically: under Zellij this mode gives you the same table inline (only `fzf-popup` floats there). Inside the window the keys are the table's own: ↑/↓ move the marker, Home/End and PgUp/PgDn jump, Enter puts the row on your line, Esc (or Ctrl+G) cancels and leaves the line exactly as it was. Nothing your history contains reaches the command line: the rows travel to the window in a file inside a private directory, and the choice travels back as one line of data that is read, never evaluated. Where the window appears is not a choice we can offer. ZLE cannot draw above the line you are typing — those rows are already-printed scrollback — so a picker that stays inside the terminal is drawn below it, and your line is lifted above the box while you choose (that is what fzf's own `--height` means by "below the cursor"). The two `-popup` modes are the ones that really float, and they need a multiplexer: measured in a 24-row tmux pane the window occupied rows 1–7 while the input line stayed at the bottom. It is unrelated to `SMART_MENU_LISTER`, which decides who draws *completion* lists. The picker only ever shows history. `fzf`/`fzf-popup` take ↑ and nothing else; `native` takes ↓ too, because a table it owns needs a key that walks the marker back and closes it — and `native-popup` binds the same pair for the case where tmux refuses the window: those rows then open under the line, where ↓ is again how you walk off them (inside the window ↓ belongs to the table anyway). With no table on screen the ↓ widget does whatever the other knob asked for, so nothing is lost. Set it back to `off` — or run `smart-disable` — and both keys are handed back to whatever else binds them, which is how this coexists with atuin or zsh-history-substring-search. ### Recent directories While completing a `cd` / `pushd` / `chdir` argument, the directories you have actually been in are offered as candidates, and they are listed immediately on the **empty word** after `cd ` (the one place where an empty word is worth listing). The data is zsh's own recent-directories database — the same one `cdr` and `~[1]` use. The plugin only *reads* it and never writes anything. If yours is still empty, two lines turn collection on: ```zsh autoload -Uz chpwd_recent_dirs add-zsh-hook add-zsh-hook chpwd chpwd_recent_dirs ``` `smart-recent status` reports how many entries are usable right now. ### Which lister draws the list? (the 2-choose-1) Two completion listers are both entitled to draw, so two lists appearing at once is not a bug either of them can fix — one of them has to stop. `SMART_MENU_LISTER` picks the owner: | value | what happens | |---|---| | `builtin` (default) | this plugin drives zsh's list, as before | | `fzf-tab` | this plugin **draws nothing**; the external floating picker is the only list on screen | It does not install fzf-tab — it makes *this* plugin stop listing so that whatever other lister you run is the only one drawing. The inline grey suggestion is untouched: only the candidate list is handed over. With `fzf-tab` we also stop setting `zstyle ':completion:*' menu select` in the Tab widget, because zsh's selectable menu is itself a list drawer competing for the same screen. ```zsh smart-lister # who owns the list right now? smart-lister builtin | fzf-tab # switch it for this shell ``` The accepted spellings are: | spells this plugin | spells "hand it over" | |---|---| | `builtin` `smart` `internal` `native` `built-in` `on` `yes` `true` `1` | `fzf-tab` `fzf_tab` `fzf` `ftb` `external` `none` `off` `no` `false` `0` | `off` means "**our** lister off" — i.e. hand it over — not "no list at all", which is what `SMART_MENU=false` is for. An unrecognised value falls back to `builtin` (a typo must not silently kill the popup) and is reported as unrecognised; a mistyped *argument* to `smart-lister` is rejected with a non-zero status, so `smart-lister fzf-tb` can no longer look like a successful switch. When it goes wrong, `smart-doctor` is the answer: it prints the current owner, and if the list has been handed to a picker that is **not loaded** it says so and makes that its verdict — because "nothing is drawn at all" is a worse state than two lists. ### Two candidate lists at once? If two lists appear on screen at the same time, `smart-doctor` prints the fingerprints of every known lister, so the question becomes readable instead of arguable: ```zsh smart-doctor ``` It reports whether `_main_complete` / `compadd` / `_complete` are still zsh's stock entry points, whether `zsh-autocomplete` / `zsh-autosuggestions` / `fzf-tab` / syntax-highlighting / zsh-vi-mode are loaded and how each one relates to this plugin, who owns `Tab`, `↑`, `↓` and `Ctrl-R` in every keymap (with zsh's own built-in widgets labelled as such, so atuin's binding and zsh's default do not look alike), the zstyles that can enable a list, and this plugin's own state — then a verdict. It is read-only, so it is safe to run in a half-broken shell. ### Companion tools: overlap, conflict, consent The plugin's whole feature set is two engines — the type-to-popup candidate list and the inline grey suggestion — plus a history picker. Everything below is judged against those, and the installer prints the same statement above every question it asks about a third-party tool. | tool | relation | what happens | |---|---|---| | zsh-autocomplete | **replaced** — its list is ours | installed before? the installer comments its loader out; `smart-doctor` names it if it is still loaded | | zsh-autosuggestions | **replaced** — its grey ghost is ours | same | | fzf-tab | a **second lister**: two lists on one screen | asked, default **no**; saying yes sets `SMART_MENU_LISTER=fzf-tab`, i.e. we stop drawing | | zsh-history-substring-search | **overlaps** `↑`/`↓` prefix search | not installed by us; `SMART_MENU_HISTORY_PICKER=off` + `SMART_MENU_HISTORY_KEYS=false` hand both keys back | | atuin | **overlaps** the history UI, but is also a history *source* | asked; the plugin reads atuin's SQLite database for the ghost. `ATUIN_NOBIND` keeps its keys unbound unless you opt into its TUI | | zsh-vi-mode | **owns the keymaps**, re-inits ZLE per prompt | asked, default no; when on it loads before us and we re-bind from `zvm_after_init` | | fast-syntax-highlighting | independent (colours the line, draws no list) | asked by itself now — it used to be cloned without a question, and a declined tool must not arrive through `zinit light` at first shell start | | zsh-syntax-highlighting | **overlaps** the fast variant: both repaint the same command line | when the fast variant is present, the installer asks; saying yes comments its loader line out (a backup is kept). The fast side is the one that stays | | starship | independent: it draws the prompt, we draw what you type | asked; an existing `starship.toml` is never replaced without asking | | powerlevel10k | independent prompt theme | its clone is a question of its own (Oh-My-Zsh path and Zinit path), because setting `ZSH_THEME` at a theme that was not cloned breaks the next prompt | | zoxide | independent | left alone; the recommended config runs `zoxide init zsh` when it is installed | | zinit | plugin manager, no overlap | asked; decline it and the written config **sources this plugin directly** instead of loading nothing | | oh-my-zsh | framework: it owns completion start-up | only through the combo you named (`keep-omz`), and it is told what that means before it installs | | fzf (as your general fuzzy tool) | no overlap: its `Ctrl-R` / `Ctrl-T` / `Alt-C` stay its own | only `fzf` / `fzf-popup` picker modes need the binary | | tmux | needed only by the two `-popup` modes | away from a `TMUX` those modes resolve to their inline table, so no key ever hangs | | Zellij | no `native-popup` (only tmux's window has been measured here) | `fzf-popup` still floats under Zellij, because there the floating is fzf's own code | `NONINTERACTIVE=1` has nobody to ask, so it **declines** every third-party tool and prints `Skipped (NONINTERACTIVE: nobody was asked)` rather than taking the documented default: a `yes` written by a script would still fetch the thing a human never saw. A run that means the defaults says so explicitly with `SMART_INSTALL_THIRD_PARTY=1`. This applies to optional tools only — the plugin itself is always installed, and so is Oh-My-Zsh when the run named the combo that needs it. ### Interactive installer options The installer asks about every optional piece — fzf-tab, the single-column layout, recent directories, Up/Down history search, zsh-vi-mode and the suggestion source — and writes your answers into a managed block in `~/.zshrc`. The block sits *above* the plugin load on purpose: options such as `SMART_MENU_HISTORY_KEYS` are read while the plugin installs its key bindings, so writing them afterwards would be silently ignored. Re-running rewrites only that block; `NONINTERACTIVE=1` takes the documented defaults. Every third-party tool in the table above is a question of its own, and the line printed directly above that question is the overlap statement for it — an answer is only informed if the reason comes before it. `SMART_INSTALL_THIRD_PARTY=1` is the headless run's way of saying "the documented defaults, on my behalf"; without it a non-interactive install sets up this plugin and nothing else. fzf-tab is opt-in (default **off**) and, when enabled, forces the built-in selectable menu off: both are completion *listers*, and running two at once is exactly how you end up with two popups fighting over the same screen area. ## Runtime commands ```zsh smart-status # print current state + config smart-disable # disable the plugin smart-enable # re-enable smart-reindex # force a history index rebuild smart-menu on # turn the type-to-popup list on smart-menu off # turn it off (inline ghost text unaffected) smart-menu status # show menu config + last listing result smart-doctor # print every fingerprint of a SECOND candidate list (another lister) smart-lister builtin|fzf-tab # choose WHICH lister owns the list (fzf-tab = we stop drawing) smart-recent on|off|status # recent-dir candidates + `cd ` empty-word listing ``` ## Local settings script The installer creates a user settings file and a small CLI to manage it, so you can tune the plugin without ever touching `~/.zshrc`. The file lives at: ``` ${SMART_USER_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/zsh-smart-complete/settings.zsh} ``` Run `zsc-settings` (the installer symlinks it into `~/.local/bin/zsc-settings`, so make sure that directory is on your `PATH`, or call the script by its full path) any time after install: | Command | What it does | | --- | --- | | `zsc-settings` | interactive wizard — pick a setting, type a new value | | `zsc-settings list` | every setting with its effective (current) value | | `zsc-settings get KEY` | print one setting's effective value | | `zsc-settings set KEY VALUE` | validate + write one setting | | `zsc-settings edit` | open the file in `$EDITOR` | | `zsc-settings reset [KEY]` | drop one override (or all) → back to default | | `zsc-settings path` | print the settings file path | | `zsc-settings init` | (re)create the file with commented defaults | Values are written as plain `KEY='VALUE'` lines. The plugin sources this file **before** its built-in defaults, so anything you write overrides the default. After changing a value, **restart zsh** (e.g. `exec zsh`) for it to take effect. `set` validates the value against the setting's type (bool / int / enum / path) and refuses invalid input. To use a different file, point `SMART_USER_CONFIG` at it before zsh starts. When the wizard changes `SMART_MENU_HISTORY_PICKER` it also says what ↑ will actually get with that value on this terminal, and — for the two `-popup` values — it *asks* whether to switch the type-to-complete popup (`SMART_MENU`) off as well, so one knob owns the screen instead of two drawing over each other. It asks rather than doing it because that second setting is yours: candidates you liked enough to turn on should not be turned off by picking a history mode, and `smart-status` tells you what ↑ really does either way. ## Uninstall ```zsh ./install.sh --uninstall # over the one-liner, where argv does not survive the pipe: curl -fsSL https://raw.githubusercontent.com/imonior/zsh-smart-complete/main/install.sh | SMART_UNINSTALL=1 bash ``` It removes exactly what the installer wrote — the two managed blocks in `~/.zshrc`, the plugin checkout, `settings.zsh` and the `zsc-settings` symlink — and it asks before touching anything (a headless run counts `SMART_UNINSTALL=1` as the confirmation). `~/.zshrc` is copied to `~/.zshrc.bak.` first, and the uninstall refuses to edit it if that copy cannot be made. Packages the installer may have installed (fzf, starship, atuin, zinit) stay installed, and so do your `starship.toml` and every `.bak.*` file: they belong to the shell, not to this plugin. Restart zsh afterwards. ## Changelog See [CHANGELOG](./CHANGELOG.md) for the full history. The GitHub Release notes are extracted from these multi-language CHANGELOG files (en / zh-CN / zh-TW / ja / ko). ## Testing ```zsh ./tests/run-all.sh # every suite, one line each ./tests/run-all.sh -v # ... with full output ./tests/run-all.sh menu # only suites whose name matches ./tests/run-all.sh --list # what would run ``` `tests/run-all.sh` **discovers** `tests/test-*.zsh` (run with zsh) and `tests/test-*.sh` (run with bash), so adding a test file needs no other edit — the CI job used to list all twelve by hand, next to a comment admitting that a new file nobody added there is silently never run. It also sums the suites' assertion tallies, so the reported count is a measured number on every run. One suite, `tests/test-perf.zsh`, asserts wall-clock caps instead of behaviour: every quadratic-complexity bug this project has fixed produced perfectly correct output, and the only symptom was seconds. `install.sh` and `install-entware.sh` each contain one block generated from `lib/install/core.sh` (the functions that are identical in both installers), which is why the two files stay standalone enough for `curl … | bash`. To change that shared behaviour: edit `lib/install/core.sh`, run `tools/build-installers.sh`, and commit both installers. `tools/build-installers.sh --check` is what CI runs; `tests/test-installer-shared.sh` covers the rest of the contract between the two files, including the list of duplications that remain on purpose. **Test summary:** `./tests/run-all.sh` runs every suite and prints the file and assertion counts it measured; all of them pass, 0 failures. The installer also now **scans other startup files** (`.zprofile`, `.zshenv`, `conf.d/*.zsh`, `.zshrc.d/*`, `/etc/zsh/zshrc`) for left-over loaders of `zsh-autocomplete` / `zsh-autosuggestions` after cleaning `~/.zshrc`, and **warns** (with exact `file:line`) if it finds any — it never edits those files. See CHANGELOG `[v2.2.5]`. Key behaviours are additionally verified end-to-end against a real `zsh -i` in a tmux pane, asserting on the rendered screen. That suite scored **24/49 on v2.1.6** back when it held 49 assertions — one of those 49 is not even reached there, because its section stops after a failure — while the two scenarios added since check the single-column list, which v2.1.6 does not draw at all. On that version the type-to-popup does not exist, the `SS3` and `Alt+→` encodings are dead, `Tab` followed by `Enter` is swallowed, recent directories are not listed, and neither the switch nor the opt-in single-column layout exists. Several of those 24 passes are *vacuous* — they assert that no list was drawn, and on v2.1.6 no list is ever drawn — which is why the baseline is measured rather than scaled from an older number. The suite also carries a **buffer-integrity** assertion — the prompt line must equal what was typed, then the command that actually ran must print the expected output — because a popup that silently swallows one keystroke per drawn list still "passes" every look-at-the-screen check. The harness ships in the repo (auto-skips without `tmux`): ```zsh ./tests/e2e-tmux.sh # advisory; asserts on the rendered screen ./tests/e2e-tmux.sh /tmp/zsc-v216 # A/B an older release ``` The method is written up in the `headless-pty-zle-verify` skill. `tests/test-repaint.zsh` covers the part the tmux harness **structurally cannot see**: the bytes zsh writes per keystroke. tmux undoes a newline-plus-cursor-up pair, so its screen *and* its scrollback come out identical whether or not the plugin emits a scroll-inducing redraw on every keypress. The test drives a real `zsh -i` through `zsh/zpty` and reads the raw byte stream, asserting one invariant: **one keystroke stays on one line** — no newline, no vertical cursor move, no screen erase. Measured, one keystroke with a two-line prompt: | build | bytes | scroll-inducing newline | | --- | --- | --- | | a redraw on every keystroke | 96 | yes — and 32 bytes even with the ghost *and* the popup switched off, where stock zsh writes 1 | | this release | 33 | no — and 1 byte with both switched off, exactly stock zsh | It fails on the old build and passes on this one, so a future "fix" that reinstates the redraw is caught by CI rather than by a user. See CHANGELOG `[v2.2.10]`. ## License MIT — see [LICENSE](./LICENSE).