Skip to main content

⚙️ Feature-rich syntax highlighting

z-shell/f-sy-h

Install F-Sy-H

Add the following to your .zshrc file.

zi light z-shell/F-Sy-H

Load the plugin in turbo mode:

~/.zshrc
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

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.

Performance

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

Syntax highlighting features

Themes

List the shipped themes and their declared backgrounds:

fsh_theme --list

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

fsh_theme --test clean

Apply a theme without the sample:

fsh_theme clean

Theme guide for F-Sy-H

Themes are declarative INI files. Start from a shipped theme so required styles and current fallbacks are retained:

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.

Rendering contract

Shipped themes declare their rendering assumptions:

[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:

FormAccepted values
Namedred, green, blue, yellow, cyan, magenta, black, white, or default
Indexed0 through 255
TruecolorSix-digit #rrggbb
Backgroundbg: before any named, indexed, or truecolor value
Attributebold, blink, conceal, reverse, standout, underline, each no- form, or none

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

Styles and fallbacks

Theme sections group styles by where they apply:

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

The exhaustive style order and each style's fallback chain live in the canonical theme schema. A missing style follows that declared chain and then default. Use the default shipped theme as the complete authoring template instead of copying a style table from documentation.

From an F-Sy-H checkout, the theme validator checks value syntax, required styles, metadata, contrast, semantic distinguishability, and colour-vision-deficiency separation:

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, 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.

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:

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

[command-point]
function = green
command = 180

Theme paths accept these shorthands:

ShorthandLocation
CONFIG:${XDG_CONFIG_HOME:-$HOME/.config}/f-sy-h/
CACHE:${XDG_CACHE_HOME:-$HOME/.cache}/f-sy-h/
LOCAL:/usr/local/share/f-sy-h/
HOME:$HOME/.f-sy-h/
OPT:/opt/local/share/f-sy-h/

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.

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:

; 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

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

Syntax highlighting eval

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

Variables

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

Syntax highlighting parameter Syntax highlighting in string

Brackets

Syntax highlighting brackets

Conditions

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

Syntax highlighting conditions

Strings

Exact highlighting that recognizes quoting.

Syntax highlighting strings

here-strings

Syntax highlighting here-strings

exec descriptor-variables

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

Syntax highlighting exec

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

Syntax highlighting loops

Function definitions

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

Syntax highlighting function

Recursive eval and $( ) highlighting

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

Syntax highlighting eval

Chroma functions

Command specific highlighting

Autoload

Syntax highlighting autoload

Awk

Syntax highlighting awk

Docker

Syntax highlighting docker

Git commit

Syntax highlighting git commit

Git checkout

Syntax highlighting git checkout

Grep

Syntax highlighting grep

Make

Syntax highlighting make

Perl

Syntax highlighting perl

Sh

Syntax highlighting sh -c

The Chroma registry maps command names to their handlers.

Inspect that mapping with fsh_chroma list:

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.

Fpath highlighting

Syntax highlighting fpath

Case highlighting

Syntax highlighting case

Math highlighting

Syntax highlighting math

Zcalc highlighting

Syntax highlighting zcalc

Custom Working Directory

Set the theme work directory before loading the plugin:

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.

Chroma guide for F-Sy-H

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

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.

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).

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:

_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.).

Example of chroma function

Use the shipped _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.

Declarative Chroma definitions

Git and Zi use declarative associative arrays instead of handwritten token loops. Their definitions live in chroma/_fsh_chroma_git and chroma/_fsh_chroma_zi. The shared _fsh_chroma_main parser 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.

Top-level entries
EntryMeaning
subcommandsA Zsh pattern, or ::function returning the patterns in reply
subcmd:NULLNodes active before a recognized subcommand
subcmd:<selector>//-separated nodes active for one subcommand or a parenthesized | pattern
subcmd:*Catch-all nodes for recognized subcommands without a more specific entry
subcmd-hookA function called after a subcommand is recognized
subcommands-blacklistA comma-separated list of subcommands that return to the normal highlighter
<LABEL>_<POSITION>_...A node describing an option or positional argument

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

Node names and positions

A node name has this form:

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.

Records and delimiters

Route entries list nodes with //:

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:

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:

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:

"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 <<>>:

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

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

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.