⚙️ Feature-rich syntax highlighting
z-shell/f-sy-h
Install F-Sy-H
- Zi
- Zgen
- Oh-My-Zsh
- Standalone
Add the following to your .zshrc file.
zi light z-shell/F-Sy-H
Load the plugin in turbo mode:
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.
zgen load z-shell/F-Sy-H
Clone the Repository:
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.
git clone https://github.com/z-shell/F-Sy-H.git \
~/some/path/to/fsh
And add the following to your .zshrc file.
source ~/some/path/to/fsh/F-Sy-H.plugin.zsh
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:
| Form | Accepted values |
|---|---|
| Named | red, green, blue, yellow, cyan, magenta, black, white, or default |
| Indexed | 0 through 255 |
| Truecolor | Six-digit #rrggbb |
| Background | bg: before any named, indexed, or truecolor value |
| Attribute | bold, 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:
| Section | Style 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:
| Shorthand | Location |
|---|---|
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:
In the above screen-shot the interior of $( ... ) uses different colors than the rest of the code. Example
for 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):
Brackets
Conditions
Comparing to the project zsh-users/zsh-syntax-highlighting (the upper line):
Strings
Exact highlighting that recognizes quoting.
here-strings
exec descriptor-variables
Comparing to the project zsh-users/zsh-syntax-highlighting (the upper line):
The for-loops and alternate syntax (brace {/} blocks)
Function definitions
Comparing to the project zsh-users/zsh-syntax-highlighting (the upper 2 lines):
Recursive eval and $( ) highlighting
Comparing to the project zsh-users/zsh-syntax-highlighting (the upper line):
Chroma functions
Command specific highlighting
Autoload
Awk
Docker
Git commit
Git checkout
Grep
Make
Perl
Sh
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
Case highlighting
Math highlighting
Zcalc highlighting
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 forchroma function– the thing that colorizes selected commands, likegit,grep, etc. invocations, seechroma functionbelow,big loop- main highlighting code, a loop over tokens, and at least 2 large structure constructs (bigifandcase); it is advanced, e.g. parsescasestatements, 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 entersgitat the command line) suppressing activity ofbig 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
- 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.
- Big loop occurs a command that has a chroma, e.g.
git. - Big loop enters "chroma" state, calls associated chroma function.
- Chroma takes care of the "chroma" state, and ensures it will be set also for the next token.
- "chroma" state is active, so all following tokens are routed to the chroma (in general skipping big-loop, see next items),
- When processing of a single token is complete, the associated chroma returns 0 (shell-truth) to request no further processing by the big loop.
- 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$\__argfrom 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_posfrom 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
| Entry | Meaning |
|---|---|
subcommands | A Zsh pattern, or ::function returning the patterns in reply |
subcmd:NULL | Nodes 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-hook | A function called after a subcommand is recognized |
subcommands-blacklist | A 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]
POSITIONis a decimal argument position or#for any position. A recognized subcommand is not counted.KINDisoptfor a token beginning with-, orargfor a positional token.0_opthandles 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.