Rustlers Molecule 1A: ToDo CLI Application

This molecule stitches together only the atoms 1.1–1.10. No structs, no macros beyond formatting, no external crates. You’ll see how far you can get with just: expressions and statements (1.1), scalars (1.2), tuples/arrays/slices (1.3), bindings and mutability (1.4), if / match (1.5), loops (1.6), functions (1.7), &str vs String (1.8), conversions (1.9), and formatting (1.10).

We’ll build a single-file program:

  • Stores tasks in todo.txt in the current directory (simple persistence).
  • Commands: add <text>, list, done <id>, del <id>, clear, help.
  • File format is deliberately simple: each line is "0|text" (not done) or "1|text" (done).
  • We use functions for each operation, match for the CLI dispatch,loops (1.6) to read/write lines, and formatting for clean output.
use std::env;
use std::fs;
use std::io;
use std::path::Path;

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

// This tuple represent a task
type Task = (bool, String);

// This function loads a list of tasks from file
fn load_tasks() -> io::Result<Vec<Task>> {

    // Check if the file exist, otherwise it creates 
    // an empty one
    if !Path::new(FILE_NAME).exists() {
        return Ok(Vec::new());
    }

    // Read the file concent
    let content = fs::read_to_string(FILE_NAME)?;

    // Create the file vector
    let mut tasks = Vec::new();
    for line in content.lines() {

        // Expected: "0|text" or "1|text"
        let mut parts = line.splitn(2, '|');
        let status = parts.next();
        let text = parts.next();

        let parsed = match (status, text) {
            (Some("0"), Some(t)) => (false, t.to_string()),
            (Some("1"), Some(t)) => (true, t.to_string()),
            _ => continue, // skip malformed lines
        };
        tasks.push(parsed);
    }
    Ok(tasks)
}

// Function that saves a list of tasks to a file, 
// returning the result of the operation 
fn save_tasks(tasks: &[Task]) -> io::Result<()> {
    let mut out = String::new();
    for &(done, ref text) in tasks {
        let flag = if done { '1' } else { '0' };
        out.push_str(&format!("{}|{}\n", flag, text));
    }
    fs::write(FILE_NAME, out)
}

// Function that handles the 'list' command which lists 
// the tasks present in the file
fn cmd_list() -> io::Result<()> {
    let tasks = load_tasks()?;
    if tasks.is_empty() {
        println!("No tasks yet.");
        return Ok(());
    }

    for i in 0..tasks.len() {
        let (done, ref text) = tasks[i];
        let box_char = if done { 'x' } else { ' ' };
        println!("[{}] {:>3}: {}", box_char, i + 1, text);
    }
    Ok(())
}

// Function that handles the 'add' command which adds 
// a new task to the file
fn cmd_add(words: &[String]) -> io::Result<()> {
    if words.is_empty() {
        eprintln!("Usage: add <text>");
        return Ok(());
    }
    let mut text = String::new();
    for (i, w) in words.iter().enumerate() {
        if i > 0 { text.push(' '); }
        text.push_str(w);
    }

    let mut tasks = load_tasks()?;
    tasks.push((false, text));
    save_tasks(&tasks)?;
    println!("Added.");
    Ok(())
}

// Function that handles the 'done' commant which mark as done
// a task present in the file, identifyed ID
fn cmd_done(id_str: &str) -> io::Result<()> {
    let id: usize = match id_str.parse() {
        Ok(n) => n,
        Err(_) => { eprintln!("ID must be a positive number."); return Ok(()); }
    };
    if id == 0 {
        eprintln!("IDs start at 1.");
        return Ok(());
    }

    let mut tasks = load_tasks()?;
    if id > tasks.len() {
        eprintln!("No task with id {}.", id);
        return Ok(());
    }

    let i = id - 1;
    tasks[i].0 = true;
    save_tasks(&tasks)?;
    println!("Marked #{} as done.", id);
    Ok(())
}

// Function that handles the 'del' command which deletes
// a task present in the file, identified by ID
fn cmd_del(id_str: &str) -> io::Result<()> {
    let id: usize = match id_str.parse() {
        Ok(n) => n,
        Err(_) => { eprintln!("ID must be a positive number."); return Ok(()); }
    };
    if id == 0 {
        eprintln!("IDs start at 1.");
        return Ok(());
    }

    let tasks = load_tasks()?;
    if id > tasks.len() {
        eprintln!("No task with id {}.", id);
        return Ok(());
    }

    let mut kept = Vec::new();
    for i in 0..tasks.len() {
        if i != id - 1 {
            kept.push(tasks[i].clone());
        }
    }

    save_tasks(&kept)?;
    println!("Deleted #{}.", id);
    Ok(())
}

// Function that handles the 'clear' command which deletes 
// all tasks present in the file
fn cmd_clear() -> io::Result<()> {
    save_tasks(&[])?;
    println!("Cleared all tasks.");
    Ok(())
}

// Helper function that prints documentation of all 
// available commands
fn print_help() {
    println!("Usage:");
    println!("  list                 Show all tasks");
    println!("  add <text>           Add a new task");
    println!("  done <id>            Mark a task as done");
    println!("  del <id>             Delete a task");
    println!("  clear                Remove all tasks");
    println!("  help                 Show this help");
}




fn main() {

    // Collect all passed arguments
    let args: Vec<String> = env::args().collect();

    // Check the number of arguments
    let result = if args.len() < 2 {
        print_help();
        Ok(())
    } else {

        // Match against all admitted arguments
        match args[1].as_str() {
            "list" => cmd_list(),
            "add" => cmd_add(&args[2..]),
            "done" => {
                if args.len() < 3 { eprintln!("Usage: done <id>"); Ok(()) }
                else { cmd_done(&args[2]) }
            }
            "del" | "delete" => {
                if args.len() < 3 { eprintln!("Usage: del <id>"); Ok(()) }
                else { cmd_del(&args[2]) }
            }
            "clear" => cmd_clear(),
            "help" | "-h" | "--help" => { print_help(); Ok(()) }
            other => { eprintln!("Unknown command: {}", other); print_help(); Ok(()) }
        }
    };

    if let Err(e) = result {
        eprintln!("{}", e);
    }
}

You'll only receive email when they publish something new.

More from GSLF
All posts