A small, dependency-light engine for semantic terminal styling in Go. Write text with named, tag-based markup and resolve it to ANSI escape sequences at render time:
semstyle.ToANSI("{{|Error|}}failed{{[-]}}: {{[red::B]}}retry{{[-]}}")It depends only on lipgloss, colorprofile, and tcell/color for color resolution — no
application, TTY, or config coupling.
The tag format is based on the color tag syntax used by cview / tview, extended with semantic names, additional flags, and hyperlink support.
Direct tags apply inline styling immediately: {{[fg:bg:flags]}}…{{[-]}}
{{[red:black:B]}} ← bold red on black
{{[::U]}} ← underline only; fg and bg unchanged
{{[green]}} ← change fg only; bg and flags unchanged
{{[:blue]}} ← change bg only; fg and flags unchanged
{{[-:blue]}} ← reset fg to default, set bg to blue
{{[red:-]}} ← set fg to red, reset bg to default
{{[-]}} ← reset all styling
Color values — any named ANSI color (red, bright-blue, …), hex (#ff8800), empty
to leave the current color unchanged, - to reset that color to the terminal default, or
~ to hard-reset that color to the terminal default using a multi-parameter SGR sequence
(useful when a compositor intercepts the standard single-parameter reset forms).
The difference between - and ~ matters when compositing:
-emits a standard SGR reset (\x1b[39m/\x1b[49m/\x1b[0m). Tools likeMaintainBackgroundintercept these and re-assert the parent container's colors.~emits a hard-reset variant (\x1b[39;39m/\x1b[49;49m/\x1b[0;39;49m) that bypasses interception, resetting all the way to the terminal's own default colors.- A bare
{{[-]}}(the close tag) uses-, soMaintainBackgroundwill re-assert the parent background — use{{[~]}}if you explicitly want to escape to terminal default.
Flags (each is a single character):
| Flag | Meaning |
|---|---|
B / b |
Bold on / off |
D / d |
Dim on / off |
U / u |
Underline on / off |
I / i |
Italic on / off |
L / l |
Blink on / off |
R / r |
Reverse on / off |
S / s |
Strikethrough on / off |
H |
High Intensity: shifts fg/bg to bright variant (red → bright-red) |
- (leading) |
Reset all attributes first, then apply remaining flags |
A leading - in the flags field resets all text attributes (bold, underline, etc.) without
touching colors. It can stand alone to reset attributes only, or be combined with flags to
reset-then-set in one step:
{{[::-]}} ← reset all attributes; fg and bg unchanged
{{[::-B]}} ← reset all attributes, then set bold; fg and bg unchanged
{{[-]}} ← reset everything (fg, bg, and all attributes)
Semantic tags reference a named style resolved at render time against a style map:
{{|Name|}}…{{[-]}}
{{|Error|}}something went wrong{{[-]}}
{{|Title|}}My App{{[-]}}
Change the style map (load a theme) and every semantic tag re-styles — no call sites change. Semantic tags are the primary use case; direct tags are what semantic tags resolve to.
Semantic tags also accept optional per-use overrides in the same fg:bg:flags format,
applied on top of the registered style:
{{|Error:yellow|}} ← Error style but fg overridden to yellow
{{|Error:yellow:black:BU|}} ← fg, bg, and flags all overridden
{{|Error::black|}} ← bg overridden; fg left unchanged (empty field)
{{|Error:-:black|}} ← bg overridden; fg reset to terminal default
{{|Error:yellow:-|}} ← fg overridden; bg reset to terminal default
{{|Error::-|}} ← reset all attributes; fg and bg unchanged
{{|Error::-B|}} ← reset all attributes, then set bold; fg and bg unchanged
semstyle(this package) — the pure-ANSI styling engine. No lipgloss dependency. Import it alone if you only need tag-based styling and define styles in code.semstyle/lg— lipgloss integration layer. Re-exports the fullsemstyleAPI so most TUI applications only need this one import. AddsMaintainBackground,ToStyle,CodeToStyle,CodeToFlags, andStyleFlags. See lg/README.md.semstyle/theme— an optional layer that parses theme files (TOML) into style maps for the engine. No lipgloss dependency. Import it only if you want file-driven themes; it pulls in a TOML parser. See theme/README.md and the Theming section below.
They're one module, three packages. Pure-ANSI consumers import only semstyle; TUI
applications typically import only semstyle/lg (which re-exports semstyle).
A process-wide Default styler backs the package functions. This is all most programs need:
import "github.com/GhostWriters/semstyle"
semstyle.RegisterConsoleTag("Notice", "{{[cyan::B]}}")
fmt.Println(semstyle.ToANSI("{{|Notice|}}hello{{[-]}}"))
plain := semstyle.ToPlain(styled) // remove all tags + ANSIEach Styler owns its own tag/color maps, so you can run several independent style
configurations in one process (e.g. different themes for different surfaces):
s := semstyle.New()
s.RegisterThemeTag("Title", "{{[magenta::B]}}")
out := s.ToANSI("{{|Title|}}Report{{[-]}}")The package functions are thin delegators to Default, so the global API and the
per-instance API are identical in behavior.
Each Styler keeps two semantic maps:
- console map — built-in / base tags (registered via
RegisterConsoleTag). - theme map — overrides loaded from a theme; takes precedence over the console map.
ToANSI without a prefix resolves against the console map; with a prefix it resolves
theme-first with console fallback. Supply a theme map with SetThemeMap (or the semtheme
companion package, which parses theme files into a map).
| Function / method | Purpose |
|---|---|
ToANSI(s, prefix...) |
Expand tags → ANSI; console map by default, theme map when prefix given |
ToTags(s, prefix...) |
Expand semantic tags → direct tags; stops before ANSI conversion |
ToPlain(s) |
Remove all tags and ANSI escapes → plain text |
StripTags(s) |
Remove tags only, leaving any existing ANSI intact |
ToColor(s) |
Color name or hex string → color.Color |
ToColorStr(c) |
color.Color → hex or ANSI index string |
Sprintf(fmt, a...) |
Format string then apply ToANSI |
RegisterConsoleTag(name, val) / …Raw |
Define a base semantic tag |
RegisterThemeTag(name, val) / …Raw |
Define a theme semantic tag |
SetThemeMap(m) |
Replace the theme map wholesale |
SetRenderPolicy(fn) |
Gate rendering (return false → ToPlain instead of color) |
New() |
Create an independent *Styler |
(*Styler).SetDelimiters(…) |
Customize this Styler's tag delimiters |
(*Styler).RegisterHyperlinkTag(name) |
Make a tag render its content as a terminal hyperlink |
ToANSI produces terminal escape sequences for plain output. The semstyle/lg package
adds lipgloss integration — import it aliased as semstyle and you get the full API plus:
import semstyle "github.com/GhostWriters/semstyle/lg"
// Render tagged text against a parent container background in one call:
out := semstyle.ToANSIOnBackground("{{|Error|}}oops{{[-]}}", parentStyle)
// Build a lipgloss style from tags:
style := semstyle.ToStyle(semstyle.Default, "{{|Error|}}", base, base)
// Maintain background on already-rendered ANSI from a third-party component:
safe := semstyle.MaintainBackground(alreadyRendered, parentStyle)Use ToTags directly when passing styled text to a TUI compositor that understands direct
tags natively rather than ANSI escapes. See lg/README.md for the full
lipgloss API.
The default delimiters are {{|…|}} (semantic) and {{[…]}} (direct). Each Styler
gets these at construction and can override them independently:
s := semstyle.New()
s.SetDelimiters("{|", "|}", "{[", "]}") // this Styler onlysemstyle.SetDelimiters(...) (package-level) changes the standard values and applies them
to Default. Delimiters are per-Styler state, so different stylers can use different
markup syntaxes in the same process.
There are two ways to produce terminal hyperlinks (OSC 8).
Register a tag name — its enclosed content becomes the URL, displayed as-is:
semstyle.RegisterHyperlinkTag("URL")
semstyle.ToANSI("{{|URL|}}https://example.com{{[-]}}") // URL is both destination and labelThe registration lives on the Styler (independent of style maps), so it persists across
theme changes.
Add a label as the final colon-field of any tag. The enclosed content is the URL; the label is the visible text. An empty label uses the URL as both:
Direct tag — label is the 4th field (fg:bg:flags:label):
{{[cyan::U:DockSTARTer Website]}}https://dockstarter.com{{[-]}}
{{[cyan::U:]}}https://dockstarter.com{{[-]}} ← empty label: URL shown as text
Semantic tag — label is the 5th field (name:fg:bg:flags:label); use empty fields to
keep the registered style with no color overrides:
{{|mylink:red:black:B:DockSTARTer Website|}}https://dockstarter.com{{[-]}}
{{|mylink::::DockSTARTer Website|}}https://dockstarter.com{{[-]}} ← no overrides
Both forms work whether or not the tag is also registered as a hyperlink tag — the explicit label always takes precedence for the display text.
By default ToANSI always emits ANSI. A host that wants to suppress color when output is
redirected (or any other condition) sets a policy:
semstyle.SetRenderPolicy(func() bool { return isTerminal() })When the policy returns false, ToANSI strips instead of rendering.
The semstyle/theme subpackage parses theme files (TOML) into a style map you can hand
to a styler. It's optional — import it only for file-driven themes.
import (
"github.com/GhostWriters/semstyle"
semtheme "github.com/GhostWriters/semstyle/theme"
)
data, _ := os.ReadFile("midnight.theme")
defaults, _ := semtheme.RegisterInto(data, "") // parse + register into the Default styler
// `defaults` is the theme's opaque [defaults] table (map[string]any) for your app to interpret.A theme file carries [metadata], an optional [palette] (reusable $vars), [styles]
(semantic name → style, may reference palette vars or other styles), optional [syntax]
delimiter overrides, and an opaque [defaults] table that semtheme passes through without
interpreting (so any app can define whatever UI defaults it wants). Full details and the
theme-file format are in theme/README.md.
- Delimiters and hyperlink tags are per-
Styler; the package-level standard delimiter values seed each newStyler. - The detected color profile is process-wide — it describes the output terminal, not an individual style configuration.
- Hard-reset constants (
CodeHardReset, etc.) are multi-parameter SGR variants useful when a compositor intercepts single-parameter resets.