todo_cli_jfc/
lib.rs

1use ansi_term::Color::{Black, Blue, Green, Red, White};
2use clap::{Parser, Subcommand};
3use core::fmt;
4use home;
5use rusqlite::{params, Connection, Result};
6use std::path::PathBuf;
7
8#[derive(Debug)]
9pub struct Todo {
10    id: i64,
11    description: String,
12    done: bool,
13}
14
15const CHECK: &str = "\u{f058}";
16const CIRCLE: &str = "\u{ebb5}";
17
18impl fmt::Display for Todo {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        let icon: &str = if self.done { CHECK } else { CIRCLE };
21        write!(f, "{} {}  {}", self.id, icon, self.description)
22    }
23}
24
25#[derive(Debug, Parser)]
26#[command(name = "todo")]
27#[command(author, version)]
28#[command(about = "A simple todo cli tool.", long_about = None)]
29pub struct Cli {
30    #[command(subcommand)]
31    command: Commands,
32}
33
34#[derive(Debug, Subcommand)]
35enum Commands {
36    #[command(about = "add a new todo to your database")]
37    Add {
38        #[arg(value_name = "description", long, short)]
39        description: String,
40    },
41    #[command(about = "toggle a todo done or undone with this")]
42    Toggle {
43        #[arg(value_name = "todo nr", long, short)]
44        number: i64,
45    },
46    #[command(about = "sweep away all todos that are done")]
47    Sweep {},
48    #[command(about = "remove a todo from your database")]
49    Remove {
50        #[arg(value_name = "todo nr")]
51        number: i64,
52    },
53    #[command(about = "show all your todos saved to the database")]
54    Show {},
55}
56
57fn add(description: &String, db: &Connection) -> Result<(), Box<dyn std::error::Error>> {
58    const SQL: &str = "
59        INSERT INTO todo 
60        (description,done) 
61        VALUES (?1, ?2)";
62    let params = params![description, "0"];
63    db.execute(SQL, params)?;
64
65    let todo = Todo {
66        id: db.last_insert_rowid(),
67        description: description.to_string(),
68        done: false,
69    };
70
71    println!("{} \n{}", White.on(Green).paint("     Added:    "), todo);
72
73    Ok(())
74}
75
76fn toggle(id: &i64, db: &Connection) -> Result<(), Box<dyn std::error::Error>> {
77    let sql: &str = "
78        update todo 
79        SET done = ((done | 1) - (done & 1)) 
80        WHERE id = ?1";
81    let params = params![id];
82    let result = db.execute(sql, params)?;
83
84    let sql: &str = "SELECT id, description, done FROM todo WHERE id = ?";
85    let mut data = db.prepare(sql)?;
86    let d = data.query_map(params, |row| {
87        Ok(Todo {
88            id: row.get(0)?,
89            description: row.get(1)?,
90            done: row.get(2)?,
91        })
92    })?;
93
94    if result > 0 {
95        println!("{}", White.on(Blue).paint("\n   Todo is toggled!    \n"));
96    } else {
97        println!("{}", Red.paint("No todo with given id found!"));
98    };
99
100    for td in d {
101        println!("{}", td?.to_string());
102    }
103
104    Ok(())
105}
106
107fn sweep(db: &Connection) -> Result<(), Box<dyn std::error::Error>> {
108    const SQL: &str = "
109        DELETE FROM todo
110        WHERE done = 1;
111    ";
112    let result = db.execute(SQL, [])?;
113
114    if result > 0 {
115        print!(
116            "{}{}",
117            Red.bold().paint("Sweeped away "),
118            Red.bold().paint(result.to_string()),
119        );
120        if result == 1 {
121            print!("{}", Red.bold().paint(" done Todo. \n"))
122        } else {
123            print!("{}", Red.bold().paint(" done Todos. \n"))
124        }
125        
126    } else {
127        println!(
128            "{}",
129            Blue.paint("No todos were marked done, so no sweep necessary.")
130        )
131    }
132    Ok(())
133}
134
135fn remove(id: &i64, db: &Connection) -> Result<(), Box<dyn std::error::Error>> {
136    const SQL: &str = "
137        DELETE FROM todo
138        WHERE id = ?";
139    let params = params![id];
140    let result = db.execute(SQL, params)?;
141
142    if result > 0 {
143        println!("{}", Red.bold().paint("Todo removed!"));
144    } else {
145        println!("{}", Red.paint("No todo with given id was found!"));
146    };
147    Ok(())
148}
149
150fn show(db: &Connection) -> Result<(), Box<dyn std::error::Error>> {
151    println!("{}", Black.on(White).bold().paint("\n   Todos:      \n"));
152
153    const SQL: &str = "SELECT id, description, done FROM todo";
154    let mut stmt = db.prepare(SQL)?;
155    let todos = stmt.query_map((), |row| {
156        Ok(Todo {
157            id: row.get(0)?,
158            description: row.get(1)?,
159            done: row.get(2)?,
160        })
161    })?;
162    for td in todos {
163        println!("{}", td?.to_string());
164    }
165    Ok(())
166}
167
168fn create_todo_table(db: &Connection) -> Result<(), Box<dyn std::error::Error>> {
169    const SQL: &str = "
170        CREATE TABLE if not exists todo (
171            id integer primary key,
172            description text not null,
173            category text,
174            done INTEGER NOT NULL DEFAULT 0 CHECK(done IN (0,1))
175        )";
176    db.execute(SQL, ())?;
177    Ok(())
178}
179
180pub fn run() -> Result<(), Box<dyn std::error::Error>> {
181    let cli = <Cli as clap::Parser>::parse();
182    match home::home_dir() {
183        Some(path) if !path.as_os_str().is_empty() => {
184            let mut dir = PathBuf::from(path);
185            dir.push(PathBuf::from("./todos.db"));
186            let db = &Connection::open(dir)?;
187
188            create_todo_table(db)?;
189            match &cli.command {
190                Commands::Add { description } => add(description, db),
191                Commands::Toggle { number } => toggle(number, db),
192                Commands::Remove { number } => remove(number, db),
193                Commands::Show {} => show(db),
194                Commands::Sweep {} => sweep(db),
195            }?;
196        }
197        _ => println!("Unable to get home dir"),
198    }
199    Ok(())
200}