---
title: ⚙️ Feature-rich syntax highlighting
description: Feature-rich Syntax Highlighting for Zsh
canonical_url: https://wiki.zshell.dev/ecosystem/plugins/f-sy-h
markdown_url: https://wiki.zshell.dev/ecosystem/plugins/f-sy-h/index.md
locale: en
source_path: ecosystem/plugins/f-sy-h.mdx
---

<!-- Generated from the canonical Z-Shell Wiki build. The linked human-facing page remains the editorial source of truth. -->

# ⚙️ Feature-rich syntax highlighting

<a id="i-classfa-brands-fa-githubi-z-shellf-sy-h"></a>

## [z-shell/f-sy-h](https://github.com/z-shell/F-Sy-H)

<a id="install-f-sy-h"></a>

## Install F-Sy-H

-   Zi
-   Zgen
-   Oh-My-Zsh
-   Standalone

Add the following to your `.zshrc` file.

```zi
zi light z-shell/F-Sy-H
```

Load the plugin in [turbo mode](https://wiki.zshell.dev/docs/getting_started/overview/index.md#turbo-mode-zsh--53):

~/.zshrc

```zi
zi wait lucid for \
  atinit"ZI[COMPINIT_OPTS]=-C; zicompinit; zicdreplay" \
    z-shell/F-Sy-H \
  blockf \
    zsh-users/zsh-completions \
  atload"!_zsh_autosuggest_start" \
    zsh-users/zsh-autosuggestions
```

Add the following to your `.zshrc` file in the same place you're doing your other `zgen load` calls in.

```zsh
zgen load z-shell/F-Sy-H
```

Clone the Repository:

```sh
git clone https://github.com/z-shell/F-Sy-H.git \
  ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/F-Sy-H
```

And add `F-Sy-H` to your plugin list.

Clone the Repository.

```sh
git clone https://github.com/z-shell/F-Sy-H.git \
  ~/some/path/to/fsh
```

And add the following to your `.zshrc` file.

```zsh
source ~/some/path/to/fsh/F-Sy-H.plugin.zsh
```

<a id="compatibility-and-loading"></a>

## Compatibility and loading

F-Sy-H uses the portable project identifier `fsh` and supports Zsh 5.8 or newer. Its authoritative entrypoint is `F-Sy-H.plugin.zsh`. Direct sourcing and plugin-manager loading expose the public `fsh_theme` and `fsh_plugin_unload` functions. The `functions/`, `completions/`, and private `chroma/` directories are added to `fpath` when absent.

Interactive loading wraps ZLE widgets, installs a `preexec` hook, and loads the required Zsh modules. Non-interactive loading defines the shell API without changing widgets or installing the hook. The plugin has no public aliases or public parameters.

Loading performs no network request and does not create the theme work directory. The explicit `fsh_theme` command creates storage only when it persists theme state. Saved `current_theme.ini`, `theme_overlay.ini`, and `secondary_theme.local.ini` files are parsed as data; legacy executable `*.zsh` theme caches are ignored.

`fsh_plugin_unload` removes plugin-owned hooks, widgets, functions, parameters, modules, and `fpath` entries. State changed by another component after loading is preserved.

<a id="performance"></a>

## Performance

Performance differences can be observed in this Asciinema recording, where a `10 kB` function is being edited.

<a id="syntax-highlighting-features"></a>

## Syntax highlighting features

<a id="themes"></a>

### Themes

List the shipped themes and their declared backgrounds:

```zsh
fsh_theme --list
```

Apply a theme and place the sample snippet above on the command line:

```zsh
fsh_theme --test clean
```

Apply a theme without the sample:

```zsh
fsh_theme clean
```

<a id="theme-guide-for-f-sy-h"></a>

#### Theme guide for F-Sy-H

Themes are declarative [INI files](https://github.com/z-shell/F-Sy-H/tree/main/themes). Start from a shipped theme so required styles and current fallbacks are retained:

```zsh
theme_dir=${XDG_CONFIG_HOME:-$HOME/.config}/f-sy-h
mkdir -p -- "$theme_dir"
fsh_theme --copy-shipped-theme default "$theme_dir/custom"
# Edit custom.ini, then apply it.
fsh_theme "$theme_dir/custom.ini"
```

The shipped set is discovered at runtime, so use `fsh_theme --list` instead of relying on a hardcoded count.

<a id="rendering-contract"></a>

##### Rendering contract

Shipped themes declare their rendering assumptions:

```ini
[theme]
palette = xterm-256
foreground = #ffffff
background = #000000
```

`palette` is either `xterm-256`, with exact `#rrggbb` foreground and background values, or `terminal-ansi16`, with both values set to `default`. External themes may omit the complete `[theme]` section for compatibility, but a partial metadata declaration is invalid.

A style value is a comma-separated combination of:

<table><thead><tr><th>Form</th><th>Accepted values</th></tr></thead><tbody><tr><td>Named</td><td><code>red</code>, <code>green</code>, <code>blue</code>, <code>yellow</code>, <code>cyan</code>, <code>magenta</code>, <code>black</code>, <code>white</code>, or <code>default</code></td></tr><tr><td>Indexed</td><td><code>0</code> through <code>255</code></td></tr><tr><td>Truecolor</td><td>Six-digit <code>#rrggbb</code></td></tr><tr><td>Background</td><td><code>bg:</code> before any named, indexed, or truecolor value</td></tr><tr><td>Attribute</td><td><code>bold</code>, <code>blink</code>, <code>conceal</code>, <code>reverse</code>, <code>standout</code>, <code>underline</code>, each <code>no-</code> form, or <code>none</code></td></tr></tbody></table>

For example, `#ff0055,bold` sets a truecolor foreground and `bg:17,underline` combines an indexed background with an attribute.

<a id="styles-and-fallbacks"></a>

##### Styles and fallbacks

Theme sections group styles by where they apply:

<table><thead><tr><th>Section</th><th>Style group</th></tr></thead><tbody><tr><td><code>[base]</code></td><td>General tokens, separators, diagnostics, and recursive input</td></tr><tr><td><code>[command-point]</code></td><td>Commands, aliases, functions, keywords, and shell syntax</td></tr><tr><td><code>[paths]</code></td><td>Paths, directories, separators, and globbing</td></tr><tr><td><code>[brackets]</code></td><td>Paired brackets and nesting levels</td></tr><tr><td><code>[arguments]</code></td><td>Options and quoted arguments</td></tr><tr><td><code>[in-string]</code></td><td>Escapes and expansions inside strings</td></tr><tr><td><code>[other]</code></td><td>Variables, assignments, and history expansion</td></tr><tr><td><code>[math]</code></td><td>Arithmetic variables, numbers, and errors</td></tr><tr><td><code>[for-loop]</code></td><td>Loop variables, numbers, operators, and separators</td></tr><tr><td><code>[case]</code></td><td>Case inputs, parentheses, and conditions</td></tr></tbody></table>

The exhaustive style order and each style's fallback chain live in the [canonical theme schema](https://github.com/z-shell/F-Sy-H/blob/main/lib/theme-schema.zsh). A missing style follows that declared chain and then `default`. Use [the default shipped theme](https://github.com/z-shell/F-Sy-H/blob/main/themes/default.ini) as the complete authoring template instead of copying a style table from documentation.

From an F-Sy-H checkout, the [theme validator](https://github.com/z-shell/F-Sy-H/blob/main/tools/validate-themes.zsh) checks value syntax, required styles, metadata, contrast, semantic distinguishability, and colour-vision-deficiency separation:

```zsh
zsh -f tools/validate-themes.zsh /path/to/theme.ini
```

The validator's JSON Lines record includes a `nearcolor256` object. It maps each distinct truecolor style literal to the xterm-256 index selected by the installed [`zsh/nearcolor` module](https://zsh.sourceforge.io/Doc/Release/Zsh-Modules.html#The-zsh_002fnearcolor-Module), or is empty when the theme has no truecolor styles. F-Sy-H attempts to load this optional module when the terminal does not advertise truecolor support. The approximation excludes terminal-defined indices 0 through 15 because their colours are not predictable, and automatic truecolor detection remains terminal-dependent.

<a id="overlays-and-paths"></a>

##### Overlays and paths

An overlay uses the same format but declares only the styles it replaces. A theme path whose base name contains `overlay` is treated as a partial overlay.

Example overlay file:

```ini
; overlay.ini
[base]
commandseparator = yellow,bold
comment          = 17

[command-point]
function       = green
command        = 180
```

Theme paths accept these shorthands:

<table><thead><tr><th>Shorthand</th><th>Location</th></tr></thead><tbody><tr><td><code>CONFIG:</code></td><td><code>${XDG_CONFIG_HOME:-$HOME/.config}/f-sy-h/</code></td></tr><tr><td><code>CACHE:</code></td><td><code>${XDG_CACHE_HOME:-$HOME/.cache}/f-sy-h/</code></td></tr><tr><td><code>LOCAL:</code></td><td><code>/usr/local/share/f-sy-h/</code></td></tr><tr><td><code>HOME:</code></td><td><code>$HOME/.f-sy-h/</code></td></tr><tr><td><code>OPT:</code></td><td><code>/opt/local/share/f-sy-h/</code></td></tr></tbody></table>

For example, use `fsh_theme CONFIG:overlay` to load `${XDG_CONFIG_HOME:-$HOME/.config}/f-sy-h/overlay.ini` as an overlay. The `.ini` extension is optional.

<a id="secondary-theme"></a>

#### Secondary theme

The optional `secondary` key names a theme used for recursively highlighted `eval` arguments and command substitutions. For example, the default theme uses `free`:

```ini
; default.ini
[base]
default          = none
unknown-token    = 210,bold
; ...
secondary        = free
```

Recursive highlighting can use the alternate palette to distinguish embedded code:

![Syntax highlighting command substitution](https://wiki.zshell.dev/img/plugins/fsh/cmdsubst.png)

In the above screen-shot the interior of `$( ... )` uses different colors than the rest of the code. Example for `eval`:

![Syntax highlighting eval](https://wiki.zshell.dev/img/plugins/fsh/eval_cmp.png)

The first line highlights the `eval` argument as a regular string. The second line uses recursive highlighting with the configured secondary theme.

<a id="variables"></a>

### Variables

Comparing to the project `zsh-users/zsh-syntax-highlighting` (the upper image):

![Syntax highlighting parameter](https://wiki.zshell.dev/img/plugins/fsh/parameter.png) ![Syntax highlighting in string](https://wiki.zshell.dev/img/plugins/fsh/in_string.png)

<a id="brackets"></a>

### Brackets

![Syntax highlighting brackets](https://wiki.zshell.dev/img/plugins/fsh/brackets.gif)

<a id="conditions"></a>

### Conditions

Comparing to the project `zsh-users/zsh-syntax-highlighting` (the upper line):

![Syntax highlighting conditions](https://wiki.zshell.dev/img/plugins/fsh/cplx_cond.png)

<a id="strings"></a>

### Strings

Exact highlighting that recognizes quoting.

![Syntax highlighting strings](https://wiki.zshell.dev/img/plugins/fsh/ideal-string.png)

<a id="here-strings"></a>

#### here-strings

![Syntax highlighting here-strings](https://wiki.zshell.dev/img/plugins/fsh/herestring.png)

<a id="exec-descriptor-variables"></a>

### `exec` descriptor-variables

Comparing to the project `zsh-users/zsh-syntax-highlighting` (the upper line):

![Syntax highlighting exec](https://wiki.zshell.dev/img/plugins/fsh/execfd_cmp.png)

<a id="the-for-loops-and-alternate-syntax-brace--blocks"></a>

### The for-loops and alternate syntax (brace `{`/`}` blocks)

![Syntax highlighting loops](https://wiki.zshell.dev/img/plugins/fsh/for-loop-cmp.png)

<a id="function-definitions"></a>

### Function definitions

Comparing to the project `zsh-users/zsh-syntax-highlighting` (the upper 2 lines):

![Syntax highlighting function](https://wiki.zshell.dev/img/plugins/fsh/function.png)

<a id="recursive-eval-and---highlighting"></a>

### Recursive `eval` and `$( )` highlighting

Comparing to the project `zsh-users/zsh-syntax-highlighting` (the upper line):

![Syntax highlighting eval](https://wiki.zshell.dev/img/plugins/fsh/eval_cmp.png)

<a id="chroma-functions"></a>

## Chroma functions

<a id="command-specific-highlighting"></a>

### Command specific highlighting

<a id="autoload"></a>

#### Autoload

![Syntax highlighting autoload](https://wiki.zshell.dev/img/cast/gif/fsh/fsh-autoload.gif)

<a id="awk"></a>

#### Awk

![Syntax highlighting awk](https://wiki.zshell.dev/img/cast/gif/fsh/fsh-awk.gif)

<a id="docker"></a>

#### Docker

![Syntax highlighting docker](https://wiki.zshell.dev/img/cast/gif/fsh/fsh-docker.gif)

<a id="git-commit"></a>

#### Git commit

![Syntax highlighting git commit](https://wiki.zshell.dev/img/cast/gif/fsh/fsh-git-commit.gif)

<a id="git-checkout"></a>

#### Git checkout

![Syntax highlighting git checkout](https://wiki.zshell.dev/img/cast/gif/fsh/fsh-git-checkout.gif)

<a id="grep"></a>

#### Grep

![Syntax highlighting grep](https://wiki.zshell.dev/img/cast/gif/fsh/fsh-grep.gif)

<a id="make"></a>

#### Make

![Syntax highlighting make](https://wiki.zshell.dev/img/cast/gif/fsh/fsh-make.gif)

<a id="perl"></a>

#### Perl

![Syntax highlighting perl](https://wiki.zshell.dev/img/cast/gif/fsh/fsh-perl.gif)

<a id="sh"></a>

#### Sh

![Syntax highlighting sh -c](https://wiki.zshell.dev/img/cast/gif/fsh/fsh-sh-c.gif)

The [Chroma registry](https://github.com/z-shell/F-Sy-H/blob/main/lib/highlight.zsh) maps command names to their handlers.

Inspect that mapping with `fsh_chroma list`:

```text
COMMAND  TARGET                   KIND       STATUS
docker   _fsh_chroma_docker       dedicated  ready
npm      _fsh_chroma_subcommand   generic    ready
```

`dedicated` means the command has a command-specific handler or parser. `generic` means it uses the shared `_fsh_chroma_subcommand` fallback, which highlights the first non-option word as a subcommand but does not validate it or parse command-specific options. Registry totals therefore include lightweight generic entries and must not be read as a count of dedicated command coverage. `STATUS` reports whether each target is `ready`, `missing`, or intentionally `disabled`.

<a id="fpath-highlighting"></a>

### Fpath highlighting

![Syntax highlighting fpath](https://wiki.zshell.dev/img/cast/gif/fsh/fsh-fpath.gif)

<a id="case-highlighting"></a>

### Case highlighting

![Syntax highlighting case](https://wiki.zshell.dev/img/cast/gif/fsh/fsh-case.gif)

<a id="math-highlighting"></a>

### Math highlighting

![Syntax highlighting math](https://wiki.zshell.dev/img/plugins/fsh/math.gif)

<a id="zcalc-highlighting"></a>

### Zcalc highlighting

![Syntax highlighting zcalc](https://wiki.zshell.dev/img/plugins/fsh/zcalc.png)

<a id="custom-working-directory"></a>

### Custom Working Directory

Set the theme work directory before loading the plugin:

```zsh
zstyle ':fsh:config' work-dir "${XDG_CACHE_HOME:-$HOME/.cache}/f-sy-h"
```

The default is `${XDG_CACHE_HOME:-$HOME/.cache}/f-sy-h`. `fsh_theme` stores parsed INI state there only when an explicit theme command needs to persist a main theme, overlay, or local secondary theme.

<a id="chroma-guide-for-f-sy-h"></a>

### Chroma guide for F-Sy-H

This guide explains how to create detailed highlighting for a **specific program**.

<a id="keywords"></a>

#### Keywords

-   `chroma` - a shorthand for `chroma function` – the thing that colorizes selected commands, like `git`, `grep`, etc. invocations, see `chroma function` below,
-   `big loop` - main highlighting code, a loop over tokens, and at least 2 large structure constructs (big `if` and `case`); it is advanced, e.g. parses `case` statements, here-string, it constitutes 90% of the F-Sy-H project,
-   `chroma function` - a plugin function that is called when a specific command occurs (e.g. when a user enters `git` at the command line) suppressing activity of `big loop` (i.e. no standard highlighting unless requested),
-   `token` - the result of splitting the whole command line (i.e. `$BUFFER`, the Zle variable) into bits called tokens, which are words in general, separated by spaces on the command line.

<a id="overview-of-functioning"></a>

#### Overview of functioning

1.  Big loop is working – token by token processes command line, changes states (e.g. enters state "inside case statement") and in the end decides on the color of the token currently processed.
2.  Big loop occurs a command that has a chroma, e.g. `git`.
3.  Big loop enters "chroma" state, calls associated chroma function.
4.  Chroma takes care of the "chroma" state, and ensures it will be set also for the next token.
5.  "chroma" state is active, so all following tokens are routed to the chroma (in general skipping big-loop, see next items),
6.  When processing of a single token is complete, the associated chroma returns 0 (shell-truth) to request no further processing by the big loop.
7.  It can also return 1 so that a single, current token will be passed into a big-loop for processing (to do a standard highlighting).

<a id="chroma-function-arguments"></a>

#### Chroma function arguments

-   `$1` - 0 or 1, denoting if it's the first call to the chroma, or the following one,
-   `$2` - the current token, also accessible by `$\__arg` from the upper scope - basically a private copy of `$__arg`; the token can be eg.: "grep",
-   `$3` - a private copy of `$_start_pos`, i.e. the position of the token in the command line buffer, used to add region\_highlight entry (see man) because Zsh colorizes by \_ranges\* applied onto command line buffer (e.g. `from-10 to-13 fg=red`),
-   `$4` - a private copy of `$_end_pos` from the upper scope; denotes where the current token ends (at which index in the string is the command line).

So example invocation could look like this:

```zsh
_fsh_chroma_example 1 "grep" "$_start_pos" "$_end_pos"
```

Big-loop will be doing such calls for the user, after occurring a specific chroma-enabled command (like e.g. `awk`), and then until chroma will detect the end of this chroma-enabled command (end of the whole invocation, with arguments, etc.; in other words, when e.g. new line or `;`\-character occurs, etc.).

<a id="example-of-chroma-function"></a>

#### Example of chroma function

Use the shipped [`_fsh_chroma_example`](https://github.com/z-shell/F-Sy-H/blob/main/chroma/_fsh_chroma_example) as the current template. It contains the supported function arguments, state handling, return statuses, and highlighting update sequence in one source file.

<a id="declarative-chroma-definitions"></a>

#### Declarative Chroma definitions

Git and Zi use declarative associative arrays instead of handwritten token loops. Their definitions live in [`chroma/_fsh_chroma_git`](https://github.com/z-shell/F-Sy-H/blob/main/chroma/_fsh_chroma_git) and [`chroma/_fsh_chroma_zi`](https://github.com/z-shell/F-Sy-H/blob/main/chroma/_fsh_chroma_zi). The shared [`_fsh_chroma_main` parser](https://github.com/z-shell/F-Sy-H/blob/main/chroma/_fsh_chroma_main) reads an array named `_fsh_chroma_<name>_def`, and the registry selects it with `_fsh_chroma_main%<name>`.

The format is maintainer-facing Zsh source. Definition files are trusted code because validation sources them to obtain the associative array and its handler functions.

<a id="chroma-definition-top-level-entries"></a>

##### Top-level entries

<table><thead><tr><th>Entry</th><th>Meaning</th></tr></thead><tbody><tr><td><code>subcommands</code></td><td>A Zsh pattern, or <code>::function</code> returning the patterns in <code>reply</code></td></tr><tr><td><code>subcmd:NULL</code></td><td>Nodes active before a recognized subcommand</td></tr><tr><td><code>subcmd:&lt;selector&gt;</code></td><td><code>//</code>-separated nodes active for one subcommand or a parenthesized <code>|</code> pattern</td></tr><tr><td><code>subcmd:*</code></td><td>Catch-all nodes for recognized subcommands without a more specific entry</td></tr><tr><td><code>subcmd-hook</code></td><td>A function called after a subcommand is recognized</td></tr><tr><td><code>subcommands-blacklist</code></td><td>A comma-separated list of subcommands that return to the normal highlighter</td></tr><tr><td><code>&lt;LABEL&gt;_&lt;POSITION&gt;_...</code></td><td>A node describing an option or positional argument</td></tr></tbody></table>

Each referenced node must be a key in the same definition array.

<a id="chroma-definition-node-names"></a>

##### Node names and positions

A node name has this form:

```text
LABEL_POSITION_KIND[MODIFIER]
```

-   `POSITION` is a decimal argument position or `#` for any position. A recognized subcommand is not counted.
-   `KIND` is `opt` for a token beginning with `-`, or `arg` for a positional token.
-   `0_opt` handles options before the first positional argument. Later options can use their current numeric position or `#`.
-   A trailing `*` lets matching continue to the next candidate node. Without it, the first matching node wins.
-   A trailing `^` marks an option node that can add or delete active nodes.

For example, `URL_1_arg` handles the first argument after a subcommand, while `FILE_#_arg` can handle any positional argument.

<a id="chroma-definition-records"></a>

##### Records and delimiters

Route entries list nodes with `//`:

```zsh
subcmd:clone "CLONE_0_opt // URL_1_arg // DIRECTORY_2_arg // NO_MATCH_#_opt"
```

An option node contains one or more selector clauses separated by `||`. `<<>>` separates the selector from the action for the option and, when present, a second action for its value:

```zsh
CLONE_0_opt "
  (--depth=|-b|--branch=)
    <<>> NO-OP // ::_fsh_chroma_option_action
    <<>> __style=\${_fsh_theme_name}optarg-string // NO-OP
  || (--quiet|-q)
    <<>> NO-OP // ::_fsh_chroma_option_action"
```

An argument node contains one action record. An optional Zsh pattern before `:::::` limits the record to an expected value:

```zsh
URL_1_arg "NO-OP // ::_fsh_chroma_verify_url"
MODE_2_arg "(fast|safe) ::::: __style=\${_fsh_theme_name}subcommand // NO-OP"
```

Every action record has `ACTION // HANDLER`. The supported action vocabulary is `NO-OP` or `__style=<style-name>`. A handler is `NO-OP` or `::function`; the function receives the subcommand, start, end, and token values. Referenced handlers must be defined by the parser or the definition file.

Directive clauses change the active node list. For option selectors, append `:add` or `:del` to the selector:

```zsh
"MODE_0_opt^" "
  --safe <<>> NO-OP // ::_fsh_chroma_option_action
  || --safe:add <<>> SAFE_FILE_1_arg // NO_MATCH_#_arg
  || --safe:del <<>> DEFAULT_FILE_1_arg"
```

Argument records place the operation after another `<<>>`:

```zsh
COMMAND_1_arg "run ::::: __style=\${_fsh_theme_name}subcommand // NO-OP <<>>
  add:RUN_TARGET_2_arg // NO_MATCH_#_arg"
```

<a id="validate-chroma-definitions"></a>

##### Validate definitions

From an F-Sy-H checkout, validate every declarative definition:

```zsh
zsh -f tools/validate-chromas.zsh
```

The validator rejects malformed records, unsupported actions, undefined handlers, and references to undefined nodes. CI runs the same check. A nested source format or generated associative array is intentionally not part of the contract; add one only if direct maintenance of these two arrays becomes a measured problem.
