agnostic/platform/opentui/syntax
Syntax styles for OpenTUI’s highlighted-text renderables: markdown,
code, and diff. Each of them resolves every span of text through an
OpenTUI SyntaxStyle: a lookup table from tree-sitter capture name —
"default", "markup.strong", "keyword", … — to a colour and a set of
text attributes.
A theme is a List(Style), declared on the config under a name with
opentui.register_syntax_style
and selected per element by that name with
opentui/attribute.syntax_style.
opentui.default_config()
|> opentui.register_syntax_style("handbook", [
syntax.style("default") |> syntax.fg("#E6EDF3"),
syntax.style("markup.heading.1")
|> syntax.fg("#58A6FF")
|> syntax.bold(True),
syntax.style("markup.link.url")
|> syntax.fg("#A5D6FF")
|> syntax.underline(True),
syntax.style("conceal") |> syntax.dim(True),
])
Capture names
Capture names come from the grammar, so the full set is whatever
@opentui/core’s assets/<grammar>/highlights.scm emits — not a closed
list. The ones worth styling first, for markdown: default, conceal,
markup.heading plus markup.heading.1 through markup.heading.6,
markup.strong, markup.italic, markup.strikethrough, markup.raw,
markup.link, markup.link.label, markup.link.url, markup.list, and
markup.quote. code adds the usual code captures — keyword, string,
comment, function, number, and so on — for whichever filetype it is
given. A capture with no entry contributes nothing to the span.
Lookup drops everything after the first dot
A capture resolves to the entry registered under its exact name; failing
that, to the entry registered under the segment before its first dot.
markup.heading.1 falls back to markup, not to markup.heading, so
register every level you want to style. default applies to a span only
when none of the captures covering it contributed anything.
Attributes you never set are left out
An entry that never calls bold sends no bold at all;
bold(False) sends an explicit false. That matters where two different
captures cover one span — markdown’s spell sits under every paragraph,
and an emphasis capture like markup.strong sits on a run inside it. The
highlighter merges those least-specific first, field by field, skipping
fields an entry does not carry, so a colour-only markup.strong leaves
spell’s attributes standing. An explicit false switches them off.
This is merging between captures, not inheritance along one. A capture
resolves to exactly one entry, by the rule above, and keyword.import
registered on its own therefore replaces keyword rather than adding to
it.
Types
Values
pub fn bg(style: Style, value: String) -> Style
Set the background colour. OpenTUI parses this as a ColorInput, so a hex
string like "#0D1117" and a CSS colour name like "black" both work.
pub fn fg(style: Style, value: String) -> Style
Set the foreground colour. OpenTUI parses this as a ColorInput, so a hex
string like "#58A6FF" and a CSS colour name like "cyan" both work.
pub fn style(capture: String) -> Style
Create a style for a tree-sitter capture name, with no colours and no
attributes set. Capture names are the grammar’s own: "default",
"conceal", "markup.strong", "keyword", and so on.
pub fn to_json(styles: List(Style)) -> json.Json
Encode a theme as the JSON object OpenTUI wants: one entry per capture name, under OpenTUI’s own field names, carrying only the fields that were set.
You should not need to call this yourself —
opentui.register_syntax_style
does it for you. It is public only because Config is opaque, so that
function has to live in opentui, where it cannot reach inside a Style.