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}