zellij-history-selector is a floating Zellij plugin for searching history and snippets from multiple sources, previewing the selected entry, and inserting it back into the pane you were using before opening the plugin.
It is built for practical workflows:
- shell history
- IPython history
- SQLite-backed local history stores
- clipboard managers such as CopyQ
- custom scripts that export lines or JSON
- opens in a floating pane
- captures the pane you were on before opening the plugin
- filters entries interactively
- previews multiline entries
- switches between multiple providers
- inserts the selected entry back into the original pane
- can optionally execute the selected entry immediately
rustup target add wasm32-wasip1
cargo build --release --target wasm32-wasip1Artifact:
target/wasm32-wasip1/release/zellij-history-selector.wasm
Place the built or downloaded plugin at:
~/.config/zellij/plugins/zellij-history-selector.wasm
If you do not want to build it yourself, download the .wasm release asset from:
https://github.com/longhongc/zellij-history-selector/releases/latest
Add a plugin alias and a keybind to ~/.config/zellij/config.kdl.
keybinds {
shared_except "locked" {
bind "Alt r" {
LaunchOrFocusPlugin "zellij-history-selector" {
floating true
move_to_focused_tab true
}
}
}
}
plugins {
zellij-history-selector location="file:~/.config/zellij/plugins/zellij-history-selector.wasm" {
default_mode "insert"
max_results "500"
preview_lines "10"
case_sensitive "false"
providers "shell"
provider.shell.type "file_lines"
provider.shell.name "Shell"
provider.shell.path "~/.bash_history"
provider.shell.reverse "true"
provider.shell.dedupe "true"
provider.shell.limit "5000"
}
}Important:
- launch the plugin by alias name:
LaunchOrFocusPlugin "zellij-history-selector" - set
move_to_focused_tab trueso triggering the keybind from another tab brings the plugin to the current tab instead of jumping back to the old one - do not launch the raw
file:/.../plugin.wasmpath if you want theplugins { ... }config block to apply - build the
.wasmyourself or download it, then place it at~/.config/zellij/plugins/zellij-history-selector.wasm
Provider config uses a simple namespaced shape:
providers "shell,ipython,copyq"provider.shell.type "file_lines"provider.ipython.type "ipython"provider.copyq.type "command_json"
These top-level options control picker behavior:
default_modeDefault action when selecting an entry. Supported values:insert: insert the selected text into the target paneexecute: insert the selected text and execute itcopy: copy the selected text to the clipboard without inserting it Current default:insert
profileSelects which named provider profile this plugin alias should use. When omitted, the plugin uses the fullproviderslist unlessprofilesis configured, in which case it defaults to the first listed profile.max_resultsMaximum number of filtered matches shown in the picker. Current default:500preview_linesMaximum number of preview lines reserved for the lower preview area. Current default:10case_sensitiveWhentrue, search matching becomes case-sensitive. Current default:false
You can combine multiple providers in one picker by listing them in providers and then configuring each one under its own provider.<id>.* namespace.
providers "shell,ipython,copyq"
provider.shell.type "file_lines"
provider.shell.name "Shell"
provider.shell.path "~/.bash_history"
provider.shell.reverse "true"
provider.shell.dedupe "true"
provider.ipython.type "ipython"
provider.ipython.name "IPython"
provider.ipython.path "~/.ipython/profile_default/history.sqlite"
provider.ipython.dedupe "true"
provider.copyq.type "command_json"
provider.copyq.name "CopyQ"
provider.copyq.command "~/.config/zellij/plugins/zellij-history-selector/scripts/export_copyq_json.py"
provider.copyq.args "clipboard"
provider.copyq.dedupe "true"The order in providers is the order used in the UI when switching between sources.
Profiles let one plugin config expose different provider subsets for different workflows.
Define providers once:
providers "shell,ipython,copyq,task_snippets"
provider.shell.type "file_lines"
provider.shell.path "~/.bash_history"
provider.ipython.type "ipython"
provider.ipython.path "~/.ipython/profile_default/history.sqlite"
provider.copyq.type "command_json"
provider.copyq.command "~/.config/zellij/plugins/zellij-history-selector/scripts/export_copyq_json.py"
provider.copyq.args "clipboard"
provider.task_snippets.type "command_json"
provider.task_snippets.command "~/.config/zellij/snippets/export_task_snippets.py"
profiles "default,task"
profile.default.providers "shell,ipython,copyq"
profile.task.providers "shell,ipython,task_snippets"Then choose the active profile at launch time. Keep one base plugin alias and override only profile from the keybind:
plugins {
zhs location="file:~/.config/zellij/plugins/zellij-history-selector.wasm" {
profile "default"
providers "shell,ipython,copyq,task_snippets"
profiles "default,task"
provider.shell.type "file_lines"
provider.shell.path "~/.bash_history"
provider.ipython.type "ipython"
provider.ipython.path "~/.ipython/profile_default/history.sqlite"
provider.copyq.type "command_json"
provider.copyq.command "~/.config/zellij/plugins/zellij-history-selector/scripts/export_copyq_json.py"
provider.copyq.args "clipboard"
provider.task_snippets.type "command_json"
provider.task_snippets.command "~/.config/zellij/snippets/export_task_snippets.py"
profile.default.providers "shell,ipython,copyq"
profile.task.providers "shell,ipython,task_snippets"
}
}Bind different keys to different profile selections:
keybinds {
shared_except "locked" {
bind "Alt r" {
LaunchOrFocusPlugin "zhs" {
floating true
move_to_focused_tab true
}
}
bind "Alt t" {
LaunchOrFocusPlugin "zhs" {
floating true
move_to_focused_tab true
profile "task"
}
}
}
}Use the simplest provider that matches your source:
file_linesBest for plain text files with one entry per line.ipythonConvenience preset for IPython history.sqlite_queryBest for local SQLite-backed history stores.command_linesBest when a command prints one logical entry per line.command_jsonBest when entries can be multiline or need structured preview.
For command_json, each output line must be a JSON object with:
- required:
text - optional:
preview - optional:
score_hint
Example:
{"text":"first line\nsecond line","preview":"full item\nwith details","score_hint":42}This recipe is intended for Bash and Zsh history files:
- Bash:
~/.bash_history - Zsh:
~/.zsh_history
providers "shell"
provider.shell.type "file_lines"
provider.shell.name "Shell"
provider.shell.path "~/.zsh_history"
provider.shell.reverse "true"
provider.shell.dedupe "true"
provider.shell.limit "5000"For Zsh with EXTENDED_HISTORY, the plugin strips the leading : <epoch>:<duration>; metadata automatically.
Other shells may use different history formats. If the file is not really one-command-per-line, use a custom exporter with command_lines or command_json instead of copying this recipe unchanged.
providers "ipython"
provider.ipython.type "ipython"
provider.ipython.name "IPython"
provider.ipython.path "~/.ipython/profile_default/history.sqlite"
provider.ipython.limit "5000"
provider.ipython.dedupe "true"providers "sqlite"
provider.sqlite.type "sqlite_query"
provider.sqlite.name "SQLite History"
provider.sqlite.path "/absolute/path/to/history.sqlite"
provider.sqlite.query "SELECT command, preview, created_at FROM command_history ORDER BY created_at DESC LIMIT 5000"
provider.sqlite.text_column "0"
provider.sqlite.preview_column "1"
provider.sqlite.timestamp_column "2"
provider.sqlite.limit "5000"
provider.sqlite.dedupe "true"CopyQ works best through command_json, so multiline clipboard items stay grouped and render correctly in preview.
The bundled helper applies exporter-side limits before data reaches the plugin runtime:
- defaults to exporting at most 500 items
- truncates oversized items to avoid plugin crashes from huge clipboard payloads
This matters because provider.limit is applied after command output reaches the plugin, so it does not protect against a few very large clipboard entries.
With helper script:
providers "copyq"
provider.copyq.type "command_json"
provider.copyq.name "CopyQ"
provider.copyq.command "~/.config/zellij/plugins/zellij-history-selector/scripts/export_copyq_json.py"
provider.copyq.args "clipboard"
provider.copyq.limit "5000"
provider.copyq.dedupe "true"Optional tighter helper limits:
provider.copyq.args "clipboard --max-items 200 --max-chars 8000"If you want a clipboard-only picker, set:
default_mode "copy"Directly through copyq eval:
providers "copyq"
provider.copyq.type "command_json"
provider.copyq.name "CopyQ Direct"
provider.copyq.command "copyq"
provider.copyq.args "eval -- \"tab('clipboard'); for (var i = size(); i > 0; --i) { var item = str(read(i - 1)); if (item.length) print(JSON.stringify({text: item, preview: item, score_hint: i}) + '\\n'); }\""
provider.copyq.limit "5000"
provider.copyq.dedupe "true"The direct copyq eval form is more compact, but the helper script is safer for real-world clipboard histories because it can cap exported rows and item size before the plugin sees the payload.
If a tool is easy to export, you usually do not need a built-in provider for it:
- use
file_linesif the tool already writes a line-based file - use
sqlite_queryif the data is already in SQLite - use
command_linesif a command can print one entry per line - use
command_jsonif entries are multiline or need structured preview
For tools that are hard to parse, write a small exporter script first. A practical location is:
~/.config/zellij/plugins/zellij-history-selector/scripts/
If you want repo-local demo providers and ready-to-use testing fixtures, see scripts/README.md.
