Skip to content

Data Widgets ​

List ​

Selectable list of items with optional headers, spacers, prefixes, left gutters, and scrollbar.

In PointerMode::Interactive, actionable list and table rows use a pointing hand. List-backed Tree, FileTree, and SearchPalette rows share that behavior. ListItem::pointer_shape(shape) overrides one row's semantic pointer without changing keyboard selection. For example, .pointer_shape(PointerShape::Default) leaves an unavailable but selectable row as an arrow.

State-style setters use StyleSlot semantics: selection_style / active_style replace theme roles, while extend_selection_style / extend_active_style patch over them and inherit_selection_style / inherit_active_style delegate to the theme.

PropTypeDescription
itemsimpl Iterator<Item = ListItem>List items
selectedOption<usize>Selected index; pass None for no current row (no selection highlight). Bare integers still work (list.selected(0))
scroll_keysboolEnable keyboard scroll keys; Ctrl, Alt, and Super chords are left to the app
navigation_wrapboolWrap Up/Down at the first and last selectable rows (default: false). Page/Home/End still stop at the ends.
scroll_wheelboolEnable mouse wheel
scrollbarboolShow scrollbar
scrollbar_configScrollbarConfigFull scrollbar configuration (variant, gap, thumb, thumb styles)
show_scroll_indicatorsboolShow top/bottom overflow indicators
scroll_indicator_styleStyleOverflow indicator style
borderboolDraw border
titleStringBorder title
paddingimpl Into<Padding>Inner padding
empty_textStringText when list is empty
empty_text_styleStyleEmpty text style
empty_text_paddingimpl Into<Padding>Extra inset around empty-state text, independent of row padding. Combined with item_horizontal_padding. Defaults to none
active_styleStyleStyle for rows where ListItem::active(true)
extend_active_style / inherit_active_styleStyle / ()Extend or inherit the active-row theme role instead of replacing it
active_symbolOption<impl Into<Arc<str>>>Prefix symbol for active rows
active_symbol_positionListSymbolPositionRender active symbol on the left or immediately after the label
active_symbol_styleStyleStyle for active symbol
selection_symbolOption<String>Prefix for selected item (e.g. "> ")
selection_symbol_rightOption<String>Trailing symbol after the selected item's label; pair with selection_symbol for "pill" caps. Shares selection_symbol_style
symbol_columnboolEnable reservation/rendering for the built-in selection/status symbol column
selection_styleStyleSelected item style
extend_selection_style / inherit_selection_styleStyle / ()Extend or inherit the selection theme role instead of replacing it
unfocused_selection_styleStyleSelected item style while list is not focused; defaults to selection_style
extend_unfocused_selection_style / inherit_unfocused_selection_styleStyle / ()Extend or inherit the unfocused selection theme role instead of replacing it
selection_full_widthboolExtend selection to full width
unfocused_selection_symbol_styleStyleSelection symbol style while list is not focused; defaults to selection_symbol_style
gutter_gapu16Cells between row-local gutters and labels; default 0, opt into spacing with .gutter_gap(n)
gutter_for_non_selectableboolInclude non-selectable rows (headers/spacers) in gutter reservation/alignment
item_horizontal_paddingimpl Into<Padding>Left/right padding for normal rows (top/bottom ignored)
header_horizontal_paddingimpl Into<Padding>Left/right padding for header rows (top/bottom ignored)
focusableboolAccept focus
tab_stopboolInclude in sequential Tab traversal (default: true)
on_focus / on_blurCallback<()>Focus gained / lost
widthLengthWidth
heightLengthHeight
on_selectCallback<ListEvent>Selection changed
on_item_clickCallback<ListEvent>Row clicked with mouse
on_activateCallback<ListEvent>Item activated by Enter, a single click by default, or a double-click when activate_on_click is false
on_ctrl_activateCallback<ListEvent>Ctrl+click or Ctrl+Enter, independently of normal activation and activate_on_click
activate_on_clickboolActivate on a single row click (default: true); when false, double-click still activates
on_scroll_toCallback<usize>Scroll position changed

ListItem Types ​

rust
ListItem::new("Normal item")          // Selectable row
ListItem::header("Section Title")      // Non-selectable header row
ListItem::divider("Section Title")     // Header drawn as ── Section Title ──, centered at any width
ListItem::spacer()                     // Non-selectable blank row

// Divider with a styled label and rule
ListItem::from_spans([Span::new("SECTION").style(Style::new().fg(Color::Cyan).bold())])
    .role(ListItemRole::Header)
    .rule(Style::new().fg(Color::DarkGray))
ListItem::new("Service").active(true)  // Marks row as active
ListItem::role(ListItemRole::Header)   // Explicit role

// Multi-line rows
ListItem::new("build")
    .line(ListItemLine::new("target/debug/build.log").selection_left(false))

// Prefix helpers
ListItem::new("Item").numbered(1)
ListItem::new("Bullet").bulleted('•')
ListItem::new("Label")
    .prefix("> ")
    .prefix_style(Style::new().fg(Color::Cyan))

// Left gutter helpers. Spinner gutters animate with the app's spinner ticker.
// Set List::gutter_gap(1) when the framework should provide label spacing.
// Use `.leading(1)` so a lone spinner lines up with text markers like `" ●"`.
ListItem::new("Building").gutter(ListItemGutter::spinner(Spinner::new()).leading(1))
ListItem::new("Changed").gutter(ListItemGutter::text("~ "))

// Status helpers render inside the existing selection/unselected symbol column.
ListItem::new("Working").status_spinner(Spinner::new())
ListItem::new("Dirty").status_symbol(" ~ ")

// Spinners anchored to a row's own text, no reserved column across rows.
ListItem::new("Deploy")
    .description("building")
    .description_spinner(Spinner::new())
ListItem::new("Index")
    .description("scanning")
    .description_spinner(Spinner::new())
    .description_spinner_position(ListSymbolPosition::Right)
ListItem::new("Sync").label_spinner(Spinner::new())

// Fitting a right-aligned description beside its label
ListItem::new("my-project")
    .description("feat/pricing-v2")
    .primary_truncate_description_first(true)            // label wins; description gets the rest
    .primary_description_truncation(ListTruncation::Start) // `…pricing-v2`: keep the tail
    .primary_description_gap(3)                          // at least 3 blank columns between them
ListItem::new("feat/a-long-branch-name")
    .description("new session")                          // default: description wins, label truncates
    .primary_description_gap(3)
ListItem::new("Codex: reviewing the focus restore path")
    .description("Working · 2m · rozi (fix/agent-picker)") // description wins, but…
    .primary_min_label_width(Length::Percent(33))        // …never below a third of the row
    .primary_description_gap(2)

// Symbol/gutter can be rendered on a non-primary line (useful for "description above")
ListItem::new("description")
    .line(ListItemLine::new("label"))
    .symbol_line(1)
    .gutter_line(1)

Keyboard/mouse selection and activation skip non-selectable rows (Header/Spacer). Pointer callbacks are independent: on_item_click and on_activate work without an on_select callback. Attach on_select only when the parent needs to own the selected index.

When a list viewport grows while showing its final rows, the retained scroll position moves upward as needed to fill the enlarged viewport instead of leaving blank rows below the content.

ListItem::line(...) adds extra visual lines under the primary line. Selection, activation, and callbacks still use the item index (not visual line index).

A description is cut from the end unless description_truncation(ListTruncation::Start) asks to keep its tail, which suits paths and branch names. description_gap(n) keeps at least n blank columns between a label and its description whenever both have content, whichever side truncate_description_first lets win. With a gap set, a description that would be cut down to a bare … is hidden instead, together with its gap.

When the description wins, min_label_width(Length) stops it from taking the whole line: the description is cut first until the label has that much, and only then the label. Length::Percent is a share of the width the label, gap, and description share on the line, so it tracks the row as the list resizes; Length::Px is a column count; Auto and Flex keep the whole label, like truncate_description_first(true). A short label is never padded out to the minimum, and a description that fits is never cut, so a long description still starts right after a short label. ListItemLine takes these options without the primary_ prefix.

ListItem::prefix(...) renders a prefix before the primary label and automatically indents extra lines to match the label start. Use .extra_line_indent(...) to override that alignment when needed.

ListItem::gutter(...) is the canonical row-local leading adornment: use it for per-row markers, icons, spinners, or badges that need their own column before the label. Gutters reserve a consistent left gutter column across participating rows so labels stay aligned even when only some rows have gutter content. Text markers usually bake a leading indent into the string (for example " ●"); spinner gutters are only as wide as the frame, so the renderer lead-pads them inside that reserved column to keep the glyph on the same cell as those markers. The default gap between the gutter and label is 0; opt into spacing with .gutter_gap(n). By default, only selectable item rows participate, keeping headers left-aligned; use .gutter_for_non_selectable(true) when headers/spacers should reserve the same gutter width.

ListItem::status(...), .status_symbol(...), and .status_spinner(...) render inside the existing list symbol column. Status is symbol-column content, not a separate row gutter. Active symbols keep priority, then row status, then selected symbols, then the unselected symbol/spaces. Use this for one-column row state such as a busy spinner.

ListItem::description_spinner(...) and .label_spinner(...) animate next to a row's own text instead of in a column shared by every row. Both are available on ListItemLine too, and .description_spinner_position(...) / .label_spinner_position(...) choose the side (ListSymbolPosition::Left, the default, or Right). The spinner gets its own reserved cells: it is measured into the row's width budget and never shares a cell with the text, so truncation, wrapping, and any highlight offsets inside the text stay put while the glyph cycles. On a wrapped row the slot stays on the first visual line, where its width was reserved. Prefer ListItem::gutter(...) when markers should line up in a column across rows, and these when the spinner belongs to one row's text.

List::symbol_column(true) enables the built-in status/selection symbol column; the reserved width still comes from configured selection/unselected/active symbols or row status content. Use List::symbol_column(false) when row-local gutters provide the leading markers and the built-in column should not measure or render.

ListConfig ​

Composite widgets such as Select, ComboBox, MultiSelect, and SearchPalette forward shared list chrome through ListConfig. In addition to the convenience setters on those widgets, the struct carries these fields for builder-style or typed configuration:

FieldTypeDescription
borderboolWhether to draw a border around the inner list
border_styleBorderStyleInner list border style
paddingPaddingInner list padding
styleStyleInner list base style
selection_styleStyleSlotSelected/active row style
unfocused_selection_styleStyleSlotSelected row style while the list is unfocused
item_hover_styleOption<StyleSlot>Per-row hover style; None lets the host widget apply its own default (several fall back to selection_style)
selection_full_widthboolExtend selection across the row width
selection_symbolOption<Arc<str>>Selected-row symbol content
selection_symbol_rightOption<Arc<str>>Trailing selected-row symbol (right "pill" cap); shares selection_symbol_style
selection_symbol_styleOption<Style>Selected-row symbol style
unfocused_selection_symbol_styleOption<Style>Selected-row symbol style while unfocused
symbol_columnboolReserve the list symbol/status column
gutter_gapu16Gap between row-local gutters and labels; default 0
gutter_for_non_selectableboolWhether headers/spacers participate in gutter reservation
item_horizontal_paddingPaddingLeft/right padding for normal rows (interior to the highlight). Selection start/end caps consume this inset on the selected row.
header_horizontal_paddingPaddingLeft/right padding for header rows
empty_text_styleStyleStyle for the empty-state placeholder text
empty_text_paddingPaddingExtra inset around empty-state text, independent of row padding. Combined with item_horizontal_padding. Defaults to none
scrollbarboolShow a vertical scrollbar when content overflows
scrollbar_configScrollbarConfigFull scrollbar configuration

Prefer ListConfig::new()/Default::default() with builder methods over struct literals so newly added fields do not break app code.

selection_symbol (e.g. "> ") is prepended to the selected item. Unselected items are padded with spaces to maintain alignment. Include a trailing space in the symbol if needed.

Pill / capsule selection: pair selection_symbol (left cap) with selection_symbol_right (right cap). Color both caps via selection_symbol_style with foreground equal to the selection background and background equal to the row/terminal background, e.g. selection_symbol(Some("")), selection_symbol_right(Some("")), selection_symbol_style(Style::new().fg(sel_bg).bg(row_bg)). That foreground is a fill, not text: caps keep it even when it would fail WCAG against the row body (set contrast_policy on the symbol style to opt back in). The trailing cap renders only on the selected row and always closes the right edge of the highlighted region. The highlight spans the full row width only when you opt in with selection_full_width(true) (or when right-aligned content must be pushed to the edge); otherwise it hugs the content. item_horizontal_padding stays interior to the highlight and does not force a full-width bar; on the selected row, start/end caps consume that inset so unselected rows keep their padding while the caps sit in those cells. Reaching the edge is decided per item, not per visual line: if any line of a multi-line row carries a description, every line of that row spans the full width, so one row never draws two differently sized bars. So selection_full_width(false) gives a tight capsule around the label (plus any remaining padding), and selection_full_width(true) gives a full-width capsule with the cap at the row's right edge. A right-positioned active_symbol shares this slot and takes priority when both apply.

Active row rendering is independent from selection. Use active_symbol_position(ListSymbolPosition::Right) to render the active marker after the label instead of in the left symbol column. When using the right position, include any desired separator in the symbol itself (for example " ✓").

item_horizontal_padding and header_horizontal_padding accept Padding, but only left/right are applied in List.

