Custom Syntax Highlighting in GNU Nano: From nanorc Patterns to True Color
Build custom syntax highlighting for GNU nano using nanorc: patterns, color slots, true-color setup, multiline limits, and nano 7.0 linter/formatter hooks — with a worked example and performance checks.
04 Sept 2026, 08:47 UTC

The Problem: Making Nano Recognize Your Language
You open a .myl file in nano and see plain white text. No keywords pop, no comments dim, no strings glow. The editor doesn't know your language exists. You could wait for a package maintainer to add a definition, or you could write one yourself in five minutes. This post shows how to build a working syntax definition, where to put it, and what breaks when you push the highlighting engine past its limits.
How Nano Finds and Loads Syntax Files
Nano reads syntax definitions from two places: the system directory (typically /usr/share/nano/*.nanorc) and your user configuration (~/.nanorc). The include directive pulls in additional files. A minimal ~/.nanorc that loads a custom language looks like this:
include "/usr/share/nano/*.nanorc"
include "~/.nano/syntax/mylang.nanorc"
The first line preserves the shipped definitions (Python, C, Markdown, etc.). The second points to your own file. Nano expands the tilde at runtime, so the path works regardless of your home directory location.
Anatomy of a Syntax Definition
Each definition lives in its own .nanorc file and contains three required parts:
- syntax — a human-readable name and a regex that matches filenames (the
headerpattern). - color / icolor — rules that bind a regex to a color slot (e.g.,
brightcyan,green).icolormakes the match case-insensitive. - start / end — optional pairs for multiline constructs like block comments.
Here is a complete ~/.nano/syntax/mylang.nanorc for a hypothetical language:
syntax "mylang" "\.myl$"
color brightcyan "\b(def|class|import)\b"
icolor green "\b(if|else|while|for|return)\b"
color yellow "\b(true|false|null)\b"
color brightred "\b[0-9]+\b"
color cyan "\"(\\\"|[^"])*\""
color brightmagenta "'(\\'|[^'])*'"
icolor brightblack "#.*$"
start="/\*" end="\*/"
color brightblack start end
Notes on the patterns:
- The
headerregex\.myl$matches the full path, not just the basename. A file namedproject/myl/configwould not trigger — only files ending in.myl. - Word-boundary escapes
\bare nano-specific (POSIX ERE does not define\b). They preventdeffrom highlighting insidedefine. - Order matters: later rules override earlier ones for overlapping matches. Put specific keywords before generic identifiers.
- The multiline comment pair
start="/\*" end="\*/"highlights everything between the first/*and the next*/. Nested/* */blocks are not supported — nano stops at the first end match.
Testing and Debugging Your Definition
After saving the file, verify that nano sees it:
nano --syntax=list | grep mylang
You should see mylang in the list. Open a test file with explicit syntax selection:
nano --syntax=mylang test.myl
If highlighting looks wrong, check which regex fired. On some builds, setting NANO_DEBUG=1 prints compiled patterns at startup:
NANO_DEBUG=1 nano test.myl 2>&1 | head -30
This output is verbose and version-dependent, but it confirms whether your patterns compiled.
True Color: When 16 Slots Aren't Enough
Nano 5.0 added 256-color and true-color (RGB) support. Enable it in ~/.nanorc:
set truecolor
Then use hex values in color rules:
color #ff8800,#111111 "\b(function|const|let)\b"
The format is #rrggbb,#rrggbb for foreground and background. Without set truecolor or on nano < 5.0, hex colors fall back to the nearest 16-color name. True color also requires a terminal that advertises the capability (e.g., xterm-256color, Alacritty, Kitty, WezTerm). If your $TERM is linux or a basic vt100, you'll see the 16-color approximation.
Performance Cost of Many Includes
Each included syntax file compiles its regexes at startup. On nano 6.x, a ~/.nanorc with 30 includes adds roughly 15–30 ms on modern hardware. Measure it yourself:
time nano --version
# baseline
time nano -c /dev/null
# with your includes loaded
If startup latency matters, combine related languages into a single file or load only the syntaxes you use daily.
Linter and Formatter Hooks (Nano 7.0+)
Nano 7.0 introduced optional linter and formatter commands per syntax. Add them inside the syntax block:
syntax "mylang" "\.myl$"
set linter "mylint %f"
set formatter "myfmt --write %f"
# ... color rules ...
The external binaries (mylint, myfmt) must be in your PATH. Nano runs the linter on save (Ctrl+S) and the formatter on demand (Ctrl+Shift+F by default). These are opt-in — no external tools are called unless you configure them.
Limitations to Keep in Mind
- No recursive parsing. Multiline start/end pairs cannot handle nested structures of the same delimiter (e.g., nested
/* */comments). The highlighter stops at the first end match. - Regex flavor is fixed. You get POSIX ERE plus nano's escapes (
\<,\>,\s,\S,\_). No lookahead, lookbehind, or backreferences. - Header matches full path. Use
header="\.myl$" notheader="myl"to avoid false positives on directories namedmyl. - System directories vary. Debian/Ubuntu/Fedora/Arch all use
/usr/share/nano/, but other distros may differ. User includes with tilde expansion are portable.
Closing Checklist
- Create
~/.nano/syntax/yourlang.nanorcwithsyntax,header, and orderedcolor/icolorrules. - Add
include "~/.nano/syntax/yourlang.nanorc"to~/.nanorc. - Run
nano --syntax=listto confirm it loads. - Test with
nano --syntax=yourlang test.yourlang. - If you need RGB colors, add
set truecolorto~/.nanorcand verify in a true-color terminal. - Measure startup with
time nano -c /dev/nullif you load many syntaxes.
That's it. You now have a maintainable, version-aware syntax definition that survives package updates and works across machines where you drop your ~/.nanorc.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.