February 27, 2025·6 min read·engineeringfishrustterminalsbindings

fish 4.0 completes the transition to a Rust core

fish 4.0 releases the Rust port while retaining the shell's language. New key notation and terminal protocols make interactive behavior more explicit.

Rewriting an interactive shell requires preserving behavior that is difficult to describe entirely through a language grammar. A working shell also includes terminal interaction and the small editing decisions users learn through repetition. fish 4.0, released on February 27, brings the project's Rust core to a stable release. The implementation language changes, while users continue writing fish syntax.

I like fish for the interactive experience it provides without extensive configuration. Its deliberate departure from POSIX syntax is a tradeoff I accept for a personal shell. The Rust transition matters to that experience through maintainability and future implementation work, but it should not be confused with a requirement to rewrite shell functions in Rust.

The proposal addressed development constraints

The original Rust proposal put a change of implementation language before the project. That is a different decision from redesigning the shell language. A port can preserve the observable behavior while changing the means available to maintainers for expressing ownership and concurrency inside the program.

For a shell, that distinction is particularly important. An interactive command line is a program interface that users build habits around. A new implementation can be internally cleaner while still being a regression if it changes how an existing script or editing command behaves unintentionally. Preserving behavior requires evidence at the interface, alongside successful compilation of the replacement implementation.

The proposal's scale also explains why the stable release deserves attention even from users who do not build fish. A language change affects the project's development and distribution process. A user receiving an ordinary package may see little of that process directly, while depending on its ability to preserve the existing shell.

The port kept a working program throughout

The maintainers' December account of the port describes moving components individually while keeping fish runnable. They retained much of the existing code structure so they could compare the old and new implementations. End-to-end tests that execute scripts or simulate terminal interactions remained usable during the transition.

That approach gave the project feedback before the final C++ component disappeared. The account also describes temporary interoperability through generated C++ and Rust bindings. The bridge enabled incremental work, but introduced its own complexity and copying costs. Those temporary costs were part of the migration mechanism, not evidence about the final port's steady-state performance.

The maintainers identify Rust's Send and Sync as useful constraints for future threading work. Their account does not claim that the port itself delivers fully parallel execution of fish functions and builtins. It describes that as work the implementation should make more tractable. This separates a present maintenance benefit from a proposed user-visible capability.

Key names become readable configuration

The binding documentation in the release describes human-readable key notation alongside the existing byte-oriented form. Names such as up and modifier combinations such as ctrl-x express the intended key directly. A comma separates keys in a sequence, which distinguishes a sequence from a combination of modifiers applied to one key.

For example, a binding can insert text with a named modifier combination. This changes the current command line without executing the inserted command.

bind ctrl-g 'commandline -i "git status"' repaint

That representation helps review custom bindings. A reader can identify the intended physical interaction without decoding a control character. It also makes a naming collision visible. A short word that previously meant a sequence of character bytes can now be a named key, so compatibility depends on the parsing rules described by the release.

The change is consequently more than a cosmetic pretty-printer. The notation determines which event a binding requests. Existing byte sequences remain useful where they are unambiguous, while the named form makes the intention explicit when a key has a recognized name.

A terminal must encode distinct keys distinctly

Readable configuration cannot create information the terminal never sends. The kitty keyboard protocol describes how richer keyboard encoding distinguishes events that traditional terminal input can represent ambiguously. The protocol provides progressive enhancements so applications can request more detailed events instead of assuming that every terminal sends them by default.

fish 4.0 requests supported enhanced keyboard behavior, including the ability to distinguish some modified keys. That means the terminal and shell cooperate on what a binding can recognize. A named binding can be valid configuration while a particular terminal still cannot produce the distinct input event needed to trigger it separately.

This relationship explains why terminal support belongs in the description of a key-binding change. The shell is one participant in an input protocol. A multiplexer or terminal implementation can affect which encoded events reach it. Diagnosing a binding therefore starts with the event that arrives, not only with the text used to declare the binding.

Abbreviations can have a command-specific scope

The abbreviation reference in 4.0 documents restricting an abbreviation to a command. That supplies useful context for a shortcut whose meaning is appropriate as an argument to one tool but surprising elsewhere. The expansion remains visible on the command line before execution.

abbr --add --command git st status

Here, the shortcut is associated with Git command input. This is an editing convenience, not a separate executable that changes Git itself. The scope expresses where fish should offer the expansion, and the resulting text remains available for inspection or editing.

I find that a good fit for fish's interactive design. A short form can reduce repeated typing while still producing an ordinary command that can be read before it runs. Restricting it to a specific command avoids assigning the same token a special meaning throughout unrelated shell input.

History can be edited independently of execution

The released history reference includes the new history append operation. It adds a command to history without executing it. That creates a clear distinction between making text available for later recall and requesting the external effect described by that text.

history append 'git status --short'

This is useful when a command is intended as future input rather than something to run immediately. History is part of the interactive environment, and populating it need not imply that the shell performed the recorded operation. Consumers of history should likewise avoid treating every entry as proof of a completed command.

The same documentation describes explicit history merging between sessions. History therefore has its own storage and session behavior, separate from the current command buffer. That distinction helps explain why editing, remembering and executing a command are separate shell operations even when the interactive interface connects them closely.

Distribution changes with the implementation

The 4.0 build documentation lists Rust as a build dependency and retains CMake as the normal packaging route. It also describes an experimental self-installing build that embeds supporting data and extracts it on interactive startup. This gives the Rust port a visible distribution consequence without requiring ordinary package users to become Rust developers.

The self-installing form includes more than the executable's instruction code. fish also ships functions and other supporting files, and the documentation explains where the embedded files are extracted. Moving a binary between machines therefore depends on the build form, rather than an assumption that every packaged executable contains everything needed for the same installation behavior.

For a packaged installation, the distributor still owns integration with the target system's paths and dependencies. For a self-installing build, the executable manages extraction of its supporting data. These approaches solve different distribution needs, and the release describes the latter as experimental rather than replacing the established packaging process wholesale.

Stable behavior remains the user-facing result

The version-pinned changelog records changes beyond the Rust port, including the default behavior of ctrl-c and the default removal of question-mark globbing. These need to be understood as explicit shell behavior changes. They should not be attributed vaguely to Rust, since a choice of implementation language does not determine either interaction.

This distinction also makes an upgrade easier to assess. The port explains why the development and build process changes. Behavioral notes explain what an existing configuration or editing habit may need to accommodate. Treating those as separate evidence gives the release a more useful account than assuming that every observed difference is a necessary consequence of rewriting the core.

My interest in fish remains centered on the commands and editing actions used at the terminal. Version 4.0 preserves that focus while changing the implementation available to maintainers. Its named bindings and scoped abbreviations make interactive intent easier to express, and the stable Rust core provides the implementation on which that behavior now runs.