# zsh-patina: syntax highlighting for the Zsh command line

> zsh-patina is a Zsh plugin that highlights what you type as you type it, using a small Rust daemon and the Sublime Text syntax definitions bundled with syntect. The daemon is shared between your shells, so a typical command is coloured in under a millisecond, and the plugin goes beyond colouring by marking commands that do not exist and underlining arguments that do point at real files.

**michel-kraemer/zsh-patina** — $ A blazingly fast Zsh syntax highlighter 🌈

- Repository: https://github.com/michel-kraemer/zsh-patina
- Stars: 526 · Forks: 19
- Language: Rust
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/michel-kraemer-zsh-patina

## A shared daemon that colours the prompt

zsh-patina is a Zsh plugin that performs syntax highlighting of your command line while you type, and the implementation is the interesting part. The plugin does not do the highlighting itself. It spawns a small background daemon written in Rust, and that daemon is shared between your Zsh sessions, caching the syntax definition and the colour theme so that they are loaded once rather than per shell.

The performance claim follows from that architecture. The README states that typical commands are highlighted in less than a millisecond, and that extremely long commands only take a few milliseconds. The distinction matters: a highlighter that re-parses a syntax definition on every keystroke is unusable in a shell, and one that keeps the definition resident is a background service that answers a question.

The highlighting quality comes from syntect, which provides syntax highlighting based on Sublime Text syntax definitions, so the result is close to what you would see in an IDE, and the project describes it that way. The default built-in theme uses the eight ANSI colours and is compatible with all terminal emulators, which is a deliberate constraint: a theme with true colour output looks better on some terminals and washes out on others, while the ANSI palette is defined everywhere.

The project is by Michel Kraämer, the licence is MIT, the crate version is 1.11.0 with the edition 2024 and a minimum Rust version of 1.89, and the release history is quick: 1.9.0 on 2026-07-18, 1.10.0 on 2026-08-23 and 1.11.0 on 2026-10-08, with the last push on 2026-10-18.

## Knowing whether a command exists before you run it

Beyond colouring, the plugin does something that most highlighter plugins do not: it checks whether what you typed makes sense.

It dynamically detects whether a command you entered is valid and can be executed, and whether the arguments refer to existing files or directories. By default, invalid commands are shown in red, and existing files and directories are underlined. So the prompt tells you that the command name is not on your PATH before you press enter, and it tells you which of your arguments point at something that is actually there.

The styling of both signals is configurable. Dynamic highlighting can be disabled in the configuration, and the colours can be changed in a custom theme, which is the mechanism for anyone who finds a red command alarming or who wants the underline to be a different colour.

The implementation behind the check is visible in the dependencies. The regex-syntax crate is there for the pattern matching, the shell expansion is handled by shellexpand, and the platform specific lookups for executables and paths come from the directories crate. The textwrapper crate with the terminal size feature is what makes it possible to handle an extremely long command line without the highlighting becoming meaningless, and rayon is used for parallel work, which fits the claim about long commands taking only a few milliseconds.

The trade-off of any such check is that it is a heuristic about the filesystem rather than a truth about it. A command installed after the cache was built, a script that only exists on one host in a cluster, and a function defined in your shell rather than on your PATH are all cases where a red or unhighlighted command can be wrong, and the configuration option to turn the feature off is there for exactly that reason.

## Seven ways to install, one line to activate

The installation matrix is unusually complete, and every path ends with the same activation step.

On macOS, Homebrew:

```shell
   brew install zsh-patina
```

For Rust developers, cargo:

```shell
   cargo install --locked zsh-patina
```

On Debian and Ubuntu there is a .deb package, installed with:

```shell
   sudo dpkg -i zsh-patina_1.11.0_amd64.deb
```

Arch users have the AUR through yay or paru, and Nix users add the package to their systemPackages:

```nix
   environment.systemPackages = [ pkgs.zsh-patina ];
```

On Windows, zsh-patina runs through MSYS2 or Cygwin, both of which provide a Unix-like terminal that can run Zsh, and the Scoop package manager installs it from the extras bucket. For everyone else there are pre-compiled archives for Linux, macOS and Windows, which you extract wherever you like and then point at with an absolute path.

Building from source needs Rust 1.89.0 or higher, and is three commands:

```shell
   git clone https://github.com/michel-kraemer/zsh-patina.git ~/.zsh-patina
   cd ~/.zsh-patina
   cargo build --release
```

The activation is the same idea everywhere. The binary has an activate subcommand that prints the shell code to evaluate, and you put that into your .zshrc at the end. For a Homebrew install that is:

```shell
   echo "eval \"\$($(brew --prefix)/bin/zsh-patina activate)\"" >> ~/.zshrc
```

and for a cargo install it is the same line with the cargo bin path, and for the pre-compiled binary it is the path you extracted to. After any of them, exec zsh starts a new shell that picks the plugin up.

The plugin managers are supported too. Antidote wants a line with michel-kraemer/zsh-patina in its plugins file, and there are sections for oh-my-zsh and Zinit. In each case the instruction is to install the binary or package first and ignore the manual .zshrc activation, because the plugin manager does that part.

## How the build is put together

The manifest shows a careful project, and a few entries explain how the plugin works.

