Druk’s language system is a single registry: every file type Druk highlights is described by one entry inDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/letstri/druk/llms.txt
Use this file to discover all available pages before exploring further.
LANGUAGES. Adding a language means dropping a grammar wasm and a highlight query into place, registering them statically so the compiled binary can embed them, and adding a row to the table. Nothing else in the codebase needs to change — the parser registration, the status bar label, and the comment-toggle all derive from the same entry.
Prerequisites
- A tree-sitter grammar wasm. Most languages are covered by the
tree-sitter-wasmsnpm package, which ships pre-compiled.wasmfiles ready to use. Checknode_modules/tree-sitter-wasms/out/for a file namedtree-sitter-<language>.wasm. - A highlight query (
.scmfile). This is a tree-sitter query that maps grammar node names to capture groups (@keyword,@string,@function,@type,@comment, and so on). Many grammars ship their own queries — the ones vendored by Druk live insrc/languages/queries/.
Steps
Confirm a grammar wasm exists
Check whether If a file appears, that wasm is your grammar source. If not, you may need to compile one from the language’s tree-sitter repository — the tree-sitter CLI can emit a
tree-sitter-wasms already includes your language:.wasm from any grammar.js. Either way, Druk expects the wasm to be importable as a static path at build time.Write a highlight query at src/languages/queries/<id>.scm
Create Druk’s actual Python query follows exactly this shape:
src/languages/queries/<id>.scm. The file is a tree-sitter query: each clause matches a grammar node and assigns it to a capture group that the active theme styles.The scopes the built-in themes define are: keyword, keyword.operator, operator, string, escape, number, boolean, constant, function, function.method, function.builtin, constructor, type, type.builtin, namespace, variable, variable.builtin, variable.member, property, attribute, tag, label, punctuation, punctuation.bracket, punctuation.delimiter, punctuation.special, comment, embedded, error, and the markup.* family for prose formats.A minimal Python-style query looks like this:src/languages/queries/mylang.scm
src/languages/queries/python.scm
Register the wasm and query in src/languages/grammars.ts
Add two static imports and an entry to The full file already follows this pattern for every vendored language — Python is a representative example:
GRAMMARS. Both imports must carry with { type: 'file' }.src/languages/grammars.ts
src/languages/grammars.ts
Add an entry to LANGUAGES in src/languages/index.ts
Spread your grammar entry into a new row. The The real Python and Go entries in the registry look like this:That’s everything needed for a new grammar-backed language. Restart Druk and open a file with the matching extension — it will be highlighted.
id must match the filetype name OpenTUI uses — typically the lowercase language name, or the form pathToFiletype returns for that extension.src/languages/index.ts
src/languages/index.ts
Bundled languages
JavaScript, TypeScript, Markdown, and Zig are bundled by OpenTUI itself. They need no wasm or query from Druk — only abundled: true flag:
src/languages/index.ts
bundled: true is the full declaration.
Dialect and grammar reuse
A dialect that is close enough to an existing language can spread its grammar entry directly.javascriptreact and tsrx (Octane’s template dialect) both reuse the tsx grammar:
src/languages/index.ts
scss, sass, and less entries similarly all spread GRAMMARS.css.
Custom filetype mapping
OpenTUI maps file extensions to filetype names via its internalpathToFiletype. For files whose type lives in the file’s name rather than its extension — .env, .env.local, bun.lock — you need to add a case to filetypeForPath in src/languages/highlight.ts:
src/languages/highlight.ts
BY_NAME entry or regex before the final pathToFiletype fallback.
Status bar label
The status bar shows theid by default. Add a label only where OpenTUI’s filetype name isn’t what a person would call the file:
src/languages/index.ts
label is omitted, id is shown as-is.
Regex-only languages
When no usable grammar exists —tree-sitter-yaml needs an external scanner that OpenTUI’s worker cannot link, and several formats have no wasm at all — declare patterns instead of a wasm and query. Patterns are { group, re } pairs painted in order; later entries win the characters they overlap:
src/languages/index.ts
patterns array in the LANGUAGES entry.
Patterns overlay (grammar + patterns)
patterns beside a grammar means something different: an overlay for tokens the grammar cannot parse. .tsrx files are TypeScript/TSX plus Octane directive syntax (@if, @for, @{) that lands in tree-sitter ERROR regions. A highlight query cannot reach inside an ERROR node, so those tokens are matched by regex instead:
src/languages/index.ts
outsideProse in highlight.ts then drops any regex match that a comment or string capture already covers — so directive syntax inside a string literal stays a string, not a keyword. Everywhere else the overlay wins, because the grammar mis-attributes the token (tsx reads @catch as a call expression and marks catch as function) rather than missing it.
Testing
Add an assertion intest/languages.test.ts that a representative source snippet produces at least one highlight segment for your language. This guards against two common mistakes: a wasm that fails to load (the language stays unstyled with no console output) and a query that compiles but matches nothing (every node name in the query is wrong).