Lighter is a command-line source-code highlighter. It combines tree-sitter syntax highlighting, provided by the amazing Arborium project, with semantic tokens from language servers, then writes the result as terminal colours, HTML, LaTeX, or Typst.
Arborium "gets you 90% of the way", is easy to use and can highlight a website dynamically. Lighter is for the perfectionists among us, who want the same quality of code highlighting as in an IDE. The trade-off is a lack of portability–it doesn't run in the browser, thus can't highlight code client-side. Instead, it generates code statically, based on information from locally installed language servers.
See the live highlighter comparison for an interactive comparison of Highlight.js, Arborium, and Lighter output.
- Syntax highlighting for the languages supported by Arborium
- Semantic highlighting through the Language Server Protocol (LSP)
- ANSI, HTML, LaTeX, and Typst output
- Built-in and custom themes
- Line selection without losing whole-file semantic context
- A background daemon that keeps language servers warm between invocations
Lighter is currently installed from source. It requires a recent stable Rust toolchain.
git clone https://github.com/AlecGhost/lighter.git
cd lighter
cargo install --path .Language servers are separate programs. Install the server for the language whose semantic highlighting you want.
If the matching language server is installed, expose the project directory as its workspace:
lighter --project . src/main.rsWhen input comes from standard input, specify the language:
printf 'fn main() {}\n' | lighter --lang rustlighter [OPTIONS] [FILE]
lighter daemon <COMMAND>
FILE is optional. Lighter infers the language from a file's extension. With
standard input, --lang is required.
The most useful options are:
| Option | Purpose |
|---|---|
-p, --project <DIR> |
Use a directory as the language server workspace |
-f, --format <FORMAT> |
Select ansi, html, latex, or typst |
-l, --lang <LANG> |
Set the language instead of detecting it |
--lines <RANGE> |
Render an inclusive, one-based line range |
--theme <THEME> |
Select a built-in theme |
--custom-theme <FILE> |
Load an Arborium TOML theme |
-c, --config <FILE> |
Load a Lighter TOML configuration file |
Run lighter --help for the complete interface and the available built-in
themes.
Ranges are inclusive and one-based:
lighter --lines 10:20 src/main.rs
lighter --lines :20 src/main.rs
lighter --lines 10: src/main.rsLighter still analyzes the complete source before selecting the requested lines. This lets a language server use declarations and imports outside the rendered range.
ansi is the default and emits terminal colour escape sequences.
lighter src/main.rshtml emits an HTML fragment using Arborium custom elements such as <a-k>
and <a-f>. It does not emit a complete page or CSS, so the embedding page
must style those elements.
It is recommended to link to Arborium's base.css
and one of its theme files,
e.g. catppuccin-mocha.css.
lighter --format html src/main.rs > snippet.htmllatex emits commands for an fvextra Verbatim environment with xcolor
enabled and commandchars=\\\{\}:
lighter --format latex src/main.rs > snippet.texA minimal wrapper looks like this:
\documentclass{article}
\usepackage{xcolor}
\usepackage{fvextra}
\begin{document}
\begin{Verbatim}[commandchars=\\\{\}]
\input{snippet.tex}
\end{Verbatim}
\end{document}I hacked together a package that invokes lighter automatically for you, with a similar interface as minted.
typst emits a Typst block composed from styled raw elements:
lighter --format typst src/main.rs > snippet.typThe fragment can be included directly:
#include "snippet.typ"Lighter enables both tree-sitter and LSP highlighting by default. It lazily
starts a language server the first time a language is used. The server must be
available on PATH and support full-document semantic tokens.
Built-in server commands are provided for:
| Languages | Command |
|---|---|
| Rust | rust-analyzer |
| Python | basedpyright-langserver --stdio |
| JavaScript, JSX, TypeScript, TSX | typescript-language-server --stdio |
| C, C++ | clangd |
| Go | gopls |
| Java | jdtls |
| Lua | lua-language-server |
| Zig | zls |
| Ruby | ruby-lsp |
| Kotlin | kotlin-lsp |
| Swift | sourcekit-lsp |
| Haskell | haskell-language-server-wrapper --lsp |
| OCaml | ocamllsp |
| Dart | dart language-server |
If no server is configured, its executable is missing, or it does not provide semantic tokens, Lighter reports an error.
Pass the source file whenever possible so the language server sees its real
path. For standard input, Lighter creates a temporary document. Use
--project <DIR> when the server needs project configuration, dependencies, or
other workspace files.
If you want to supply your own servers or add custom capture mappings, use
--config:
lighter --config lighter.toml src/main.rsA configuration can choose a theme, add or replace language-server commands, provide server settings, and map LSP token names to Arborium captures:
theme = "Tokyo Night"
[servers]
gleam = "gleam lsp"
[servers.go]
command = "gopls"
config = { gopls = { semanticTokens = true } }
[captures]
parameter = "variable.parameter"
[captures.rust]
const = "constant"
decorator = "constant"A custom theme can be selected relative to the configuration file:
theme = { path = "theme.toml" }Starting a daemon keeps language-server processes alive, avoiding their startup cost on every invocation:
lighter daemon spawn
lighter --project . src/main.rs
lighter daemon killNormal invocations discover and use the daemon automatically. Startup options set its defaults:
lighter daemon spawn \
--config lighter.toml \
--theme "Tokyo Night" \
--format ansiPer-invocation --format, --theme, and --custom-theme options
override the daemon defaults. Passing --config
updates the active server and capture configuration for that daemon session.
No language server available for …
The language has no built-in or configured server. Add an entry under
[servers], or pass --no-lsp.
Failed to start server for …
Install the configured language server and make sure its executable is on
PATH.
The language cannot be detected
Use an explicit language:
lighter --lang rust path/to/sourceThe language server does not understand the project
Pass its workspace directory:
lighter --project path/to/project path/to/project/src/main.rs