File icons in a plain list: the Nerd Font icon resolvers FileTree uses are exposed via tui_lipan::utils::{file_icon, file_icon_span, directory_icon, directory_icon_span}, so you can prefix list rows with themed file/folder icons without reimplementing the mapping. Pass a FileIconPalette (e.g. theme.file_icons): ListItem::from_spans([file_icon_span(name, &palette), Span::new(format!(" {name}"))]) for files, or directory_icon_span(expanded, &palette) for folders (bare glyph only; no disclosure arrow).

Pointer vs keyboard row hover: For List and Table, when item_hover_style is non-empty, changing selected from the keyboard or from component logic (not a row click) stops using the mouse position for per-row hover until the pointer moves. Widget-level hover and row clicks behave as usual. Tree uses an inner List with the same item_hover_style prop, so it follows the same rules automatically.

rust
List::new()
    .items(self.files.iter().map(|f| ListItem::new(f.name.clone())))
    .selected(self.selected)
    .scrollbar(true)
    .selection_symbol(Some("> ".to_string()))
    .selection_style(Style::new().fg(Color::Cyan).bold())
    .on_select(ctx.link().callback(|e| Msg::FileSelected(e.index)))
    .on_activate(ctx.link().callback(|e| Msg::FileOpened(e.index)))

Table ​

Structured data with rows, columns, and optional scrollbar.

PropTypeDescription
headerTableRowColumn header row
rowsArc<[TableRow]> / rows_arcData rows (rows(...) collects into Arc; prefer rows_arc when sharing)
widthsVec<ColumnWidth>Column widths
selectedOption<usize>Selected row index
column_spacingu16Space between columns
row_gapu16Blank terminal rows between rendered table rows
scroll_keysboolKeyboard scroll
scroll_wheelboolMouse wheel
scrollbarboolScrollbar
scrollbar_configScrollbarConfigFull scrollbar configuration (variant, gap, thumb, thumb styles)
show_scroll_indicatorsboolOverflow indicators
scroll_indicator_styleStyleIndicator style
selection_symbolOption<String>Selected row prefix
selection_styleStyleSelected row style
extend_selection_style / inherit_selection_styleStyle / ()Extend or inherit the selection theme role instead of replacing it
header_styleStyleHeader row style
row_styleStyleDefault row style
alternating_row_styleStyleStyle applied to odd rows for zebra striping
column_style(usize, Style)Style a zero-based column; applies to header and data cells
column_stylesimpl IntoIterator<Item = Style>Set zero-based per-column styles in order
row_style_at(usize, Style)Style a zero-based absolute data row; does not affect the header
row_stylesimpl IntoIterator<Item = Style>Set zero-based per-data-row styles in order
row_style_full_widthboolExtend row hover/selection/zebra style across full row width
focusableboolAccept focus
tab_stopboolInclude in sequential Tab traversal (default: true)
on_focus / on_blurCallback<()>Focus gained / lost
widthLengthWidth
heightLengthHeight
on_selectCallback<TableEvent>Row selection changed
on_activateCallback<TableEvent>Row activated by Enter or double-click
on_scroll_toCallback<usize>Scroll position

on_activate works without on_select; use both only when activation must also update parent-owned selection.

Table style precedence for body cells is: alternating row style, then TableRow::style, indexed row style (row_style_at / row_styles), column style (column_style / column_styles), TableCell::style, and finally state styles such as hover/selection/disabled. Header cells use header styling plus column and cell styles; indexed row styles only target data rows. Full-row backgrounds from row_style_full_width(true) use row-level styles, not column or cell styles.

Column Widths ​

rust
ColumnWidth::Fixed(10)   // Fixed cell width
ColumnWidth::Fill(1)     // Proportional fill
ColumnWidth::Min(5)      // Minimum width, fills remaining

Row Sizing ​

rust
TableRow::new(vec!["col1", "col2"])
    .height(2)           // Fixed height
    .auto_height()       // Height = max line count of cells
    .bottom_margin(1)    // Spacing below row

Table::new()
    .row_gap(1)          // Global blank rows between rendered rows

Table::row_gap(gap) inserts a global number of blank terminal rows between rendered table rows. It applies between the header and first data row only when data rows exist, and between data rows, but never after the final data row. This global gap is additive with per-row TableRow::bottom_margin: use row_gap for consistent table-wide spacing, and bottom_margin for row-specific extra space.

Heatmap Cells ​

rust
TableCell::heat_fg(value, &gradient, GradientRange::new(0.0, 100.0))  // Color fg by value
TableCell::heat_bg(value, &gradient, GradientRange::new(0.0, 100.0))  // Color bg by value

Inspector Patterns ​

rust
Table::inspector(true)   // Enable inspector presets

// Row helpers
TableRow::key_value("Name", "Alice")
TableRow::section("Personal Info")
TableRow::separator()

// Hierarchy
TableRow::new(cells)
    .depth(2)
    .disclosure(TableDisclosureState::Expanded)

Inspector styling hooks: inspector_key_style, inspector_value_style, inspector_section_style, inspector_separator_style, inspector_indent_size, inspector_disclosure_symbols, inspector_separator_char.

Row semantics: TableRowRole::{Normal, Section, Separator}.

rust
Table::new()
    .header(TableRow::new(vec!["ID", "Name", "Status"]))
    .rows(self.data.iter().map(|d| {
        TableRow::new(vec![d.id.to_string(), d.name.clone(), d.status.clone()])
    }).collect())
    .widths(vec![ColumnWidth::Fixed(5), ColumnWidth::Fill(1), ColumnWidth::Fixed(10)])
    .alternating_row_style(Style::new().bg(Color::indexed(236)))
    .row_style_full_width(true)
    .selected(Some(self.selected))
    .selection_style(Style::new().fg(Color::Cyan))
    .on_select(ctx.link().callback(|e| Msg::RowSelected(e.index)))

Tree ​

Hierarchical tree view with expand/collapse.

