Rust Project: a To-Do List
Congratulations on reaching the last topic! Let us celebrate by building something complete: a small to-do list that you control by typing commands. It uses almost everything from this tutorial: a struct for each task, an enum for the commands, a vector to hold the list, match to decide what to do, Result for commands that do not make sense, and a loop that reads your input until you say goodbye.
Read the program below slowly. Every piece is something you have seen before; the new part is only how they fit together.
The plan
Taskis a struct: the text of the task and whether it is done.Commandis an enum: add a task, mark one done, list them all, or quit.parse_commandturns a typed line into aCommand, or anErrwith a helpful message.mainkeeps aVec<Task>and loops: read a line, parse it, act on it.
The program
use std::io;
struct Task {
text: String,
done: bool,
}
enum Command {
Add(String),
Done(usize),
List,
Quit,
}
fn parse_command(line: &str) -> Result<Command, String> {
let line = line.trim();
let (word, rest) = match line.split_once(' ') {
Some((w, r)) => (w, r.trim()),
None => (line, ""),
};
match word {
"add" if !rest.is_empty() => Ok(Command::Add(rest.to_string())),
"add" => Err(String::from("write the task after add, like: add buy milk")),
"done" => match rest.parse::<usize>() {
Ok(n) if n > 0 => Ok(Command::Done(n)),
_ => Err(String::from("write the task number after done, like: done 1")),
},
"list" => Ok(Command::List),
"quit" => Ok(Command::Quit),
_ => Err(format!("I do not know the command {:?}", word)),
}
}
fn print_list(tasks: &[Task]) {
if tasks.is_empty() {
println!(" (nothing to do!)");
}
for (i, task) in tasks.iter().enumerate() {
let mark = if task.done { "x" } else { " " };
println!(" {}. [{}] {}", i + 1, mark, task.text);
}
}
fn main() {
let mut tasks: Vec<Task> = Vec::new();
println!("To-do list. Commands: add <task>, done <number>, list, quit");
loop {
let mut line = String::new();
if io::stdin().read_line(&mut line).expect("could not read") == 0 {
break;
}
match parse_command(&line) {
Ok(Command::Add(text)) => {
println!("Added: {}", text);
tasks.push(Task { text, done: false });
}
Ok(Command::Done(n)) => match tasks.get_mut(n - 1) {
Some(task) => {
task.done = true;
println!("Well done! Finished: {}", task.text);
}
None => println!("There is no task {}", n),
},
Ok(Command::List) => print_list(&tasks),
Ok(Command::Quit) => break,
Err(message) => println!("Sorry, {}", message),
}
}
let left = tasks.iter().filter(|t| !t.done).count();
println!("Goodbye! {} task(s) still to do.", left);
}add water the plants add learn Rust dance done 1 done 5 list quit
To-do list. Commands: add <task>, done <number>, list, quit Added: water the plants Added: learn Rust Sorry, I do not know the command "dance" Well done! Finished: water the plants There is no task 5 1. [x] water the plants 2. [ ] learn Rust Goodbye! 1 task(s) still to do.
A few things worth noticing
Match guards. "add" if !rest.is_empty() is a pattern with an extra condition, called a guard. The arm only matches when the condition is true too, so an empty add falls through to the next arm with its friendly message.
split_once. It cuts the line at the first space into the command word and the rest, and returns None when there is no space, which match handles neatly.
Moving the text. tasks.push(Task { text, done: false }) moves the String into the new task. That is why we print it first: after the push, text belongs to the task. Swap the two lines and the compiler explains the move.
read_line returns 0 at the end of input. Pressing Ctrl+D in the terminal ends the input, read_line reads 0 bytes, and the loop stops cleanly instead of spinning forever.
Where to go next
You now know the core of Rust, and you can be proud of that. The exercises below grow this project a little further, each one practising a topic from the tutorial. After that, try writing small programs of your own: a quiz, a unit converter, a budget tracker. Building things you care about is the best way to keep learning.
Practice exercises
- Add a
remove <number>command that deletes a task from the list. - Save the list to
todo.txtwhen the user quits, and load it again when the program starts. - Add a
clearcommand that removes every finished task, usingretain. - Print how many tasks are done and how many are left after every
list.