Rust File I/O Tutorial: Build a Persistent Todo App with Modules | Rust by Examples #6
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.