Rustlers Molecule 1A: ToDo CLI Application
<figure class="video" style="margin:0;">
<div style="position:relative;width:100%;padding-bottom:56.25%;height:0;overflow:hidden;">
<iframe src="https://www.youtube.com/embed/jGXDv8CcMm0?si=Uen0TTNWmIJVB8S3"
title="YouTube video player"
style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen></iframe>
</div>
</figure>
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.txtin 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);
}
}