Skip to content
UrushiDocumentation

Customize component presentations

There are two customization levels.

Clone a Theme-owned presentation and replace glyphs, spacing, borders, or role styles. Then call its compose method:

use std::io;
use urushi::{Color, List, ThemePreset, TextStyle, dash_enumerator};
fn main() -> io::Result<()> {
let theme = ThemePreset::get("Catppuccin Mocha")
.expect("built-in theme")
.theme();
let list = List::new().item("Compile").item("Test");
let presentation = theme
.components()
.list()
.clone()
.enumerator(dash_enumerator)
.enumerator_style(TextStyle::new().foreground(Color::CYAN));
urushi::println_view(&presentation.compose(&list))
}

Rendered output

- Compile
- Test

This policy remains independent of the list’s concrete value type.

Use ListItemPresentation<T>, TableRowPresentation<T>, or TreeNodePresentation<T> when application values need component-specific text or per-value style overrides. Pass that policy to compose_with.

Callbacks are evaluated during composition. The resulting View retains owned text and styles, not application callbacks or borrowed model state.

use std::io;
use urushi::{Color, List, ListItemPresentation, ListPosition, TextStyle, ThemePreset};
struct Job {
name: &'static str,
complete: bool,
}
fn main() -> io::Result<()> {
let theme = ThemePreset::get("Catppuccin Mocha")
.expect("built-in theme")
.theme();
let jobs = List::new()
.item(Job { name: "compile", complete: true })
.item(Job { name: "test", complete: false });
let items = ListItemPresentation::new(|job: &Job, position: ListPosition| {
let mark = if job.complete { "x" } else { " " };
format!("[{mark}] {} ({}/{})", job.name, position.index() + 1, position.len())
})
.item_style(|job, _, _| {
job.complete.then(|| TextStyle::new().foreground(Color::GREEN))
});
let view = theme.components().list().compose_with(&jobs, &items);
urushi::println_view(&view)
}

Rendered output

• [x] compile (1/2)
• [ ] test (2/2)

Use the corresponding item_style, cell_style, or node_style callback when a typed value must also replace the canonical role style. Returning None keeps the component-wide fallback; returning Some(style) replaces it.

let table_rows = TableRowPresentation::<(&str, u32)>::new(|row, _, cells| {
cells.text(row.0);
cells.display(row.1);
})
.cell_style(|row, cell, _fallback: &BlockStyle| {
(row.1 == 0 && cell.column() == 1)
.then(|| BlockStyle::new().foreground(Color::GREEN).bold())
});
let tree_nodes = TreeNodePresentation::<String>::display()
.node_style(|name, _| {
name.ends_with(".rs")
.then(|| TextStyle::new().foreground(Color::CYAN))
});
fallback table cell   Errors  0
fallback tree node    └── main.rs

Only the matching count cell and Rust filename return Some(style); every other cell or node keeps its component-wide fallback.

A presentation may decide formatting, glyphs, spacing, borders, and styles. It does not own output, available terminal area, selection state, navigation, or an event loop. Those stay with the caller and the chosen surface.