syntect is at 5.3.0 with the parsing and regex-fancy features and default features off, and the build dependencies enable default-syntaxes and yaml-load instead, along with the regex-fancy feature. The split tells you where the work happens: the syntax definitions and the theme are loaded at build time through the build script, and the runtime crate only needs to parse and highlight. That is the difference between a plugin that starts in milliseconds and one that reads and parses a large syntax set every time you open a terminal.

The configuration is handled by figment with the toml feature, with the toml crate and serde for the data, and env_logger for the log output, using the humantime feature so that log timestamps are readable. The command line is parsed by clap with the derive and wrap_help features, and clap_complete provides the shell completions, which is the optional completions feature of the plugin.

The templates directory and the askama.toml file at the top level point to askama, the templating engine, which is used to generate the shell code that the activate subcommand prints, rather than assembling that string by hand. There is a themes/ directory for the built-in and custom themes, and assets/ for the other bundled resources.

Two build profiles are worth noting. The release-lto profile inherits release and sets lto to fat with codegen-units at 1, which is the standard way to get the smallest and fastest binary at the cost of build time, and the dev profile raises the optimisation level of the insta crate to 3, because snapshot testing is much slower in a debug build and the insta dependency is used for the snapshot tests.

The test dependencies are also telling. insta is the snapshot testing library, pretty_assertions improves failure output, tabwriter formats the table driven tests, and testcontainers with tokio provides containerised integration tests, which is how a project that spawns a daemon and a shell tests the two together.

## The repository layout and where to look first

The top level of the repository is small and tells you the shape of the project. There is src/ for the Rust code, templates/ for the askama templates that generate the shell side, themes/ for the colour themes, assets/ for bundled resources, docs/ for the documentation, tests/ for the test suite, and a build.rs that runs the syntect based generation at build time.

Two files at the root explain the integration with the wider Zsh ecosystem. zsh-patina.plugin.zsh is the oh-my-zsh plugin file, which is why the oh-my-zsh section of the README needs nothing more than the plugin name. askama.toml configures the template engine, so the generated shell code is edited through configuration rather than through Rust string literals.

There is a flake.nix, so the project is packaged for Nix users as well as through Nixpkgs, and a CHANGELOG.md and a CONTRIBUTING.md, and a LICENSE with the MIT terms, and a .github directory with the CI workflow that the badge points at.

For a reader deciding whether to try it, the order is sensible: the table of contents points to installation, configuration, theming, the optional completions, benchmarks, troubleshooting and removal. The benchmarks section is the one to read if you are comparing against another highlighter, and the troubleshooting section is the one to read if the colours are wrong, since a theme that assumes true colour output on a terminal that only does the ANSI palette is the most likely cause of a disappointing first run.

## Conclusion

zsh-patina fits a Zsh user who spends the day in a terminal and wants the command line to look like the editor, especially on a machine where the editor is slow to start, since the daemon is shared between shells and a typical command is highlighted in under a millisecond. It is the wrong tool if you use Bash, because the plugin is Zsh specific, and it is the wrong tool if you cannot install a compiled binary, since every install path except a plugin manager that fetches prebuilt files needs Rust or a package of your distribution. Before you install it, read the theme section, because the default theme deliberately uses the eight ANSI colours so it works in any terminal, and check the configuration reference for the dynamic highlighting, since the red invalid command styling is the feature most likely to surprise someone used to a quieter prompt.

## FAQ

### What does zsh-patina do in my terminal?

It is a Zsh plugin that highlights the command line while you type, using the Sublime Text syntax definitions provided by syntect. It also detects whether a command is valid and whether arguments refer to existing files or directories, showing invalid commands in red and underlining existing paths, and both behaviours can be turned off in the configuration.

### How do I install zsh-patina and turn it on?

Install the binary with your package manager, for example brew install zsh-patina, cargo install --locked zsh-patina, or the .deb, AUR, Nixpkgs or Scoop packages, and then add the activation line to your .zshrc, which evaluates the output of the zsh-patina activate subcommand. Restarting the shell with exec zsh then loads the plugin. Build from source needs Rust 1.89 or newer and cargo build --release.

### How fast is zsh-patina at highlighting?

The README states that typical commands are highlighted in less than a millisecond, and that extremely long commands take only a few milliseconds. The speed comes from the architecture, since the plugin spawns a small background daemon in Rust that is shared between Zsh sessions and caches the syntax definition and the colour theme.

### Does zsh-patina work on Windows and with Zsh plugin managers?

On Windows it runs through MSYS2 or Cygwin, and the Scoop package manager installs it from the extras bucket. The plugin managers are supported as well, with sections for Antidote, oh-my-zsh and Zinit, where you install the binary first and let the plugin manager handle the activation.

## Sources

- [Issues](https://github.com/michel-kraemer/zsh-patina/issues)
- [License: MIT](https://github.com/michel-kraemer/zsh-patina/blob/main/LICENSE)
- [michel-kraemer/zsh-patina on GitHub](https://github.com/michel-kraemer/zsh-patina)
- [README](https://github.com/michel-kraemer/zsh-patina/blob/main/README.md)
- [Releases](https://github.com/michel-kraemer/zsh-patina/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/michel-kraemer-zsh-patina