PropTypeDescription
rootTreeNodeConstructor - root node; use Tree::forest(roots) for multiple roots
controlled_expansionboolNode expanded flags are authoritative; defaults to false
toggle_on_activateboolActivation also toggles expandable nodes; defaults to true
item_hover_effectOption<VisualEffect>Paint effect over hovered selectable or collapsible composed rows, including their interactive descendants
on_hoverCallback<TreeHoverEvent>Composed-row pointer entry and exit
on_pointer_moveCallback<TreeEvent>Pointer movement over composed rows
on_viewport_changeCallback<ScrollViewportEvent>Composed-row viewport metadata
selectedusizeControlled selected visible row index
clear_selectionboolWhen true, suppress the selection highlight (authoritative over selected and internal state)
force_scroll_to_selectedboolForce the internal list to reveal the selected row on next render
gapu16Vertical gap between items
indentu16Cells each level of depth adds (default: 2); zero puts every level in one column
content_gapu16Gap after the guides and arrow column, before the icon or content (default: 1); allows 0
show_arrowsboolShow the arrow column (default: true)
expanded_arrowStringArrow on expanded rows (default: ▼)
collapsed_arrowStringArrow on collapsed rows (default: ▶)
leaf_arrowOption<String>Glyph in the arrow column of leaves, rows with nothing to expand (default: none); AlwaysOpen rows keep their junction
arrow_styleStyleArrow style; TreeNode::arrow_style overrides it per row
show_iconsboolShow row icons from TreeNode::icon (default: true)
icon_widthu16Cells of the icon column (default: 1)
icon_gapu16Gap after the icon column, before the content (default: 1)
indent_styleIndentStyleIndent guide glyph variant: None, Line, LineCapped, LineCappedRounded, Branch, or BranchRounded
indent_guide_styleStyleVertical indent guide
indent_guide_start_depthusizeFirst non-root depth that renders guides (default: 1)
indent_gradientColorGradientGradient for indent depth
styleStyleBase style
hover_styleStyleHover style
extend_hover_style / inherit_hover_styleStyle / ()Extend or inherit the hover theme role instead of replacing it
item_hover_styleStyleIndividual item hover
extend_item_hover_style / inherit_item_hover_styleStyle / ()Extend or inherit the item hover theme role instead of replacing it
selection_styleStyleSelected item style
extend_selection_style / inherit_selection_styleStyle / ()Extend or inherit the selection theme role instead of replacing it
unfocused_selection_styleStyleSelected item style while tree is not focused; defaults to selection_style
extend_unfocused_selection_style / inherit_unfocused_selection_styleStyle / ()Extend or inherit the unfocused selection theme role instead of replacing it
selection_symbolStringSelected item prefix
selection_symbol_styleStylePrefix style
unfocused_selection_symbol_styleStylePrefix style while tree is not focused; defaults to selection_symbol_style
scrollbarboolScrollbar
scrollbar_configScrollbarConfigFull scrollbar configuration (variant, gap, thumb, thumb styles)
scroll_keysboolKeyboard scroll
scroll_wheelboolMouse wheel
empty_textStringText when tree is empty
empty_text_styleStyleEmpty text style
focusableboolAccept focus
tab_stopboolInclude in sequential Tab traversal (default: true)
on_focus / on_blurCallback<()>Focus gained / lost
activate_on_clickboolSingle-click activates
keymapTreeKeymapKeyboard expand/collapse mapping
focus_policyFocusAccordionTree accordion behavior
focus_keyStringStable key assigned to the inner focusable List or ScrollView
widthLengthWidth
heightLengthHeight
on_selectCallback<TreeEvent>Node selected
on_ctrl_activateCallback<TreeEvent>Ctrl+click or Ctrl+Enter activates without toggling expansion
on_toggleCallback<TreeToggleEvent>Node expanded/collapsed

Text-only Tree uses an inner List of flattened visible rows. Its per-row item_hover_style and pointer versus keyboard row hover match List. Trees with composed row content use a ScrollView, with tree navigation and arrows supplied by Tree.

IndentStyle selects guide appearance. Line draws vertical guides; LineCapped and LineCappedRounded finish the last sibling with └ and ╰. Branch and BranchRounded add horizontal branches, filling the available indentation width. None reserves indentation without guides. Every style uses exactly the indent cells per depth, including width zero, and the first line and continuation lines have equal widths. Arrows and icons occupy their own columns and never change indentation width. With Branch or BranchRounded, a row without an arrow extends its branch through the unused arrow cells, keeping the content gap blank before the icon or label. Other guide styles leave that column blank. Width zero suppresses the extension along with the guides.

Row layout ​

Tree and FileTree lay rows out the same way, and their options mean the same thing. From left to right, a row is:

  1. its guides, indent cells per level of depth;
  2. the arrow column, while show_arrows is on: an arrow on rows that can expand or collapse, and the same width on every other row, so sibling labels line up;
  3. content_gap blank cells, on any row with guides or an arrow column in front of it;
  4. the icon column, icon_width cells, once any row has a TreeNode::icon and show_icons is on; rows without an icon keep it blank;
  5. icon_gap blank cells after the icon column;
  6. the row's label or composed content.

Children hang beneath their parent's first glyph after its branch: its arrow, or, on a row without an arrow column, its icon or the first character of its label. Each level therefore sits indent cells deeper than its parent beneath an arrow, and indent plus content_gap cells deeper beneath an icon or label. An AlwaysOpen row shows no arrow, so with Branch guides its branch turns down into its children instead: ├─┬ group. On a row taller than one line, a guide carries down to the first child, except where the row's own content occupies that column on its later lines.

Migration: Tree now uses FileTree's option names. Replace show_icons(bool) with show_arrows(bool), expanded_icon / collapsed_icon / leaf_icon with expanded_arrow / collapsed_arrow / leaf_arrow, and icon_style with arrow_style. show_icons and icon_gap now control row icons. Replace TreeNode::indent(n) on every node with one Tree::indent(n). A top-level leaf now keeps the arrow column, like its expandable siblings.

Migration: replace Short/Long with Branch and ShortRounded/LongRounded with BranchRounded; use indentation width to control branch length.

Multiple roots and composed rows ​

Tree::forest(roots) displays several top-level nodes without an artificial parent row. A top-level node's TreeNode::gap_before(n) sets its group apart with n blank lines that are space between rows, not rows of their own: the pointer hovers nothing there, the keyboard steps over it, and row indices are unchanged. It is ignored on the first row and below the top level. Events report paths starting with each root's index. Tree::new(root) continues to display one root.

A node normally contains a ListItem, with styled spans, descriptions, status symbols, and detail lines. For independent buttons, badges, or other composed content, use TreeNode::content(element). The original ListItem supplies the row's semantic label and selectable role. Set content_height(n) for content taller than one cell. leading(element) places a column before its guides, for example an active-row marker. row_style(style) styles the whole row, including the guides. key(id) gives composed content a stable reconciliation identity across reordering. show_arrow(false) makes a row a section heading: it has no arrow column, so its label starts where its siblings' arrows would. Use it for titles over groups, typically at the top level with TreeExpansionMode::AlwaysOpen.

icon(element) gives a row an icon, such as a status glyph or a spinner, in the tree's icon column (see Row layout). It is drawn only, not a toggle; a click on it activates the row. Setting an icon renders the tree with composed rows. arrow_style(style) replaces the tree's arrow_style for one row's arrow, for example to tint a collapsed row with the state of the rows it hides.

rust
TreeNode::new("agents")
    .arrow_style(Style::new().fg(theme.status.warning))
    .child(TreeNode::new("fix login").icon(Text::new("▲")))
    .child(TreeNode::new("review").icon(Spinner::new()))

Migration: replace TreeNode::show_disclosure(false) with show_arrow(false).

Composed rows use the tree's scroll view and keyboard navigation. Item hover styles and effects apply only to selectable or collapsible rows. Non-selectable headings and other inert rows keep their normal appearance. Hover callbacks still report entry and exit for every composed row. On interactive rows, guides and arrows gain hover foreground overrides and contrast adjustment while the pointer is over the row. Selection and active-row backgrounds alone preserve the authored guide and arrow colors. Their child controls handle their own clicks; a button click does not activate the surrounding row. toggle_on_activate(false) separates row activation from expansion. Composed-row arrows still toggle independently, and Left/Right, h/l, and Space retain tree navigation. A row with no arrow to click, because show_arrows(false) or TreeNode::show_arrow(false) hides it, expands and collapses when it is clicked, in addition to activating. Each click folds it at most once: a click whose activation already toggles the row does not toggle it again, and a Ctrl+click handled by on_ctrl_activate only activates. PageUp and PageDown move the selection by a page. Moving to a child or parent emits on_select, without emitting a spurious expansion event.

With controlled_expansion(true), every node's expanded value is authoritative after a props update. Store expansion under application data IDs and map on_toggle paths back to those IDs. This preserves the intended state when producers reorder nodes or supply lazy children.

on_hover, on_pointer_move, and on_viewport_change describe composed rows. item_hover_effect applies a paint effect over hovered selectable or collapsible composed rows, including their interactive descendants. Text-only trees retain their List rendering. Selection symbols and List-specific column layout apply to text rows; composed rows supply those decorations through their content or leading column.

Run cargo run --example tree_rows to try multiple roots, detail rows, and independent buttons. cargo run --example tree_showcase shows FileTree and Tree side by side, each with live controls for the shared layout options, and for Tree's tinted arrows, always-open rows, section headings, and detail lines.

Building Nodes ​

rust
TreeNode::new("parent")
    .expanded(true)
    .child(
        TreeNode::new("child-1")
    )
    .child(
        TreeNode::new("child-2")
            .child(TreeNode::new("grandchild"))
    )

// With styled ListItem
TreeNode::new(ListItem::from_spans(vec![
    Span::new("file.rs").fg(Color::Cyan),
    Span::new(" [modified]").fg(Color::Yellow),
]))

Choose a node's behavior with TreeNode::expansion_mode(TreeExpansionMode::...):

  • Auto (default): nodes with children are collapsible; empty nodes are leaves.
  • Collapsible: keep the arrow and expand/collapse behavior even before lazy children arrive.
  • AlwaysOpen: always show children, with no arrow. The row keeps its arrow column, filled like a leaf's, so its label lines up with its siblings, and its branch turns down into its children (├─┬ group). Add show_arrow(false) for a section heading. Label styling is unchanged: expansion mode controls arrows and navigation, not the supplied ListItem or composed content. Click and Enter still activate the node; Space does not toggle it, Right enters its children, and Left moves to its parent.

Migration: an AlwaysOpen row below the top level now keeps its arrow column. Add .show_arrow(false) for a row without one.

expanded(bool) supplies the initial or controlled state for collapsible nodes. AlwaysOpen ignores that flag, including under controlled expansion and focus-based squashing. A collapsed ancestor still hides the whole subtree. Changing expansion mode preserves unrelated user-expanded branches in an uncontrolled Tree.

Migration: replace .expandable(true) with .expansion_mode(TreeExpansionMode::Collapsible). Replace .expandable(false) with Auto, or omit it. The expandable(bool) builder has been removed.

A node's ListItem renders with the tree's indentation, arrow, and icon columns in front of its label. Everything else about the item is kept: gutter, status, and prefix stay columns in front of the indentation, and extra lines, extra_line_indent, and primary_max_label_width move right with the label so they keep lining up under it, including on numbered rows. Left label spinners render after the tree guides and arrow, keeping connectors aligned with adjacent rows.

Guides for composed rows ​

TreeGuides draws the same indentation Tree does, for rows that are composed elements rather than ListItems. Describe where each visible row sits with a TreeBranch, built top-down, and put the guide spans in front of the row's own content:

rust
let guides = TreeGuides::new(IndentStyle::Branch)
    .indent(2)
    .guide_style(Style::new().fg(theme.border));
let project = TreeBranch::root();
let checkouts = [project.child(false), project.child(true)];
// `spans(branch, max_depth)`: `max_depth` spreads a `gradient` across the visible rows.
let first: Vec<Span> = guides.spans(&checkouts[0], 1); // "├ "
let width = guides.width(&checkouts[1]); // 2 cells: "└ "
TreeGuides methodDefaultEffect
new(IndentStyle)-Connector glyphs, as for Tree::indent_style
indent(u16)2Cells per level
guide_style(Style)defaultStyle of the guide characters
gradient(ColorGradient)noneColors guides by depth
start_depth(usize)1Shallowest depth that draws guides; shallower rows indent with blanks

For a row with more than one line, put continuation_spans(branch, max_depth) in front of the later lines. It keeps guides running through ancestors that still have rows below, and through the row's own column while a later sibling follows, at the same width as spans.

Events ​

rust
// TreeEvent { index: usize, path: Vec<usize> }
// TreeToggleEvent { index: usize, path: Vec<usize>, expanded: bool }

.on_select(ctx.link().callback(|e: TreeEvent| Msg::NodeSelected(e.path)))
.on_toggle(ctx.link().callback(|e: TreeToggleEvent| Msg::NodeToggled(e.path, e.expanded)))

Keymap: TreeKeymap supports expand/collapse via Left/Right, h/l, Space (toggle).


FileTree ​

Lazy-loading filesystem explorer built on Tree, with local or application-provided directory entries and git-backed or application-provided change projections.

PropTypeDescription
rootimpl Into<Arc<str>>Constructor - root directory path
entry_sourceFileTreeEntrySourceDirectory entry source; defaults to local filesystem enumeration
entry_refresh_tokenu64Re-read local root and currently expanded directories when the token changes; ignored for provided entries
refresh_policyFileTreeRefreshPolicyHow local entries notice filesystem changes: Manual (default, tokens only), Polling(Duration), or Watch
show_hiddenboolShow hidden files (. prefix)
max_entries_per_dirusizeCap entries per directory, keeping the first in display order
show_iconsboolShow file and directory icons independently of arrows (default: true)
show_arrowsboolShow directory expansion arrows independently of icons (default: true)
expanded_arrowimpl Into<Arc<str>>Expanded-directory arrow (default: ▼)
collapsed_arrowimpl Into<Arc<str>>Collapsed-directory arrow (default: ▶)
arrow_styleStyleExpansion arrow style, independent of icon and label styles
content_gapu16Blank cells after guides and optional arrows, before the item icon or label; default 1, allows 0
icon_gapu16Blank cells between icons and labels; default 1, allows 0, ignored when icons are hidden
icon_styleFileIconStyleText, Nerd Font, or colored Nerd Font icons
icon_paletteFileIconPaletteSemantic icon colors
icon_override(pattern, icon, Option<Color>)Override icons by filename, directory name, or file extension
directory_label_styleStyleStyle applied to directory names
file_label_styleStyleStyle applied to regular file names
hidden_styleStyleStyle patched onto the icon and label of dot-prefixed entries, e.g. Style::new().dim() (default: none)
ignored_styleStyleStyle patched onto the icon and label of entries that ignore rules exclude, and everything inside them (default: none)
path_style(path, FileTreeItemStyle)Apply row/icon/label/suffix styles to one exact path
path_stylesimpl IntoIterator<Item = (path, FileTreeItemStyle)>Apply exact path-specific item styles in bulk
symlink_targetsboolShow where each symlink points, as link → target (default: true)
symlink_target_arrowimpl Into<Arc<str>>What separates a symlink from its target (default: "→")
symlink_target_styleStyleStyle of the arrow and target after a symlink's name (default: dim)
git_statusboolShow change status badges for git/provided changes (default: true)
git_status_padding_rightu16Blank cells after the final status marker; default 0. Use 1 for Nerd Font icons that need room to render at full size
highlight_changed_labelsboolAlso apply change status colors to file/directory labels (default: false)
change_suffix_styleStyleStyle only the right-side change metadata suffix, such as status markers and diff stats
change_suffix_priorityFileTreeSuffixPriorityWhether labels or right-side change metadata are preserved first when rows are narrow
change_sourceFileTreeChangeSourceChange metadata source; defaults to local git status/diff data
change_viewFileTreeChangeViewAllFiles (default) or ChangedOnly source-agnostic view
compact_directoriesboolIn ChangedOnly, fold single-subdirectory chains into one a/b/c row (default: false)
show_diff_statsboolShow +N -M diff stats next to change markers
git_suffix_styleStyleCompatibility setter for styling only right-side git metadata
git_suffix_priorityFileTreeSuffixPriorityCompatibility setter for right-side git metadata truncation priority
git_viewFileTreeGitViewAllFiles (default) or ChangedOnly git-focused view
git_changed_onlyboolCompatibility convenience setter for changed-only mode
git_diff_statsboolCompatibility setter for +N -M diff stats next to change markers
git_status_cacheFileTreeGitStatusCacheApp-owned bounded cache that keeps the last successful local Git snapshot visible across mounts while it revalidates
git_refresh_tokenu64Token to trigger deterministic git refresh
selectedusizeControlled selected visible row index
clear_selectionboolWhen true, suppress the selection highlight (authoritative over selected and internal state)
selected_pathimpl Into<Arc<str>>Controlled selection by absolute path under the root or path relative to the root, when the row is visible
reveal_pathimpl Into<Arc<str>>Expand/load ancestors for an absolute or root-relative path when possible
select_pathimpl Into<Arc<str>>Reveal and select a path, forcing the tree to scroll to the row when visible
force_scroll_to_selectedboolForce the tree to reveal the selected row on next render
expanded_pathsimpl IntoIterator<Item = impl Into<Arc<str>>>Controlled expanded directory paths; the root path is kept expanded automatically
initial_expanded_pathsimpl IntoIterator<Item = impl Into<Arc<str>>>Expansion seeded on mount and on each root change, which the tree then owns
empty_text_paddingimpl Into<Padding>Inset around the empty-state text; defaults to none, flush with the rows
indent_widthu16Indentation cells per hierarchy level (default: 2); use 1 for compact guides and content_gap for the separator
directory_iconStringDirectory icon
opened_directory_iconStringExpanded directory icon in text mode
file_iconStringFile icon
symlink_iconStringSymlink icon
other_iconStringOther entry icon
explorerboolShow fuzzy search input
explorer_max_matchesusizeMatches a query shows, best-scoring first (default 1000)
tree_focus_keyStringKey used for direct focus of the inner tree
explorer_focus_keyStringKey used for direct focus of the explorer input
explorer_placeholderStringSearch input placeholder
explorer_prefixStringSearch input prefix
explorer_input_borderboolSearch input border
explorer_match_styleStyleFuzzy match highlight
explorer_dividerboolShow divider between search and tree
on_explorer_focusCallback<FileTreeExplorerFocusOrigin>Explorer input focused from the tree or pointer
on_explorer_blurCallback<()>Explorer input lost focus
on_explorer_escapeCallback<()>Leave pointer-focused explorer input on Escape or an outside tree/divider click
focusableboolAccept focus
tab_stopboolInclude the tree target in sequential Tab traversal (default: true)
on_focus / on_blurCallback<()>Tree focus gained / lost
on_selectCallback<FileTreeEvent>File/dir selected
on_ctrl_activateCallback<FileTreeEvent>Ctrl+click or Ctrl+Enter on a file or directory, without toggling expansion
on_toggleCallback<FileTreeToggleEvent>Directory expanded/collapsed
on_entry_requestCallback<FileTreeEntryRequest>Missing provided directory listing requested

Plus all Tree styling/scrolling props, including indent_style and scrollbar_config.

Behavior:

  • Local directories load on demand on first expand.
  • When on_ctrl_activate is set, Ctrl+click and Ctrl+Enter invoke it instead of normal activation. Ctrl+click still selects the entry, but leaves directory expansion unchanged. This works with activate_on_click(false) too. Without the callback, existing activation behavior is retained.
  • Change entry_refresh_token(...) to refresh local directory entries in place without remounting or resetting expansion or explorer state. The root and currently expanded directories are reread; collapsed descendants are unloaded and reread when reopened. Reads run in a background command; results for an older token, root, or entry source are ignored. An active explorer query reruns against the refreshed entries, and uncontrolled selection follows its path with the root as the deterministic fallback when that path disappears. This does not refresh Git status; use git_refresh_token(...) separately. Provided entry sources ignore the token because their listings are owned by the application.
  • refresh_policy(FileTreeRefreshPolicy::Polling(interval)) rereads the root, the expanded directories, and Git status at that interval, with a floor of 100 ms.
  • refresh_policy(FileTreeRefreshPolicy::Watch) watches the root and each expanded directory, one level each, so the watch set follows what is on screen rather than the size of the tree. Changes in a directory are collected for about 100 ms and that directory alone is reread; open subdirectories stay open. A busy directory still refreshes several times a second instead of waiting for writes to stop. Reopening a collapsed directory rereads it, since nothing watched it while it was closed.
  • With Git status shown, Watch also watches, open or not, every directory that holds a tracked file or a non-ignored untracked file, and every untracked directory including empty ones, plus the repository's metadata (HEAD, index, refs, packed-refs, info/exclude; linked worktrees are resolved through Git). Any change there, including a mode change such as chmod +x, refreshes Git status after about 400 ms. A created or removed entry, or an edit to a .gitignore, also updates what is watched. Ignored directories such as build output stay unwatched. A repository with more than 4,096 such directories is not watched this way. Two cases wait for the repair reread: a file added to an empty directory nested inside an otherwise empty untracked directory, and an edit to a global core.excludesFile.
  • The whole tree and Git status are reread every 30 seconds to repair missed events, or every 5 seconds while anything cannot be watched. Watching stops when the tree unmounts. It needs the file-tree-watch feature; without it, Watch polls every 5 seconds.
  • Git status reads run with GIT_OPTIONAL_LOCKS=0, so refreshing never rewrites the index.
  • Every directory is expandable, including one that turns out to be empty; a lazy tree cannot know a directory is empty until it has been opened, so its row keeps the same shape either way.
  • empty_text renders in place of the tree when a ChangedOnly view has nothing changed. Browsing keeps its root row instead, since that row is the directory being browsed rather than a heading over a projection. With FileTreeChangeSource::Git the placeholder waits for the first git status to finish; with provided change data it appears as soon as the supplied list is empty, so an application that fetches its own changes should vary empty_text while it loads.
  • With FileTreeEntrySource::Provided, an absent directory listing is pending: the widget renders loading_label, emits one FileTreeEntryRequest for the expanded path, and waits for the app to rebuild it with a matching FileTreeDirectoryListing. The callback must enqueue remote or other blocking work through a Command; it must not perform that work on the UI thread.
  • Provided paths use the flavor of the root (/home/... stays POSIX and C:\... stays Windows), regardless of the client's operating system. Relative roots still resolve against the local current directory.
  • Keep completed listings in app state and batch responses received together into one state update. The widget incrementally applies changed directory results, while one batched prop update avoids repeated comparison and status-projection work across a large listing set.
  • Provided entries include is_dir, is_symlink, git_status, and ignored. Their Git status is rendered without local Git discovery. Ignored entries remain visible during normal browsing, as with the local source, but explorer search excludes them.
  • In changed-only mode, provided directories with pending descendants remain visible so they can be expanded and requested; a loaded clean directory disappears from that projection.
  • Git is the default change source; use FileTreeChangeSource::Provided(...) to display backend-provided change data without requiring a local git repository.
  • Clone one FileTreeGitStatusCache into trees that replace one another to avoid blank Git indicators after a keyed remount. The cache retains successful local snapshots only, separates status-only and diff-stat data, ignores provided sources, and revalidates every mount.
  • directory_label_style(...) and file_label_style(...) style names independently from icons and right-aligned change indicators. A symlink takes the label style of what it resolves to.
  • hidden_style(...) patches the icon and label of entries whose name starts with a dot, so dotfiles can recede without being filtered out. Path styles and changed-label colors still layer over it.
  • ignored_style(...) does the same for ignored entries, such as a target/ build directory and everything in it. A local tree learns them from Git status, so it needs the Git change source; a provided tree reads FileTreeEntry::ignored.
  • With Nerd Font icons, a symlink draws as a linked file or a linked directory, depending on what it resolves to. The row stays a leaf either way.
  • path_style(...) / path_styles(...) match exact paths and can override row, icon, label, and suffix styling for reviewed, pinned, or otherwise annotated files.
  • change_suffix_style(...) and git_suffix_style(...) style only right-side metadata such as +30 -21 M, leaving icons and labels unchanged.
  • change_suffix_priority(FileTreeSuffixPriority::Suffix) keeps right-side metadata visible first on narrow rows, truncating labels before suffixes.
  • Change colors apply to the right-aligned indicators by default; use highlight_changed_labels(true) to also color dirty file and directory names, layered over their label styles.
  • FileTreeChangeView::ChangedOnly shows changed files only, grouped by ancestor directories. With provided change data this projection is virtual/source-agnostic and can include nonexistent or deleted paths supplied by the backend.
  • FileTreeGitView is a compatibility alias for FileTreeChangeView; git_view, git_changed_only, and git_diff_stats remain available for git-focused call sites.
  • Changed-only mode respects show_hidden; paths under hidden components such as .github/ appear only when hidden entries are enabled.
  • compact_directories(true) folds each run of directories that hold only one subdirectory into a single changed-only row, such as src/widgets/file_tree. The row stands for the innermost directory, so expansion, selection, and events use that path. An active explorer query shows the tree unfolded.
  • show_diff_stats(true) displays numeric diff stats when the selected change source provides them; untracked and binary files may show a status marker without +N -M counts.
  • Change metadata displays diff statistics first, followed by staged and unstaged status markers, for example +42 -11 M. Status markers align at the right edge across rows with and without statistics. git_status_padding_right(1) reserves a blank cell after the final marker for Nerd Font glyphs that need room to render at full size. The default is 0, and rows with only diff statistics receive no trailing padding.
  • Filesystem fuzzy matching respects .gitignore/.ignore rules.
  • Explorer search runs in a background command. The first keystroke walks the root once; later keystrokes filter that walk, so typing stays fast in large trees. The walk is taken again, and an active query reruns, when a watched directory's entries change or a watched .gitignore, .ignore, or info/exclude changes. An entry refresh and each new search also walk again. A change the tree does not watch, including any change under FileTreeRefreshPolicy::Manual, shows up in the next search.
  • Search offers only entries the tree can show: in a directory over max_entries_per_dir, it matches the same entries the tree lists, ignored ones counting toward the cap.
  • A query matches entry names, and paths below the root unless it contains a . without a path separator. The root's own path never takes part, so a query that spells part of it does not match every entry. A name match ranks above a match that needs the directories above it.
  • The tree shows the explorer_max_matches best-scoring matches. The input's counter reads shown/total when some matches are left out.
  • In changed-only mode, fuzzy matching is scoped to the changed-path projection instead of the whole filesystem.
  • Auto-expands ancestor directories to reveal search matches.
  • Clicking the explorer input focuses it. Enter commits the query and focuses the tree; Escape entered from the tree with / also returns to the tree. Pointer-entered Escape and outside tree or divider clicks emit on_explorer_escape when configured.
  • on_explorer_focus reports FileTreeExplorerFocusOrigin::Tree or Pointer, and on_explorer_blur reports when the input releases focus for app-level key routing.
  • selected_path, reveal_path, and select_path normalize absolute paths under the root or paths relative to the root. They are no-ops for paths outside the root, paths hidden by show_hidden(false), absent/unreadable/capped entries, or rows filtered out by the current all-files/changed-only projection. selected_path only selects an already-visible row; reveal_path expands/loads ancestors when possible; select_path combines reveal + selection and scrolls to the selected row. With controlled expanded_paths, app-provided expansion remains authoritative, so reveal/select can only display rows made available by the controlled expansion set plus the reveal request during rendering.
  • initial_expanded_paths restores expansion across an unmount or a root change: it seeds the tree's own expansion state on mount and on every root change, and is never applied again, so it cannot fight expansion the user has changed since. Record the set with on_toggle. Paths outside the root are ignored, and ancestors are not expanded alongside a seeded path — collapsing a directory leaves what is inside it expanded, so a seeded descendant must not reopen the parent that hides it. Ignored when controlled expanded_paths is set.
  • Restores pre-search expansion state when query clears.
  • Queries containing file extensions (e.g. layout.rs) prioritize filename matches.

Expansion arrows and file icons are independent in every icon style. For an arrow-only tree, use .show_icons(false).show_arrows(true). For labels only, disable both. Sibling files reserve the arrow column so their labels align with directory labels. Custom arrow glyphs share a column sized to the wider glyph, followed by content_gap blank cells. icon_gap controls the blank cells between icons and labels. Both gaps default to one and allow zero. The content gap remains when arrows are hidden and follows the guides instead. Rows without a structural prefix, such as a root with no arrow, receive no content gap. Icons alone do not create a structural prefix. With arrows hidden, a directory's children hang beneath its icon, or its name when icons are hidden too. This is the same row layout Tree uses, with the same option names. Override Nerd Font arrows with .expanded_arrow("").collapsed_arrow("") when desired.

Migration: show_icons(false) now leaves expansion arrows visible, and text icons gain arrows when show_arrows is enabled. Add .show_arrows(false) to retain an arrow-free tree. Directory icon overrides now keep their expansion arrows; use expanded_arrow and collapsed_arrow to customize arrows instead of replacing directory icons.

Run the complete asynchronous pattern with cargo run --example provided_file_tree.

rust
let listings = vec![FileTreeDirectoryListing::new(
    project_root.clone(),
    [
        FileTreeEntry::directory("src"),
        FileTreeEntry::file("README.md").git_status(GitFileStatus::new(
            None,
            Some(GitChangeState::Modified),
        )),
    ],
)];

FileTree::new(project_root)
    .entry_source(FileTreeEntrySource::Provided(listings))
    .on_entry_request(
        ctx.link()
            .callback(|request: FileTreeEntryRequest| Msg::ListDirectory(request.path)),
    )
rust
let changes = vec![
    FileTreeChange::new("src/main.rs", FileTreeChangeStatus::Modified)
        .kind(FileKind::File)
        .diff_stat(12, 3)
        .staged(true),
    FileTreeChange::new("docs/removed.md", FileTreeChangeStatus::Deleted)
        .kind(FileKind::File),
];

FileTree::new(project_root)
    .change_source(FileTreeChangeSource::Provided(changes))
    .change_view(FileTreeChangeView::ChangedOnly)
    .show_diff_stats(true)
    .path_style(
        "src/main.rs",
        FileTreeItemStyle::new()
            .row(Style::new().fg(Color::DarkGray))
            .icon(Style::new().fg(Color::DarkGray))
            .label(Style::new().fg(Color::DarkGray)),
    )
    .change_suffix_style(Style::new().fg(Color::Yellow))
    .change_suffix_priority(FileTreeSuffixPriority::Suffix)
rust
FileTree::new("/home/user/projects")
    .git_status(true)
    .change_view(FileTreeChangeView::ChangedOnly)
    .show_diff_stats(true)
    .show_hidden(false)
    .explorer(true)
    .explorer_placeholder("Filter files...")
    .on_select(ctx.link().callback(|e: FileTreeEvent| Msg::FileSelected(e.path)))

Git styles and porcelain semantics ​

Unset git_style_modified, git_style_added, git_style_deleted, git_style_renamed, git_style_untracked, and git_style_conflicted use the matching theme.git_status color. They follow the active scoped theme when it changes. Each setter replaces that role's style completely, including an explicitly empty Style::default(). Setting only some roles leaves the rest theme-derived. Staged non-conflict markers and staged label highlights use the added role; conflicts use the conflicted role. Numeric additions and deletions use the added and deleted roles.

GitFileStatus::from_porcelain_xy(x, y) decodes semantic status columns without running Git or reading paths. Applications can execute Git remotely, impose their own timeouts and output limits, and convert the result into their own transport types. FileTree's local status reader uses the same decoder.

The decoder keeps index and worktree changes in staged and unstaged; primary_state() prefers staged changes. ?? is unstaged Untracked, while ignored !!, clean pairs, and invalid columns return None. A space or porcelain v2 dot denotes an unchanged column. All seven unmerged pairs, UU, AA, DD, AU, UA, DU, and UD, yield a single Conflicted entry in staged, with no unstaged entry. These columns describe merge sides rather than index and worktree changes. Copies share the Renamed presentation category, and type changes use Modified. Callers needing copy identity or the precise conflict subtype should retain the original columns alongside the decoded presentation status.

Events ​

rust
// FileTreeEvent { path: Arc<str>, kind: FileKind }
// FileTreeToggleEvent { path: Arc<str>, kind: FileKind, expanded: bool }
// FileTreeEntryRequest { path: Arc<str> }

LogView ​

High-throughput log list with level highlighting and fuzzy filtering.

PropTypeDescription
bufferArc<LogBuffer>Bounded ring buffer of log entries
filter_modeMatchModeFuzzy, Substring, Exact
case_sensitiveboolCase-sensitive filtering
auto_followboolAuto-scroll to newest entry
pausedboolPause log streaming display
trace_styleStyleTRACE level style
debug_styleStyleDEBUG level style
info_styleStyleINFO level style
warn_styleStyleWARN level style
error_styleStyleERROR level style
unfocused_selection_styleStyleSelected row style while log view is not focused; defaults to selection_style
extend_unfocused_selection_style / inherit_unfocused_selection_styleStyle / ()Extend or inherit the unfocused selection theme role instead of replacing it
on_selectCallback<LogViewEvent>Entry selected
on_activateCallback<LogViewEvent>Entry activated

Plus standard list/scroll styling props, including scrollbar_config.

rust
let buffer = Arc::new(LogBuffer::new(10_000)); // 10k entry ring buffer

// In a background thread: buffer.push(LogEntry { level, message });

LogView::new(buffer.clone())
    .filter_mode(MatchMode::Fuzzy)
    .auto_follow(true)
    .info_style(Style::new().fg(Color::Green))
    .error_style(Style::new().fg(Color::Red).bold())

Events ​

rust
// LogViewEvent { visible_index: usize, source_index: usize, entry: LogEntry }

MPL-2.0