Shiklo Tools
PDF40
Image20
Calculators & Converters25
Text & Writing13
Developer26
Video14
Audio14
Document14
Data & Spreadsheet12
Web & SEO9
OCR & Scanning5
Archive & File12
Security & Privacy23
Health & Medical29
QR & Barcode5

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

  • Task is a struct: the text of the task and whether it is done.
  • Command is an enum: add a task, mark one done, list them all, or quit.
  • parse_command turns a typed line into a Command, or an Err with a helpful message.
  • main keeps a Vec<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);
}
You type
add water the plants
add learn Rust
dance
done 1
done 5
list
quit
Output
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

  1. Add a remove <number> command that deletes a task from the list.
  2. Save the list to todo.txt when the user quits, and load it again when the program starts.
  3. Add a clear command that removes every finished task, using retain.
  4. Print how many tasks are done and how many are left after every list.