Skip to content

Data Widgets

List

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

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
selectedusizeSelected index
scroll_keysboolEnable keyboard scroll keys
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
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
widthLengthWidth
heightLengthHeight
on_selectCallback<ListEvent>Selection changed
on_item_clickCallback<ListEvent>Row clicked with mouse
on_activateCallback<ListEvent>Item activated (Enter/double-click)
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::spacer()                     // Non-selectable blank row
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.
ListItem::new("Building").gutter(Spinner::new())
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(" ~ ")

// 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).

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

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. 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.

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)
header_horizontal_paddingPaddingLeft/right padding for header rows
empty_text_styleStyleStyle for the empty-state placeholder text
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)). 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. So selection_full_width(false) gives a tight capsule around the label (plus any 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 resolver FileTree uses is exposed via tui_lipan::utils::{file_icon, file_icon_span}, so you can prefix list rows with themed file 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}"))]).

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
rowsVec<TableRow>Data rows
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
widthLengthWidth
heightLengthHeight
on_selectCallback<TableEvent>Row selection changed
on_activateCallback<TableEvent>Row activated
on_scroll_toCallback<usize>Scroll position

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
selectedusizeControlled selected visible row index
force_scroll_to_selectedboolForce the internal list to reveal the selected row on next render
gapu16Vertical gap between items
icon_gapu16Gap between icon and label
show_iconsboolShow expand/collapse icons
expanded_iconStringIcon for expanded nodes
collapsed_iconStringIcon for collapsed nodes
leaf_iconStringIcon for leaf nodes
icon_styleStyleIcon style
indent_styleIndentStyleIndent guide glyph variant: None, Line, Short, Long, ShortRounded, or LongRounded
indent_guide_styleStyleVertical indent guide
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
activate_on_clickboolSingle-click activates
keymapTreeKeymapKeyboard expand/collapse mapping
focus_policyFocusPolicyAccordion behavior
widthLengthWidth
heightLengthHeight
on_selectCallback<TreeEvent>Node selected
on_toggleCallback<TreeToggleEvent>Node expanded/collapsed

Tree is implemented with an inner List (flattened visible rows). Per-row item_hover_style and pointer vs keyboard row hover behavior match List - see the callout under List.

IndentStyle controls connector glyphs: None disables guides, Line uses , Short uses /, Long uses ├─/└─, ShortRounded uses /, and LongRounded uses ├─/╰─.

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),
]))

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 git-backed or application-provided change projections.

PropTypeDescription
rootimpl Into<Arc<str>>Constructor - root directory path
show_hiddenboolShow hidden files (. prefix)
max_entries_per_dirusizeCap entries per directory
directory_label_styleStyleStyle applied to directory names
file_label_styleStyleStyle applied to regular file names
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
git_statusboolShow change status badges for git/provided changes (default: true)
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
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_refresh_tokenu64Token to trigger deterministic git refresh
selectedusizeControlled selected visible row index
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
directory_iconStringDirectory icon
file_iconStringFile icon
symlink_iconStringSymlink icon
other_iconStringOther entry icon
explorerboolShow fuzzy search 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_selectCallback<FileTreeEvent>File/dir selected
on_toggleCallback<FileTreeToggleEvent>Directory expanded/collapsed

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

Behavior:

  • Directories load on demand in a background command on first expand.
  • Git is the default change source; use FileTreeChangeSource::Provided(...) to display backend-provided change data without requiring a local git repository.
  • directory_label_style(...) and file_label_style(...) style names independently from icons and right-aligned change indicators.
  • 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 M +30 -21, 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.
  • 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.
  • Filesystem fuzzy matching respects .gitignore/.ignore rules.
  • 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.
  • 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.
  • Restores pre-search expansion state when query clears.
  • Queries containing file extensions (e.g. layout.rs) prioritize filename matches.
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)))

Events

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

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 }

MIT OR Apache-2.0