Fish Key Bindings and Command-Line Editing: Modes, Buffers, and Terminal Reality
Map Fish key sequences to editor actions with explicit modes, command-line buffer operations, repaint rules, terminal diagnostics, and repeatable integration tests.
Fish key bindings connect terminal input sequences to editor actions. A binding can invoke a built-in input function, run Fish commands that inspect or modify the command line, or transition between binding modes. Reliable customization depends on separating three layers: the key sequence the terminal actually sends, the mode in which Fish looks it up, and the editor operation that consumes it.
A key combination printed on a keyboard is not necessarily distinguishable by the terminal protocol. A binding can also work in one Fish mode and appear broken in another. Diagnose those boundaries independently before changing default mappings.
A key name is a terminal input sequence
Fish represents named keys such as up, escape, tab, and f1, and accepts modifier prefixes such as ctrl-, alt-, shift-, and super-. Comma-separated names represent sequences rather than simultaneous chords. For example, ctrl-x,ctrl-e means press one key sequence and then another. Use fish_key_reader in the terminal where the binding must work to find the representation actually received by Fish.
The reader is a diagnostic tool, not a way to make an incapable terminal send new information. Traditional terminal encodings can make combinations indistinguishable: ctrl-i and tab commonly share an encoding, and shift may not be detectable when ctrl is pressed. Escape is also used as a prefix for many sequences and historically overlaps with Alt combinations. Fish can request richer terminal key encodings, but support depends on the terminal and multiplexing layers. When two physical keys produce the same sequence, Fish cannot infer which physical key was pressed.
# Read the actual sequence from this terminal.
fish_key_reader
# List named keys known to the running Fish build.
bind --key-names
# List available input functions.
bind --function-names
Test in the actual chain: local terminal, SSH, tmux or screen, and any remote host. Each layer can change how input is represented. If fish_key_reader reports a different sequence than expected, bind the observed sequence or configure the terminal layer. Do not spend time tuning a Fish mapping for a sequence that never reaches the shell.
Bindings are mode-specific and layered
Each binding belongs to a Fish bind mode. Without an explicit mode, bind uses default mode, or the vi command mode when Fish vi bindings are active. In vi mode, insert and command behavior are separate. Use -M to select the mode in which a mapping is active, and -m when the binding should switch to another mode after it runs. The current mode is visible in fish_bind_mode.
function fish_user_key_bindings
# This example applies in the default editing mode.
bind ctrl-x,ctrl-r insert_review_marker
end
function insert_review_marker
commandline --insert 'REVIEW: '
commandline -f repaint
end
The special input function repaint asks Fish to redraw the editor after a binding script changes the command line. Finish custom command-line mutations with this function; otherwise the buffer may have changed while the visible display remains stale until a later redraw.
Custom bindings are normally user-level bindings. Fish also has preset bindings, and user bindings take precedence when Fish resolves a mapping. Avoid editing preset bindings for one local customization. To restore a preset mapping, erase the user binding in the relevant mode rather than trying to guess and reconstruct Fish’s built-in command.
# Inspect mappings, including bindings known to Fish.
bind ctrl-x,ctrl-r
# Remove a user override in the default mode.
bind --erase --mode default ctrl-x,ctrl-r
If a binding works in default mode but not vi insert mode, the issue is not necessarily the sequence. The mapping may exist only in a different mode. Inspect the active mode and bind the sequence explicitly where it should apply. When changing modes in a function, Fish documents setting fish_bind_mode; when defining a transition as part of a binding, use the binding’s mode option rather than relying on incidental editor state.
Use input functions for editor-native actions
Fish exposes special input functions for cursor movement, token deletion, history navigation, completion, kill-ring operations, repainting, and other editor actions. Prefer those primitives for common editor behavior. A mapping to backward-kill-token, for example, preserves Fish’s notion of a token; manually deleting a fixed number of characters would not.
# In default mode, remove the token before the cursor.
bind alt-backspace backward-kill-token
# Inspect the current buffer and token interactively.
bind ctrl-x,ctrl-b 'commandline; commandline -t'
The second example is diagnostic only: commandline with no update option prints the current buffer, while -t selects the current token. Avoid binding noisy output in a daily configuration. The important distinction is that commandline can read or update the editor buffer, whereas input functions are editor operations that Fish queues or executes in the line editor.
The commandline builtin can select the full buffer, current job, current process, token, selection, or search field. A job here is a pipeline and stops at logical operators and terminators; a process is one command and stops at pipes as well. Use the narrowest scope that matches the intended edit. Replacing the entire line when the feature only means to insert at the cursor can destroy text the user has already entered.
function insert_git_status
commandline --insert 'git status --short'
commandline -f repaint
end
bind ctrl-x,ctrl-g insert_git_status
This binding inserts a command into the editing buffer. It does not execute it. That separation is valuable for interactive tooling: users can inspect, edit, or cancel the result before running it. A key binding that performs a mutating action directly should have a clearly deliberate key and should not accidentally trigger because a prefix sequence was mistyped.
Make sequence timing an explicit UX choice
When one binding is a prefix of a longer sequence, Fish may wait to determine whether the user is entering the shorter mapping or continuing the longer one. fish_sequence_key_delay_ms controls how long Fish waits to disambiguate such sequences. A long delay makes chords easier to complete but makes the prefix feel sluggish; a short delay can cause the longer sequence to split when input arrives slowly.
Escape has a related ambiguity: Fish may wait after receiving escape to distinguish a standalone Escape from Alt-prefixed input or another escape sequence. fish_escape_delay_ms controls that delay. Lowering it can make vi-mode Escape feel faster but can make Alt combinations harder to recognize in a slow or remote terminal. Set these values only after reproducing the perceived lag in the target terminal path.
# A moderate example for disambiguating key sequences.
set -g fish_sequence_key_delay_ms 200
# Tune only if standalone Escape feels too slow in the target terminal.
set -g fish_escape_delay_ms 100
These are global Fish variables and affect interactive behavior. Measure the tradeoff instead of copying a number from another setup. A local terminal over a fast connection and a high-latency SSH session may need different choices.
Persist user bindings without fighting startup
Put personal bind statements in config.fish or define fish_user_key_bindings. Fish automatically executes that function for custom key bindings. Prefer one function as the binding entry point so that the configuration’s intended order is visible and repeatable. Avoid re-defining Fish’s entire preset binding set for one added shortcut.
function fish_user_key_bindings
bind alt-backspace backward-kill-token
bind ctrl-x,ctrl-g insert_git_status
end
If a binding depends on a helper function, ensure that helper is available before the binding is used. Fish autoloading can make ordinary function discovery convenient, but a missing or misspelled function name may only become apparent when the key is pressed. Keep custom functions in their normal autoload path or define them before registering dependent bindings.
When a mapping stops working after editing configuration, inspect the effective binding with bind and review the active mode. Remove duplicate definitions and check whether a later configuration snippet overwrote it. Since Fish’s system, vendor, and user configuration can all contribute behavior, a minimal test shell is useful for separating a configuration conflict from a terminal-sequence issue.
A practical troubleshooting sequence
- Run fish_key_reader and record what the terminal sends for the key.
- Confirm the active bind mode and inspect the binding in that mode.
- Check whether the requested command or special input function exists.
- If a function edits the line, confirm it ends with repaint.
- Test the same input in the terminal multiplexer or remote path where it fails.
- Adjust sequence or Escape delay only when timing is the demonstrated issue.
- Restart an interactive Fish process and verify the configuration registration path.
Use a harmless test function while debugging. A binding is invoked from the user’s editor context, so avoid using a destructive command as the first proof that a mapping is active. Test that insertion changes only the buffer, then test a command’s actual execution path separately.
Acceptance checks for a custom editor map
Before sharing a binding configuration, verify that its key sequence is distinguishable in the supported terminals, that it is registered in every intended mode, and that it does not shadow a required default without a deliberate reason. Confirm that command-line updates preserve the user’s existing text, repaint immediately, and do not execute commands unexpectedly. Check the result through local terminal, SSH, and any supported multiplexer configuration.
Document mappings with the exact sequence and mode rather than a visual key description alone. When support is limited by terminal encoding, state that limitation and provide a fallback. This turns keybinding bugs into a diagnosable interface contract rather than a collection of machine-specific guesses.
Related:
- How GNU Readline Turns Keystrokes into Shell Commands
- Bracketed Paste Mode: How Terminals and Shells Mark Pasted Text Safely
Sources: