Styling & themes
Every visible pixel in a Verum TUI carries a Style:
public type Style is {
fg: Maybe<Color>,
bg: Maybe<Color>,
underline_color: Maybe<Color>,
add_modifier: Modifier,
sub_modifier: Modifier,
};
None means "use the surrounding cell's style". This lets widgets inherit
gracefully and lets Block borders use one style while the inner area
uses another.
Colors
public type Color is
| Reset
| Base16(Int) // 16 ANSI, 0..15
| Ansi256(Int) // 256, 0..255
| Rgb(Rgb) // TrueColor
| Hsl(Hsl) // parsed to Rgb at render
| Lab(Lab); // for perceptual work
Convenience constants (Color.Red, Color.DarkRed, Color.Cyan, …) pick
the right base-16 index and are the usual starting point.
Parsing hex
Color.Rgb(Rgb.from_hex("#1e90ff").unwrap()) // explicit
style.fg(hex("1e90ff")) // prelude shortcut
Gradients
mount core.term.style.color_utils.hex_gradient;
let stops = hex_gradient("#0066ff", "#ff00aa", 8); // List<Rgb>, 8 evenly spaced
Modifiers
Bitset of terminal attributes:
BOLD DIM ITALIC
UNDERLINED DOUBLE_UNDERLINED CURLY_UNDERLINED
SLOW_BLINK RAPID_BLINK
REVERSED HIDDEN
CROSSED_OUT OVERLINED
Style.new().bold().italic().fg(Color.Yellow)
Style.new().add_modifier(Modifier.BOLD.union(Modifier.UNDERLINED))
add_modifier specifies what to turn on, sub_modifier what to explicitly
turn off when merging with ambient style — useful when temporarily
unsetting bold inside an already-bold block.
Perceptual downsampling
TermCapabilities.color_profile is detected once at startup. Anything you
ask for — even a 24-bit RGB — is lowered to that profile by adapt_color:
The CIELAB step matters because RGB-Euclidean distance gets colour lookups embarrassingly wrong on very dark or very saturated inputs (two visually identical colours can be far apart in RGB; two different colours can be close). CIELAB is designed to be approximately perceptually uniform.
Enforce an explicit profile for dev/testing:
mount core.term.style.profile.ColorProfile;
terminal.set_color_profile(ColorProfile.Base16); // force 16-color on kitty
Themes
A Theme assigns colors to semantic roles:
public type Theme is {
surface: Color, // window background
surface_alt: Color, // alternate rows, stripes
primary: Color, // main text
muted: Color, // secondary text
accent: Color, // highlights, focus
success: Color,
warning: Color,
error: Color,
border: Color,
};
Two built-ins: Theme.dark() and Theme.light(). A typical app flips
between them:
type Msg is ToggleTheme;
fn update(&mut self, msg: Msg) -> Command<Msg> {
match msg {
ToggleTheme => {
self.theme = if self.theme == Theme.dark() { Theme.light() } else { Theme.dark() };
Command.none()
}
}
}
fn view(&self, f: &mut Frame) {
let t = &self.theme;
Block.new()
.title("Dashboard")
.borders(Borders.ALL)
.style(Style.new().fg(t.border).bg(t.surface))
.render(f.size(), f.buffer);
}
For your own theme:
let corporate = Theme {
surface: Color.Rgb(Rgb.new(18, 18, 24)),
primary: Color.Rgb(Rgb.new(230, 230, 240)),
accent: hex("00b4d8"),
..Theme.dark()
};
The text builder DSL
For inline styled runs, skip Line.styled(...) boilerplate:
mount core.term.style.text_builder.*;
let line = Line.new([
bold("Status: "),
green("online"),
Span.raw(" ("),
italic("3 peers"),
Span.raw(")"),
]);
Hyperlinks
style.hyperlink("https://verum-lang.org")
Uses OSC 8 under the hood. Terminals that don't support OSC 8 ignore it and render the text untouched.