Review of a Todo program using CLI and formatting

⚓ rust    📅 2025-05-28    👤 surdeus    👁️ 4      

surdeus

Warning

This post was published 32 days ago. The information described in this article may have changed.

Here's my Todo List program

use clap::{Parser, Subcommand};
use colored::Colorize;
use rusqlite::{params, Connection};
use rusqlite::types::Null;
use time::{PrimitiveDateTime, OffsetDateTime, Duration, Date};
use time::macros::format_description;

fn slice_to_string(slice: &[i32]) -> String {
    let mut str = String::new();
    for (i, item) in slice.iter().enumerate() {
        str.push_str(item.to_string().as_str());
        if i <= slice.len() - 2 {
            str.push_str(", ");
        }
    }
    str
}

#[derive(Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    Add { item: String },
    Complete { id: i32 },
    Remove { id: i32 },
}

fn add_item(item: String, conn: &Connection) -> Result<(), Box<dyn std::error::Error>> {
    println!("Do you want to add a due date? (y/n)");
    let str = read_line()?;
    if str.chars().nth(0) == Some('y'){
        println!("What date do you want to add a due date (YYYY-MM-DD)");
        let format = format_description!("[year]-[month]-[day]");
        let date_time = Date::parse(&read_line(), format)?.midnight();
        conn.execute("INSERT INTO items (ITEM, DUE_DATE, IS_COMPLETED) VALUES (?, ?, ?)", params![item, date_time, false])?;
    } else {
        conn.execute("INSERT INTO items (ITEM, DUE_DATE, IS_COMPLETED) VALUES (?, ?, ?)", params![item, Null, false])?;
    }
    Ok(())
}

fn read_line() -> Result<String, Box<dyn std::error::Error>> {
    let mut buf = String::new();
    std::io::stdin().read_line(&mut buf)?;
    Ok(buf)
}

fn list_items(conn: &Connection) -> Result<(), Box<dyn std::error::Error>> {
    let mut stmt = conn.prepare("SELECT * FROM items")?;
    let rows = stmt.query_map((), |rows| Ok((rows.get::<_, i32>(0)?, rows.get::<_, String>(1)?, rows.get::<_, PrimitiveDateTime>(2)?, rows.get::<_, bool>(3)?)))?;
    println!("{0: <10} | {1: <50} | {2: <20} | {3: <10}", "ID", "Description", "Due Date", "Status");
    let row_vec = rows.map(|rows| rows.unwrap()).collect::<Vec<_>>();
    let now = PrimitiveDateTime::new(OffsetDateTime::now_local()?.date(), OffsetDateTime::now_local()?.time());
    for row in row_vec.iter() {
        let row = row;
        let id = row.0;
        let description = &row.1;
        let due_date = row.2;
        let is_completed = row.3;
        if !is_completed {
            if now < due_date && due_date < now + Duration::days(3) {
                println!("{0: <10} | {1: <50} | {2: <20} | {3: <10}", id.to_string().yellow(), description.yellow(), due_date.to_string().yellow(), is_completed.to_string().yellow());
            } else if now > due_date {
                println!("{0: <10} | {1: <50} | {2: <20} | {3: <10}", id.to_string().red(), description.red(), due_date.to_string().red(), is_completed.to_string().red());
            } else {
                println!("{0: <10} | {1: <50} | {2: <20} | {3: <10}", id, description, due_date, is_completed);
            }
        }
    };
    let overdue_items = &row_vec.iter().filter(|row| row.2 < now).map(|row| row.0).collect::<Vec<_>>()[..];
    let close_items = &row_vec.iter().filter(|row| now + Duration::days(3) > row.2 && row.2 > now).map(|row| row.0).collect::<Vec<_>>()[..];
    if overdue_items.len() > 0 { println!("{} {:?} {}", "Items".red(), slice_to_string(overdue_items).red(), "are overdue! Finish them quickly!".red()); }
    if close_items.len() > 0 { println!("{} {:?} {}", "Items".yellow(), slice_to_string(close_items).yellow(), "are close to deadline! Finish them quickly!".yellow()); }
    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();
    let conn = Connection::open("./todo.db")?;
    conn.execute("CREATE TABLE IF NOT EXISTS items (\
         ID INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,\
         ITEM TEXT NOT NULL UNIQUE,\
         DUE_DATE TEXT,\
         IS_COMPLETED INTEGER NOT NULL\
    )", ())?;
    match cli.command {
        Some(Commands::Add { item, }) => {
            add_item(item, &conn)?;
        }
        Some(Commands::Complete { id }) => {
            conn.execute("UPDATE items SET IS_COMPLETED = 1 WHERE ID = ?", params!(id))?;
        },
        Some(Commands::Remove { id }) => {
            conn.execute("DELETE FROM items WHERE ID = ?", params!(id))?;
        }
        None => {
            list_items(&conn)?;
        }
    }
    Ok(())
}

1 post - 1 participant

Read full topic

🏷️ rust_feed