Customize component presentations
There are two customization levels.
Change component-wide presentation
Section titled “Change component-wide presentation”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.
Format typed values
Section titled “Format typed values”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.
Keep responsibilities separate
Section titled “Keep responsibilities separate”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.