README.md
April 5, 2026 · View on GitHub
Zsh/Fish fuzzy completion and utility plugin with Deno.
Features
- Insert snippet and abbrev snippet
- Completion with fzf
- Builtin git completion
- User defined completion
- ZLE utilities
- Persistent preprompt prefix (placeholder jump)
- Smart History Selection (global / repository / directory / session scopes, delete, export/import)
Demo
Abbrev snippet

Completion with fzf

Requirements
Installation
zinit
zinit ice lucid depth"1" blockf
zinit light yuki-yano/zeno.zsh
sheldon
[plugins.zeno]
github = "yuki-yano/zeno.zsh"
use = ["zeno-plugin.zsh"]
git clone
$ git clone https://github.com/yuki-yano/zeno.zsh.git
$ echo "source /path/to/dir/zeno.zsh" >> ~/.zshrc
source /path/to/dir/zeno.zsh remains the default installation method and
still performs the full initialization eagerly, so existing users can update
without changing their setup.
Lazy-load for Zsh
If you want lazy-load, source zeno-bootstrap.zsh and use the upstream lazy key
API:
source /path/to/dir/zeno-bootstrap.zsh
zeno-bind-default-keys --lazy
zsh-defer zeno-preload
For a custom bind:
source /path/to/dir/zeno-bootstrap.zsh
zeno-register-lazy-widget zeno-completion
bindkey '^i' zeno-completion
Details for state variables, public APIs, and fallback behavior are in Lazy-load APIs for Zsh.
Fish Shell Support (Experimental)
Fish support is experimental. A quick overview is included here; full installation and configuration details are available later in the document.
Usage
CLI
zeno history ...zeno server {start|stop|restart|status|run}
Abbrev snippet
Require user configuration file
$ gs<Space>
Insert
$ git status --short --branch
$ gs<Enter>
Execute
$ git status --short --branch
Completion
$ git add <Tab>
Git Add Files> ...
Insert snippet
Use zeno-insert-snippet zle
Preprompt prefix
- Bind a key to
zeno-preprompt(example:bindkey '^xp' zeno-preprompt). - Invoke it with a non-empty buffer to save that text as the next prompt prefix (a space is auto-appended).
- Start typing on the next line with the prefix already inserted; call the widget again with an empty buffer to reset.
- You can embed
{{placeholder}}in the prefix; the first one is replaced and the cursor starts there. Use the existingnext-placeholderwidget to jump through the remaining placeholders. - Use
zeno-preprompt-snippetto pick a configured snippet (via fzf or by passing a snippet name as an argument) and set it as the next prompt prefix.
History Selection
- Press
Ctrl-Rto open the classic History widget. - Fzf searches the command column; press Enter to paste the selected command into the prompt immediately.
- The classic picker operates directly on your shell history (
HISTFILE) and does not depend on the SQLite subsystem.
Smart History Selection (Experimental)
- Press
Ctrl-Rto open the Smart History Search widget; press it again to cycle the scope in the orderglobal → repository → directory → session. - Search the command column (fzf ignores the time column), and press Enter to paste the raw command into your prompt.
- Press
Ctrl-Dfor a soft delete (logical delete) orAlt-Dfor a hard delete that also edits yourHISTFILE. - Use
zeno history query|log|delete|export|importto work directly with the SQLite-backed history from scripts. - Configure
history.fzf_command/history.fzf_optionsin your YAML or TypeScript config to override the fzf (or fzf-tmux) command and its options.
Change ghq managed repository
Use zeno-ghq-cd zle
Post-hook for ghq-cd
You can execute custom processing after changing directory by registering a post-hook.
For Zsh:
Register a ZLE widget named zeno-ghq-cd-post-hook:
# Example: Rename tmux session to repository name
function zeno-ghq-cd-post-hook-impl() {
local dir="$ZENO_GHQ_CD_DIR"
if [[ -n $TMUX ]]; then
local repository=${dir:t}
local session=${repository//./-}
tmux rename-session "${session}"
fi
}
zle -N zeno-ghq-cd-post-hook zeno-ghq-cd-post-hook-impl
For Fish:
Define a function named zeno-ghq-cd-post-hook:
# Example: Rename tmux session to repository name
function zeno-ghq-cd-post-hook
set -l dir "$ZENO_GHQ_CD_DIR"
if set -q TMUX
set -l repository (basename "$dir")
set -l session (string replace -a '.' '-' $repository)
tmux rename-session "$session" 2>/dev/null
end
end
The hook can access the selected directory path via the ZENO_GHQ_CD_DIR environment variable.
Configuration files
zeno loads configuration files from the project and user config directories and
merges them in priority order. You can explicitly set the workspace config
directory with ZENO_LOCAL_CONFIG_PATH; otherwise zeno loads .zeno/ in the
detected project root (unless disabled by setting
ZENO_DISABLE_AUTOMATIC_WORKSPACE_LOOKUP to '1'). After workspace config loading,
zeno loads the user config directory ($ZENO_HOME or ~/.config/zeno/), and
finally any XDG config directories. Within each location, files are merged
alphabetically. Both YAML (*.yml, *.yaml) and TypeScript (*.ts) files are
supported, so you can pick the format that suits your workflow. TypeScript
configs can import defineConfig and types from jsr:@yuki-yano/zeno, giving
you access to the full ConfigContext for dynamic setups.
Configuration example
Completion and abbrev snippet
# if defined load configuration files from there
# export ZENO_HOME=~/.config/zeno
# if disable deno cache command when plugin loaded
# export ZENO_DISABLE_EXECUTE_CACHE_COMMAND=1
# if enable fzf-tmux
# export ZENO_ENABLE_FZF_TMUX=1
# if setting fzf-tmux options
# export ZENO_FZF_TMUX_OPTIONS="-p"
# by default a unix domain socket is used
# if disable it
# export ZENO_DISABLE_SOCK=1
# timeout for socket client reads in seconds (fractional values are allowed)
# default: 0.3
# export ZENO_CLIENT_TIMEOUT_SECONDS=0.3
# if disable builtin completion
# export ZENO_DISABLE_BUILTIN_COMPLETION=1
# if disable automatic lookup of projectRoot/.zeno
# export ZENO_DISABLE_AUTOMATIC_WORKSPACE_LOOKUP=1
# load workspace-local config from explicit directory
# absolute path or project-root-relative path
# export ZENO_LOCAL_CONFIG_PATH=.zeno
# default
export ZENO_GIT_CAT="cat"
# git file preview with color
# export ZENO_GIT_CAT="bat --color=always"
# default
export ZENO_GIT_TREE="tree"
# git folder preview with color
# export ZENO_GIT_TREE="eza --tree"
# upstream default key set
# zeno-bind-default-keys
# upstream lazy key set
# source /path/to/dir/zeno-bootstrap.zsh
# zeno-bind-default-keys --lazy
# zsh-defer zeno-preload
if [[ -n $ZENO_LOADED ]]; then
bindkey ' ' zeno-auto-snippet
# fallback if snippet not matched (default: self-insert)
# export ZENO_AUTO_SNIPPET_FALLBACK=self-insert
# if you use zsh's incremental search
# bindkey -M isearch ' ' self-insert
bindkey '^m' zeno-auto-snippet-and-accept-line
bindkey '^i' zeno-completion
bindkey '^xx' zeno-insert-snippet # open snippet picker (fzf) and insert at cursor
bindkey '^x ' zeno-insert-space
bindkey '^x^m' accept-line
bindkey '^x^z' zeno-toggle-auto-snippet
# preprompt bindings
bindkey '^xp' zeno-preprompt
bindkey '^xs' zeno-preprompt-snippet
# Outside ZLE you can run `zeno-preprompt git {{cmd}}` or `zeno-preprompt-snippet foo`
# to set the next prompt prefix; invoking them with an empty argument resets the state.
bindkey '^r' zeno-history-selection # classic history widget
# bindkey '^r' zeno-smart-history-selection # smart history widget
# fallback if completion not matched
# (default: fzf-completion if exists; otherwise expand-or-complete)
# export ZENO_COMPLETION_FALLBACK=expand-or-complete
fi
Builtin completion
- git
- add
- diff
- diff file
- checkout
- checkout file
- switch
- reset
- reset file
- restore
- fixup and squash commit
- rebase
- merge
See: src/completion/source/git.ts
User configuration file
The configuration files are discovered and merged in the following order.
- If
$ZENO_LOCAL_CONFIG_PATHis set, load all<resolved-path>/*.yml/*.yaml/*.ts(A→Z).- Absolute paths are used as-is.
- Relative paths are resolved from the detected project root.
- If
$ZENO_LOCAL_CONFIG_PATHis not set and$ZENO_DISABLE_AUTOMATIC_WORKSPACE_LOOKUPis not set to'1', and the detected project root contains.zeno/, load.zeno/*.yml/*.yaml/*.ts(A→Z). - If
$ZENO_HOMEis a directory, merge all*.yml/*.yaml/*.tsdirectly under it. - For each path in
$XDG_CONFIG_DIRS, ifzeno/exists, merge allzeno/*.yml/*.yaml/*.ts(directories are processed in the order provided by XDG). - Fallbacks for backward compatibility (used only when no files were found in
the locations above):
$ZENO_HOME/config.yml$XDG_CONFIG_HOME/zeno/config.ymlor~/.config/zeno/config.yml- Find
.../zeno/config.ymlfrom each in$XDG_CONFIG_DIRS
Example (YAML)
You can keep everything in a single file, or split it into multiple YAML files
such as 10-snippets.yml and 20-completions.yml. YAML files are loaded
non-recursively and merged in alphabetical order within each config directory.
$ touch ~/.config/zeno/10-snippets.yml
$ touch ~/.config/zeno/20-completions.yml
For example:
# ~/.config/zeno/10-snippets.yml
snippets:
# snippet and keyword abbrev
- name: git status
keyword: gs
snippet: git status --short --branch
# snippet with placeholder
- name: git commit message
keyword: gcim
snippet: git commit -m '{{commit_message}}'
- name: "null"
keyword: "null"
snippet: ">/dev/null 2>&1"
# auto expand condition
# If not defined, it is only valid at the beginning of a line.
context:
# buffer: ''
lbuffer: '.+\s'
# rbuffer: ''
- name: branch
keyword: B
snippet: git symbolic-ref --short HEAD
context:
lbuffer: '^git\s+checkout\s+'
evaluate: true # eval snippet
# ~/.config/zeno/20-completions.yml
completions:
# simple sourceCommand, no callback
- name: kill signal
patterns:
- "^kill -s $"
sourceCommand: "kill -l | tr ' ' '\\n'"
options:
--prompt: "'Kill Signal> '"
# use excludePatterns and callback
- name: kill pid
patterns:
- "^kill( .*)? $"
excludePatterns:
# -l, -n or -s is followed by SIGNAL instead of PID
- " -[lns] $"
sourceCommand: "LANG=C ps -ef | sed 1d"
options:
--multi: true
--prompt: "'Kill Process> '"
callback: "awk '{print \$2}'"
# Use null (\0) termination Input / Output
- name: chdir
patterns:
- "^cd $"
sourceCommand: "find . -path '*/.git' -prune -o -maxdepth 5 -type d -print0"
options:
# Added --read0 if null termination is used in `sourceCommand` output.
--read0: true
--prompt: "'Chdir> '"
--preview: "cd {} && ls -a | sed '/^[.]*$/d'"
callback: "cut -z -c 3-"
callbackZero: true # null termination is used in `callback` I/O
TypeScript config files can also replace sourceCommand with a sourceFunction
that returns completion candidates programmatically. The function receives the
same ConfigContext as defineConfig and may return a ReadonlyArray<string>
or a Promise of it. Only one of sourceCommand or sourceFunction can be
specified for a completion.
For post-filtering selected values, TypeScript configs can use
callbackFunction instead of shell callback.
callback/callbackZeroandcallbackFunctionare mutually exclusivecallbackFunctionreceives{ selected, context, lbuffer, rbuffer, expectKey }callbackFunctionmust returnReadonlyArray<string>(orPromiseof it)previewFunctionreceives{ item, context, lbuffer, rbuffer }previewFunctionmust return preview text asstring(orPromiseof it)previewFunctionand static preview options (preview/options["--preview"]) are mutually exclusive
Example (TypeScript)
TypeScript configs can be split into multiple files. Each file returns a partial
Settings object and zeno merges them in alphabetical order.
// ~/.config/zeno/10-snippets.ts
import { defineConfig } from "jsr:@yuki-yano/zeno";
export default defineConfig(({ projectRoot, currentDirectory }) => ({
snippets: [
{
name: "git status",
keyword: "gs",
snippet: "git status --short --branch",
},
{
name: "branch",
keyword: "B",
snippet: "git symbolic-ref --short HEAD",
context: { lbuffer: "^git\\s+checkout\\s+" },
evaluate: true,
},
{
name: "null",
keyword: "null",
snippet: ">/dev/null 2>&1",
context: { lbuffer: ".+\\s" },
},
],
}));
// ~/.config/zeno/20-completions.ts
import { defineConfig } from "jsr:@yuki-yano/zeno";
import { join } from "jsr:@std/path@^1.0.0/join";
export default defineConfig(({ projectRoot, currentDirectory }) => ({
completions: [
{
name: "kill signal",
patterns: ["^kill -s $"],
sourceCommand: "kill -l | tr ' ' '\\n'",
options: { "--prompt": "'Kill Signal> '" },
},
{
name: "kill pid",
patterns: ["^kill( .*)? $"],
excludePatterns: [" -[lns] $"],
sourceCommand: "LANG=C ps -ef | sed 1d",
options: { "--multi": true, "--prompt": "'Kill Process> '" },
callback: "awk '{print \$2}'",
},
{
name: "chdir",
patterns: ["^cd $"],
sourceCommand:
"find . -path '*/.git' -prune -o -maxdepth 5 -type d -print0",
options: {
"--read0": true,
"--prompt": "'Chdir> '",
"--preview": "cd {} && ls -a | sed '/^[.]*$/d'",
},
callback: "cut -z -c 3-",
callbackZero: true,
},
{
name: "npm scripts",
patterns: ["^npm run $"],
sourceFunction: async ({ projectRoot }) => {
try {
const pkgPath = join(projectRoot, "package.json");
const pkg = JSON.parse(
await Deno.readTextFile(pkgPath),
) as { scripts?: Record<string, unknown> };
return Object.keys(pkg.scripts ?? {});
} catch {
return [];
}
},
options: { "--prompt": "'npm scripts> '" },
callbackFunction: ({ selected, expectKey }) => {
if (expectKey === "alt-enter") {
return selected.map((script) => `${script} -- --watch`);
}
return selected;
},
previewFunction: async ({ item, context }) => {
try {
const pkgPath = join(context.projectRoot, "package.json");
const pkg = JSON.parse(
await Deno.readTextFile(pkgPath),
) as { scripts?: Record<string, string> };
const script = pkg.scripts?.[item];
return script ? `${item}\n${script}` : item;
} catch {
return item;
}
},
},
],
}));
Smart History Selection settings (Experimental)
Configure the zeno-smart-history-selection widget and the zeno history …
CLI via a history block in any loaded YAML or TypeScript config file:
history:
defaultScope: global # "global" | "repository" | "directory" | "session"
fzfCommand: fzf-tmux # Override the command that spawns the picker
fzfOptions:
- "-p 50%,50%" # Additional arguments passed to the picker command
redact: [] # Strings to hide from the History view
keymap:
deleteSoft: ctrl-d # Soft delete (logical delete)
deleteHard: alt-d # Hard delete (edits HISTFILE)
toggleScope: ctrl-r # Cycle through scopes within the widget
togglePreview: ? # Toggle the preview window
Lazy-load APIs for Zsh
Public state:
ZENO_BOOTSTRAPPED=1: bootstrap is complete and zeno functions / widgets are availableZENO_LOADED=1: heavy init is complete and socket / history / preprompt are ready
Bootstrap (source zeno-bootstrap.zsh):
- sets
ZENO_ROOT - appends
bin/fpath - exposes bundled
_zenoCLI completion - autoloads functions and widgets
- registers ZLE widgets with their original names
- sets
ZENO_BOOTSTRAPPED=1
Heavy init (zeno-init / zeno-ensure-loaded):
- optional
deno cache - socket setup (
zeno-enable-sock) - history hooks (
zeno-history-hooks) - preprompt hooks (
zeno-preprompt-hooks) - sets
ZENO_LOADED=1
Public APIs:
zeno-initzeno-ensure-loadedzeno-preloadzeno-register-lazy-widget <widget>zeno-register-lazy-widgets <widget>...zeno-bind-default-keyszeno-bind-default-keys --lazy
Builtin lazy fallback behavior:
zeno-completion->ZENO_COMPLETION_FALLBACK, otherwisefzf-completionorexpand-or-completezeno-auto-snippet->ZENO_AUTO_SNIPPET_FALLBACK, otherwiseself-insertzeno-auto-snippet-and-accept-line->accept-linezeno-history-selection->history-incremental-search-backwardzeno-smart-history-selection->zeno-history-selection, otherwisehistory-incremental-search-backwardzeno-insert-space-> literal space insertion
This keeps widget names unchanged, so eager and lazy setups can both use
bindkey '^i' zeno-completion and similar bindings without user-defined wrapper
widgets.
Fish usage
Installation for Fish
Using Fisher (Recommended)
fisher install yuki-yano/zeno.zsh
Fisher will automatically clone the full repository to ~/.local/share/zeno.zsh
and set up the necessary paths.
Manual installation
git clone https://github.com/yuki-yano/zeno.zsh.git /path/to/zeno.zsh
echo "set -gx ZENO_ROOT /path/to/zeno.zsh" >> ~/.config/fish/config.fish
ln -s /path/to/zeno.zsh/shells/fish/conf.d/zeno.fish ~/.config/fish/conf.d/
Note: Setting ZENO_ROOT explicitly is recommended for manual installations to
avoid path resolution issues.
Configuration for Fish
Copy the example key bindings:
cp /path/to/zeno.zsh/shells/fish/conf.d/zeno-keybindings.fish.example ~/.config/fish/conf.d/zeno-keybindings.fish
Or manually set up key bindings in your config.fish:
if test "$ZENO_LOADED" = "1"
bind ' ' zeno-auto-snippet
bind \r zeno-auto-snippet-and-accept-line
bind \t zeno-completion
bind \cx\x20 zeno-insert-space
end
Available features for Fish
- Abbrev snippet expansion - Expand abbreviations with Space key
- Auto snippet and accept line - Expand and execute with Enter key
- Fuzzy completion - Tab completion with fzf
- Insert literal space - Insert space without expansion (Ctrl-X Space)
- Snippet placeholder navigation - Navigate through snippet placeholders
- History selection - Fuzzy search command history (Ctrl-R)
- GHQ repository navigation - Navigate to ghq-managed repositories
- Insert snippet - Select and insert snippets from list
- Toggle auto snippet - Enable/disable automatic snippet expansion
Current limitations for Fish
- Socket mode is enabled by default (disable with ZENO_DISABLE_SOCK=1)
- Key binding syntax differs from Zsh
FAQ
Q: zsh-syntax-highlighting does not work well.
A: Use fast-syntax-highlighting instead.
Related project
Inspiration
- Preprompt feature inspired by by-binds-yourself