initialise project
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user