fish 4.0 beta brings Rust and more explicit terminal input
The first fish 4.0 beta ships a Rust core, readable key-binding notation and richer terminal integration. The input changes expose distinctions that traditional terminal byte sequences could not represent.
A terminal application can distinguish only the input its terminal sends. If Tab and Control-I arrive as the same value, a shell cannot give them separate bindings by choosing a more descriptive configuration syntax. fish 4.0's first beta combines readable key names with requests for richer keyboard encodings, addressing both the configuration and the information available to interpret it.
The 4.0b1 release notes, dated 17 December, also announce the port of fish's core from C++ to Rust. The notes at the beta tag describe themselves as incomplete draft notes and ask users to report problems. This is a beta of the new implementation, with input behavior and packaging changes that deserve their own explanations.
I like fish's preference for a usable interactive environment with limited setup. The beta's terminal work interests me because it makes distinctions that people already understand, such as a key and its modifiers, more directly available to the shell. The Rust implementation is a separate change whose practical effects include how the program is built and distributed.
A binding can name a key
The bind manual shipped with 4.0b1 defines key arguments as comma-separated key names. Modifiers use prefixes such as ctrl-, alt- and shift-. Named keys include arrows, Enter and Backspace, so a binding can express the intended input without requiring the author to recognize its escape sequence.
For example, the following bindings use named keys and a two-key sequence.
bind pageup history-search-backward
bind ctrl-x,ctrl-e edit_command_buffer
The second argument names an editor input function or an ordinary fish command. In the sequence binding, the comma means Control-X followed by Control-E. It does not mean pressing both modified keys simultaneously. Modifier prefixes describe one key event, while the comma separates successive events.
This notation also changes an ambiguity in earlier spellings. The release notes explicitly say bind up now names the up-arrow key instead of a sequence consisting of the letters u and p. A string that used to be interpreted as raw key bytes can now have a named-key interpretation.
The beta retains a compatibility rule for much existing notation. Arguments beginning with an ASCII control character use the old interpretation. Short arguments of up to three characters can also retain it when they are not named keys and contain neither a comma nor a hyphen. The rule aims to preserve most existing bindings while allowing a more structured syntax.
That compatibility behavior is not a promise that every old invocation is unchanged. The release lists the binding rework among backward-incompatible changes and says rare cases may need adjustment. Configuration authors should interpret the new key-name grammar before assuming a short string still describes a sequence of printable characters.
Better names need better encodings
The bind manual explains the historical limit behind the change. Traditional control-key encoding makes some combinations identical to other keys. Control-I is Tab under that encoding, while Control-J is a newline. Control and Shift also cannot always be distinguished through the older representation.
fish now requests XTerm's modifyOtherKeys encoding and progressive enhancements from the kitty keyboard protocol. According to the beta notes, supported terminals can then supply additional modifier combinations and distinguish inputs such as Control-I and Tab. The terminal's capabilities determine whether that distinction is actually available.
This creates two independent requirements for a custom binding. The configuration must name the intended input, and the terminal must encode that input distinctly. Changing only the binding text cannot recover a distinction erased before the shell receives the event.
The documented diagnostic tool is fish_key_reader. It reports the input name that fish observes. If pressing Control-I still reports Tab, the manual says the terminal does not support the better encoding needed for separate treatment. This gives a direct way to inspect the active path instead of reasoning from a terminal application's feature list alone.
The beta also changes some default behaviors when the richer encodings are available. Shift-Enter can insert a newline without executing the command, and Control-Backspace deletes a word. These distinctions are conditional on terminal support, which is why their availability should not be described as identical across every terminal session.
I prefer this explicit dependency to a configuration that appears to promise combinations the terminal cannot represent. It gives a binding author a concrete question to answer by observing input. The readable notation then describes the event that observation actually established.
Argument editing respects shell structure
The release changes Alt-Left and Alt-Right to move by an argument, including arguments with quoted spaces. Alt-Backspace similarly deletes an argument, while Control-Backspace supplies word deletion. This gives the editor different operations for shell tokens and for smaller textual units.
Consider an argument containing a quoted directory name. Its spaces separate words for reading, but they do not separate arguments passed to the program. An argument-oriented movement function can treat that quoted value as one editing unit. A word-oriented function remains useful when changing part of its contents.
The beta introduces input functions including forward-token, backward-token, kill-token and backward-kill-token. The bind manual documents them alongside word and character operations. This is a visible editing change that happens to arrive with the implementation port, rather than a consequence that follows automatically from Rust.
There are also passive character movement functions. Unlike ordinary movement functions, they do not accept autosuggestions or change selection in the completion pager. A custom binding can therefore request movement without also requesting the contextual behavior attached to a more general editor action.
The change is useful when composing bindings because an input function's side effects matter. A binding intended to move a cursor can otherwise accept suggested text at the end of a line. Distinct functions make that choice part of the binding's definition.
Terminals receive structured information about output
The beta adds OSC 133 markers for prompt and command-output regions. The tagged release notes explain that supporting terminals can use these markers to navigate between prompts or display the last command's output in a pager. These markers supply the region boundaries instead of requiring the terminal to infer them from prompt appearance.
fish also reports the working directory through OSC 7 and sets the window title through OSC 0 without restricting those operations to a list of selected terminals. Cursor changes similarly apply to supporting terminals without a fixed terminal list. This favors capability through recognized sequences over special handling based only on terminal identity.
The release notes identify a compatibility consequence. Terminals that fail to ignore unknown OSC or CSI sequences can display unwanted text, with cool-retro-term and Emacs ansi-term named as affected examples. The beta therefore expands integration while relying on terminals to handle unsupported sequences correctly.
These markers concern the relationship between the shell and the terminal displaying it. They do not change the command's own output format or make arbitrary programs produce structured logs. Their practical benefit is that the terminal can know where command interaction regions begin and end.
The Rust port changes the distribution contract
The distributor section specifies Rust 1.70 or later for this beta. A C++ compiler is no longer required, but a C compiler remains necessary for glue code and tests. Describing the core as Rust does not imply that building every part needs only the Rust compiler.
CMake remains the recommended build system, with version 3.5 as the stated minimum. The notes attribute that recommendation to Cargo's limited support for installing supporting files. A package needs the shell's runtime data as well as its executable, so a successful compiler invocation is only part of installation.
The beta also offers an experimental self-installing binary build through Cargo. On its first interactive run, that binary extracts supporting data under ~/.local/share/fish/install/. This provides a way to copy a built binary between machines, while keeping the conventional CMake installation as the recommended packaging path.
fish no longer depends on the ncurses library but still uses a terminfo database. The distributor guidance asks packagers to depend on the package supplying that database instead of the curses library. Removing one implementation dependency does not remove the terminal descriptions that the shell still consumes.
The beta combines a new core implementation with deliberate changes at the terminal boundary. Its build instructions specify how to supply the runtime files, while the bind manual specifies how to observe and name input events. Those are concrete compatibility contracts for testing this prerelease on the terminals and package layouts where it will run.