//! # [Ratatui] List example
//! The latest version of this example is available in the [examples] folder in the repository.
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind},
layout::{Constraint, Layout, Rect},
palette::tailwind::{BLUE, GREEN, SLATE},
Color, Modifier, Style, Stylize,
Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph,
StatefulWidget, Widget, Wrap,
const TODO_HEADER_STYLE: Style = Style::new().fg(SLATE.c100).bg(BLUE.c800);
const NORMAL_ROW_BG: Color = SLATE.c950;
const ALT_ROW_BG_COLOR: Color = SLATE.c900;
const SELECTED_STYLE: Style = Style::new().bg(SLATE.c800).add_modifier(Modifier::BOLD);
const TEXT_FG_COLOR: Color = SLATE.c200;
const COMPLETED_TEXT_FG_COLOR: Color = GREEN.c500;
fn main() -> Result<()> {
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
/// This struct holds the current state of the app. In particular, it has the `todo_list` field
/// which is a wrapper around `ListState`. Keeping track of the state lets us render the
/// associated widget with its state and have access to features such as natural scrolling.
/// Check the event handling at the bottom to see how to change the state on incoming events. Check
/// the drawing logic for items on how to specify the highlighting style for selected items.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
todo_list: TodoList::from_iter([
(Status::Todo, "Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust"),
(Status::Completed, "Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui."),
(Status::Todo, "Pet your cat", "Minnak loves to be pet by you! Don't forget to pet and give some treats!"),
(Status::Todo, "Walk with your dog", "Max is bored, go walk with him!"),
(Status::Completed, "Pay the bills", "Pay the train subscription!!!"),
(Status::Completed, "Refactor list example", "If you see this info that means I completed this task!"),
impl FromIterator<(Status, &'static str, &'static str)> for TodoList {
fn from_iter<I: IntoIterator<Item = (Status, &'static str, &'static str)>>(iter: I) -> Self {
.map(|(status, todo, info)| TodoItem::new(status, todo, info))
let state = ListState::default();
fn new(status: Status, todo: &str, info: &str) -> Self {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
while !self.should_exit {
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
if let Event::Key(key) = event::read()? {
fn handle_key(&mut self, key: KeyEvent) {
if key.kind != KeyEventKind::Press {
KeyCode::Char('q') | KeyCode::Esc => self.should_exit = true,
KeyCode::Char('h') | KeyCode::Left => self.select_none(),
KeyCode::Char('j') | KeyCode::Down => self.select_next(),
KeyCode::Char('k') | KeyCode::Up => self.select_previous(),
KeyCode::Char('g') | KeyCode::Home => self.select_first(),
KeyCode::Char('G') | KeyCode::End => self.select_last(),
KeyCode::Char('l') | KeyCode::Right | KeyCode::Enter => {
fn select_none(&mut self) {
self.todo_list.state.select(None);
fn select_next(&mut self) {
self.todo_list.state.select_next();
fn select_previous(&mut self) {
self.todo_list.state.select_previous();
fn select_first(&mut self) {
self.todo_list.state.select_first();
fn select_last(&mut self) {
self.todo_list.state.select_last();
/// Changes the status of the selected list item
fn toggle_status(&mut self) {
if let Some(i) = self.todo_list.state.selected() {
self.todo_list.items[i].status = match self.todo_list.items[i].status {
Status::Completed => Status::Todo,
Status::Todo => Status::Completed,
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
let [header_area, main_area, footer_area] = Layout::vertical([
let [list_area, item_area] =
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(main_area);
App::render_header(header_area, buf);
App::render_footer(footer_area, buf);
self.render_list(list_area, buf);
self.render_selected_item(item_area, buf);
/// Rendering logic for the app
fn render_header(area: Rect, buf: &mut Buffer) {
Paragraph::new("Ratatui List Example")
fn render_footer(area: Rect, buf: &mut Buffer) {
Paragraph::new("Use ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.")
fn render_list(&mut self, area: Rect, buf: &mut Buffer) {
.title(Line::raw("TODO List").centered())
.border_set(symbols::border::EMPTY)
.border_style(TODO_HEADER_STYLE)
// Iterate through all elements in the `items` and stylize them.
let items: Vec<ListItem> = self
let color = alternate_colors(i);
ListItem::from(todo_item).bg(color)
// Create a List from all list items and highlight the currently selected one
let list = List::new(items)
.highlight_style(SELECTED_STYLE)
.highlight_spacing(HighlightSpacing::Always);
// We need to disambiguate this trait method as both `Widget` and `StatefulWidget` share the
// same method name `render`.
StatefulWidget::render(list, area, buf, &mut self.todo_list.state);
fn render_selected_item(&self, area: Rect, buf: &mut Buffer) {
// We get the info depending on the item's state.
let info = if let Some(i) = self.todo_list.state.selected() {
match self.todo_list.items[i].status {
Status::Completed => format!("✓ DONE: {}", self.todo_list.items[i].info),
Status::Todo => format!("☐ TODO: {}", self.todo_list.items[i].info),
"Nothing selected...".to_string()
// We show the list item's info under the list in this paragraph
.title(Line::raw("TODO Info").centered())
.border_set(symbols::border::EMPTY)
.border_style(TODO_HEADER_STYLE)
.padding(Padding::horizontal(1));
// We can now render the item info
.wrap(Wrap { trim: false })
const fn alternate_colors(i: usize) -> Color {
impl From<&TodoItem> for ListItem<'_> {
fn from(value: &TodoItem) -> Self {
let line = match value.status {
Status::Todo => Line::styled(format!(" ☐ {}", value.todo), TEXT_FG_COLOR),
Line::styled(format!(" ✓ {}", value.todo), COMPLETED_TEXT_FG_COLOR)