Back to Blog

Rust File I/O Tutorial: Build a Persistent Todo App with Modules | Rust by Examples #6

Sandy LaneSandy Lane

Video: Rust File I/O Tutorial: Build a Persistent Todo App with Modules | Rust by Examples #6 by Taught by Celeste AI - AI Coding Coach

Watch full page →

Rust File I/O Tutorial: Build a Persistent Todo App with Modules

In this tutorial, you'll learn how to add file persistence to a Rust Todo List application using modules. We'll organize our code with Rust's module system and implement reading and writing tasks to a text file, ensuring your todo items are saved between program runs.

Code

use std::fs;
use std::io::{self, Write};

// todo.rs - Defines Todo and TodoList structs with methods
pub mod todo {
  #[derive(Debug)]
  pub struct Todo {
    pub id: usize,
    pub title: String,
    pub completed: bool,
  }

  pub struct TodoList {
    pub todos: Vec<Todo>,
    pub next_id: usize,
  }

  impl TodoList {
    pub fn new() -> Self {
      Self { todos: Vec::new(), next_id: 1 }
    }

    pub fn add(&mut self, title: String) {
      let todo = Todo { id: self.next_id, title, completed: false };
      self.todos.push(todo);
      self.next_id += 1;
    }

    pub fn list(&self) {
      for todo in &self.todos {
        let status = if todo.completed { "✔" } else { " " };
        println!("[{}] {}: {}", status, todo.id, todo.title);
      }
    }

    pub fn complete(&mut self, id: usize) {
      if let Some(todo) = self.todos.iter_mut().find(|t| t.id == id) {
        todo.completed = true;
      }
    }

    pub fn delete(&mut self, id: usize) {
      self.todos.retain(|t| t.id != id);
    }
  }
}

// storage.rs - Handles file reading and writing for persistence
pub mod storage {
  use super::todo::{TodoList, Todo};
  use std::fs;
  use std::path::Path;

  const FILE_PATH: &str = "todo.txt";

  pub fn save(list: &TodoList) {
    let mut content = format!("{}\n", list.next_id);
    for todo in &list.todos {
      // Format: id|completed|title
      let line = format!("{}|{}|{}\n", todo.id, todo.completed, todo.title);
      content.push_str(&line);
    }
    fs::write(FILE_PATH, content).expect("Failed to write todo file");
  }

  pub fn load() -> TodoList {
    if !Path::new(FILE_PATH).exists() {
      return TodoList::new();
    }
    let data = fs::read_to_string(FILE_PATH).unwrap_or_default();
    let mut lines = data.lines();
    let next_id = lines.next().and_then(|line| line.parse().ok()).unwrap_or(1);
    let mut todos = Vec::new();

    for line in lines {
      let parts: Vec<&str> = line.split('|').collect();
      if parts.len() != 3 {
        continue; // skip malformed lines
      }
      let id = parts[0].parse().unwrap_or(0);
      let completed = parts[1].parse().unwrap_or(false);
      let title = parts[2].to_string();
      todos.push(Todo { id, completed, title });
    }

    TodoList { todos, next_id }
  }
}

// main.rs - Uses the modules to build the CLI app
mod todo;
mod storage;

use todo::todo::TodoList;
use storage::storage;
use std::io::{self, Write};

fn main() {
  let mut list = storage::load();

  loop {
    println!("\nTodo List Menu:");
    println!("1) Add task");
    println!("2) List tasks");
    println!("3) Complete task");
    println!("4) Delete task");
    println!("5) Quit");
    print!("Enter choice: ");
    io::stdout().flush().unwrap();

    let mut choice = String::new();
    io::stdin().read_line(&mut choice).unwrap();

    match choice.trim() {
      "1" => {
        print!("Enter task title: ");
        io::stdout().flush().unwrap();
        let mut title = String::new();
        io::stdin().read_line(&mut title).unwrap();
        list.add(title.trim().to_string());
        storage::save(&list);
      }
      "2" => {
        list.list();
      }
      "3" => {
        print!("Enter task ID to complete: ");
        io::stdout().flush().unwrap();
        let mut id_str = String::new();
        io::stdin().read_line(&mut id_str).unwrap();
        if let Ok(id) = id_str.trim().parse() {
          list.complete(id);
          storage::save(&list);
        }
      }
      "4" => {
        print!("Enter task ID to delete: ");
        io::stdout().flush().unwrap();
        let mut id_str = String::new();
        io::stdin().read_line(&mut id_str).unwrap();
        if let Ok(id) = id_str.trim().parse() {
          list.delete(id);
          storage::save(&list);
        }
      }
      "5" => {
        println!("Goodbye!");
        break;
      }
      _ => println!("Invalid option, try again."),
    }
  }
}

Key Points

  • Rust modules (mod and pub) help organize code into separate files and control visibility.
  • The std::fs module provides convenient functions like read_to_string and write for file I/O.
  • Using Result and matching on Ok and Err allows graceful error handling, including missing files.
  • Data is serialized to a simple text format with delimiters, making it easy to save and parse todos.
  • Combining these techniques enables building a persistent CLI app where tasks survive between runs.