initialise project

This commit is contained in:
2026-08-23 13:03:06 +02:00
commit 2dfdf54d42
22 changed files with 3169 additions and 0 deletions
+307
View File
@@ -0,0 +1,307 @@
use chrono::Local;
use clap::{Parser, Subcommand};
use rusqlite::{params, Connection};
use std::io::{self, Write};
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "todo",
version,
about = "A simple SQLite-based todo manager"
)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
/// Add a new todo
Add {
/// Text of the todo
text: String,
},
/// List all todos
List,
/// Delete a todo by number or exact text
Del {
/// Todo number or exact todo text
target: String,
},
}
fn database_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
let home = dirs::home_dir()
.ok_or("Could not determine home directory")?;
let directory = home.join(".sjn").join("todo");
std::fs::create_dir_all(&directory)?;
Ok(directory.join("todo.db"))
}
fn open_database() -> Result<Connection, Box<dyn std::error::Error>> {
let path = database_path()?;
let connection = Connection::open(path)?;
connection.execute(
"
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
added_at TEXT NOT NULL
)
",
[],
)?;
Ok(connection)
}
fn add_todo(
connection: &Connection,
text: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let text = text.trim();
if text.is_empty() {
println!("Error: todo text cannot be empty.");
return Ok(());
}
let timestamp = Local::now().to_rfc3339();
connection.execute(
"INSERT INTO todos (text, added_at) VALUES (?1, ?2)",
params![text, timestamp],
)?;
let id = connection.last_insert_rowid();
println!("Todo #{} added: {}", id, text);
Ok(())
}
fn list_todos(
connection: &Connection,
) -> Result<(), Box<dyn std::error::Error>> {
let mut statement = connection.prepare(
"
SELECT id, text, added_at
FROM todos
ORDER BY id
",
)?;
let todos = statement.query_map([], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
})?;
let mut found = false;
for todo in todos {
let (id, text, added_at) = todo?;
found = true;
println!("#{} {} [{}]", id, text, added_at);
}
if !found {
println!("No todos.");
}
Ok(())
}
fn delete_todo(
connection: &Connection,
target: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let target = target.trim();
if target.is_empty() {
println!("Error: no todo specified.");
return Ok(());
}
// Try interpreting the target as a todo number first.
if let Ok(id) = target.parse::<i64>() {
let affected = connection.execute(
"DELETE FROM todos WHERE id = ?1",
params![id],
)?;
if affected == 0 {
println!("Error: todo #{} does not exist.", id);
} else {
println!("Todo #{} deleted.", id);
}
return Ok(());
}
// Otherwise delete by exact text.
let affected = connection.execute(
"DELETE FROM todos WHERE text = ?1",
params![target],
)?;
if affected == 0 {
println!("Error: no todo with that text exists.");
} else {
println!("Todo deleted: {}", target);
}
Ok(())
}
fn execute_command(
connection: &Connection,
input: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
let input = input.trim();
if input.is_empty() {
return Ok(true);
}
if input.eq_ignore_ascii_case("exit")
|| input.eq_ignore_ascii_case("quit")
{
return Ok(false);
}
if input.eq_ignore_ascii_case("help") {
println!();
println!("Commands:");
println!(" add <text> Add a todo");
println!(" list List all todos");
println!(" del <number> Delete todo by number");
println!(" del <text> Delete todo by exact text");
println!(" help Show this help");
println!(" exit / quit Exit the shell");
println!();
return Ok(true);
}
let args = match shell_words::split(input) {
Ok(args) => args,
Err(error) => {
println!("Error parsing command: {}", error);
return Ok(true);
}
};
if args.is_empty() {
return Ok(true);
}
let command = args[0].to_lowercase();
match command.as_str() {
"add" => {
if args.len() < 2 {
println!("Usage: add <text>");
} else {
let text = args[1..].join(" ");
add_todo(connection, &text)?;
}
}
"list" => {
if args.len() > 1 {
println!("Usage: list");
} else {
list_todos(connection)?;
}
}
"del" | "delete" | "remove" => {
if args.len() < 2 {
println!("Usage: del <number> or del <text>");
} else {
let target = args[1..].join(" ");
delete_todo(connection, &target)?;
}
}
_ => {
println!("Unknown command: {}", args[0]);
println!("Type \"help\" for help.");
}
}
Ok(true)
}
fn shell(
connection: &Connection,
) -> Result<(), Box<dyn std::error::Error>> {
println!("Todo Shell");
println!("Type \"help\" for help.");
println!();
loop {
print!("todo> ");
io::stdout().flush()?;
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(0) => {
println!();
break;
}
Ok(_) => {
if !execute_command(connection, &input)? {
break;
}
}
Err(error) => {
eprintln!("Error reading input: {}", error);
break;
}
}
}
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let connection = open_database()?;
let cli = Cli::parse();
match cli.command {
Some(Command::Add { text }) => {
add_todo(&connection, &text)?;
}
Some(Command::List) => {
list_todos(&connection)?;
}
Some(Command::Del { target }) => {
delete_todo(&connection, &target)?;
}
None => {
shell(&connection)?;
}
}
Ok(())
}