Building a Command-Line To-Do App in Rust

In this blog post, I will create a simple command-line To-Do app in Rust. I will go through the code step-by-step, explaining how each part works. By the end, you will have a basic understanding of Rust and a functioning To-Do app. 

Here is the repo for this simple To-Do app.


Prerequisites:

  • Basic Rust knowledge
  • Rust installed on your computer

Getting Started:
First, let's create a new Rust project:
cargo new todo_app 
cd todo_app

Now open src/main.rs in your favorite text editor.


Importing Necessary Modules:
We'll start by importing the necessary modules from the Rust standard library:

use std::io::{stdin, stdout, Write};
use std::collections::HashMap;

Here's what each module does:

  • std::io::stdin: Reads user input from the standard input stream (usually the keyboard).
  • std::io::stdout: Accesses the standard output stream (usually the console).
  • std::io::Write: Provides the flush() method for output streams, including stdout.
  • std::collections::HashMap: Implements the HashMap data structure, a key-value store.

Setting Up the Main Function and Data Structures:
Next, let's define the main function and create the data structures for storing our tasks:
fn main() {
    let mut tasks: HashMap = HashMap::new();
    let mut task_id_counter: u32 = 1;
We use a HashMap to store tasks, with a unique identifier (u32) as the key and the task description (String) as the value. We also initialize a task_id_counter to assign unique IDs to tasks as they are added.


The Main Loop and User Interaction:
Now we'll set up the main loop to display the menu and handle user actions:

    loop {
        println!("What would you like to do?");
        println!("1) Add task");
        println!("2) List tasks");
        println!("3) Complete task");
        println!("4) Quit");

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

We use a loop to continually display the menu and read the user's choice. The stdin().read_line(&mut choice) method reads input from the user and stores it in the choice variable.


Handling User Actions:
We will use a match statement to handle the user's actions based on their input.

        match choice.trim().parse::<u8>() {

We trim() the user's input to remove any leading or trailing whitespace, and then parse() it as a u8 value.


Adding a Task:
When the user selects "1", we prompt them to enter the task description.

            Ok(1) => {
                let mut task = String::new();
                print!("Enter the task: ");
                stdout().flush().unwrap();
                stdin().read_line(&mut task).unwrap();

                tasks.insert(task_id_counter, task.trim().to_string());
                println!("Task added with ID: {}", task_id_counter);
                task_id_counter += 1;
            }

We read the task description, store it in the tasks HashMap, increment the task_id_counter, and display a confirmation message.

Listing Tasks:
When the user selects "2", we list the tasks. We can list the tasks in order by using a Vec.

            Ok(2) => {
                if tasks.is_empty() {
                    println!("No tasks.");
                } else {
                    println!("Tasks:");

                    // Collect tasks into a Vec and sort by task ID
                    let mut sorted_tasks: Vec<(&u32, &String)> = tasks.iter().collect();
                    sorted_tasks.sort_by_key(|&(id, _)| id);

                    // Iterate through the sorted Vec and print the task ID and description
                    for (id, task) in sorted_tasks {
                        println!("{} - {}", id, task);
                    }
                }
            }

By using tasks.iter().collect() to create a Vec of references to the tasks, we avoid cloning the data. Then, we use sort_by_key() to sort the Vec based on the task IDs. Finally, we iterate through the sorted Vec to print the tasks in order. This approach doesn't modify the original HashMap.


Completing a Task:
When the user selects "3", we prompt them to enter the task ID to mark it as completed and remove it from the list.

            Ok(3) => {
                let mut task_id = String::new();
                print!("Enter the task ID: ");
                stdout().flush().unwrap();
                stdin().read_line(&mut task_id).unwrap();

                match task_id.trim().parse::<u32>() {
                    Ok(id) => {
                        if tasks.remove(&id).is_some() {
                            println!("Task {} completed.", id);
                        } else {
                            println!("Task not found.");
                        }
                    }
                    Err(_) => println!("Invalid task ID."),
                }
            }
We read the task ID, try to parse it as a u32, and attempt to remove the task from the tasks HashMap. If the task is successfully removed, we display a confirmation message. If the task is not found or the input is invalid, we display an error message.


Quitting the App:
When the user selects "4", we exit the loop and end the program.
            Ok(4) => {
                println!("Goodbye!");
                break;
            }
We print a farewell message and use break to exit the loop, which will terminate the app.


Handling Invalid Input:
For any other input, we display an error message.
            _ => {
                println!("Invalid choice.");
            }
        }
    }
}
If the user enters an invalid choice, we display an error message and continue with the next iteration of the loop, prompting the user for another input.


Running the To-Do App:
To run your To-Do app, navigate to the project folder in your terminal and enter:
cargo run
That's it! You now have a simple command-line To-Do app in Rust. Enjoy using and modifying it to suit your needs. In the process, you'll learn more about Rust and its features. Happy coding!

Comments

Popular posts from this blog

A Beginner's Guide to Node.js