Lists and Selection
Lists are controlled components - you own the data and selection state, the widget handles display and keyboard navigation.
Two separate concerns:
- Navigation - Arrow keys move selection, fires
on_selectwith the new index - Activation - Enter key acts on the selected item (you handle this separately with
use_command)
You provide the items and current selection. When the user navigates, you update your state. The widget never mutates your data directly.
let items = vec!["One".to_string(), "Two".to_string(), "Three".to_string()];
let selected = state!(cx, || 0usize);
View::list()
.items(items)
.selected(selected.get())
.on_select(with!(selected => move |idx: usize| selected.set(idx)))
.build()
Run with: cargo run -p telex-tui --example 05_todo_list
Basic list
The simplest list just displays items:
let items = vec![
"Learn Telex".to_string(),
"Build something cool".to_string(),
];
View::list()
.items(items)
.build()
The list widget automatically handles:
- Scrolling when content exceeds viewport
- Arrow key navigation (↑/↓)
- Visual highlighting of the selected item
Managing selection
Track which item is selected with state:
let items = state!(cx, || vec!["Apple".to_string(), "Banana".to_string()]);
let selected = state!(cx, || 0usize);
View::list()
.items(items.get())
.selected(selected.get())
.on_select(with!(selected => move |idx: usize| selected.set(idx)))
.build()
on_select fires when the user navigates with arrow keys. The callback receives the new index.
Responding to activation
Use the use_command hook to bind keyboard shortcuts to actions. This lets you handle the Enter key on the selected item:
let selected_idx = selected.get();
cx.use_command(
KeyBinding::key(KeyCode::Enter),
with!(items, selected => move || {
let idx = selected.get();
let items_vec = items.get();
if idx < items_vec.len() {
// Do something with items_vec[idx]
println!("Activated: {}", items_vec[idx]);
}
})
);
This pattern separates navigation (on_select) from activation (Enter key).
Adding and removing items
Lists work with Vec<T> state. Use .update() to modify:
let items = state!(cx, || Vec::new());
// Add an item
items.update(|v| v.push("New item".to_string()));
// Remove at index
let idx = selected.get();
items.update(|v| {
if idx < v.len() {
v.remove(idx);
}
});
When removing items, adjust the selection to stay in bounds:
items.update(|v| {
if idx < v.len() {
v.remove(idx);
// Move selection up if we removed the last item
if idx > 0 && idx >= v.len() {
selected.set(idx - 1);
}
}
});
Empty state
Show alternate content when the list is empty:
let items = items.get();
if items.is_empty() {
View::styled_text("No items yet").dim().build()
} else {
View::list()
.items(items)
.selected(selected.get())
.on_select(on_select)
.build()
}
This gives users context rather than showing an empty list.
Working with structured data
Lists work with any type that implements Display:
struct Task {
name: String,
done: bool,
}
impl Display for Task {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let check = if self.done { "✓" } else { " " };
write!(f, "[{}] {}", check, self.name)
}
}
let tasks = state!(cx, || vec![
Task { name: "First".into(), done: false },
Task { name: "Second".into(), done: true },
]);
View::list()
.items(tasks.get())
.selected(selected.get())
.on_select(on_select)
.build()
List vs Table vs Tree
Use List when:
- Displaying a simple collection
- Items fit on one line
- Order matters (chronological, priority)
Use Table when:
- Data has multiple columns
- You need sortable headers
- Alignment matters (numbers, dates)
Use Tree when:
- Data is hierarchical
- Users need to expand/collapse sections
- Showing file systems or nested structures
Real-world example
The file browser (example 07) demonstrates:
- Directory navigation with Enter
- Parent directory (
..) handling - Distinguishing files from folders (trailing
/) - Showing file details in a modal
// In example 07_file_browser.rs
cx.use_command(
KeyBinding::key(KeyCode::Enter),
with!(current_path, selected => move || {
let entry = &entries[selected.get()];
if entry == ".." {
// Go to parent directory
} else if entry.ends_with('/') {
// Enter directory
} else {
// Show file info
}
})
);
Tips
Selection bounds: Always check idx < items.len() before accessing. Lists can be empty or shrink.
Preserve selection: When filtering or sorting, decide if selection should track the same item or stay at the same index.
Initial selection: Start at 0, or use .selected(None) if no default selection makes sense.
Performance: Lists render all items. For very large collections (10k+ items), consider virtualization or pagination.
Next: Text Input