DEV Community

Cover image for Building a Terminal TODO App in Rust
Tramposo
Tramposo

Posted on

Building a Terminal TODO App in Rust

Project Overview

The Terminal TODO App is a command-line task manager built with Rust. It utilizes the tui and crossterm crates to create an interactive terminal user interface.

Key Features

  • Dual list management (TODO and DONE lists)
  • Interactive navigation
  • Task editing and deletion
  • Data persistence
  • Vim-inspired keybindings

Technical Breakdown

1. State Management

The app's state is managed through a central App struct:

struct App {
    todos: Vec<String>,
    done: Vec<String>,
    input: String,
    input_mode: InputMode,
    todo_list_state: ListState,
    done_list_state: ListState,
    editing_index: Option<usize>,
}
Enter fullscreen mode Exit fullscreen mode

This structure captures all the necessary data for the application, including the task lists, current input, and UI state.

2. User Input Handling

User input is processed in the main event loop:

fn run_app<B: tui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
    loop {
        terminal.draw(|f| ui(f, app))?;

        if let Event::Key(key) = event::read()? {
            match app.input_mode {
                InputMode::Normal => match key.code {
                    // Handle normal mode inputs
                },
                InputMode::Editing => match key.code {
                    // Handle editing mode inputs
                },
                InputMode::EditingExisting => match key.code {
                    // Handle existing item editing inputs
                },
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This pattern allows for different behaviors based on the current input mode.

3. Rendering the UI

The terminal UI is rendered using the tui crate:

fn ui<B: tui::backend::Backend>(f: &mut Frame<B>, app: &App) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .margin(2)
        .constraints([
            Constraint::Length(3),
            Constraint::Min(0),
            Constraint::Length(3),
        ])
        .split(f.size());

    // Render TODO and DONE lists
    let lists = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
        .split(chunks[1]);

    let todo_items: Vec<ListItem> = app.todos.iter().map(|i| ListItem::new(i.as_ref())).collect();
    let todo_list = List::new(todo_items)
        .block(Block::default().borders(Borders::ALL).title("Todo"));

    f.render_stateful_widget(todo_list, lists[0], &mut app.todo_list_state);

    // Similar rendering for DONE list...
}
Enter fullscreen mode Exit fullscreen mode

This function demonstrates how to create a layout and render widgets like lists and input fields.

4. Data Persistence

Task persistence is implemented using simple file I/O:

impl App {
    pub fn save_to_file(&self, filename: &str) -> io::Result<()> {
        let mut file = OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .open(filename)?;

        writeln!(file, "[TODO]")?;
        for item in &self.todos {
            writeln!(file, "{}", item)?;
        }

        writeln!(file, "[DONE]")?;
        for item in &self.done {
            writeln!(file, "{}", item)?;
        }

        Ok(())
    }

    pub fn load_from_file(filename: &str) -> io::Result<Self> {
        // Implementation of file loading...
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach is straightforward to save and load the application state between sessions.

Top comments (0)