mod store;
mod task;
use std::env;
use std::path::PathBuf;
use store::TaskStore;
use task::Task;
fn db_path() -> PathBuf {
PathBuf::from(".taskman")
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
print_help();
return;
}
let command = args[1].as_str();
let result = match command {
"add" => cmd_add(&args[2..]),
"list" => cmd_list(&args[2..]),
"done" => cmd_set_status(&args[2..], true),
"undone" => cmd_set_status(&args[2..], false),
"show" => cmd_show(&args[2..]),
"delete" => cmd_delete(&args[2..]),
"stats" => cmd_stats(),
"export" => cmd_export(&args[2..]),
"import" => cmd_import(&args[2..]),
"flush" => cmd_flush(),
"help" | "--help" | "-h" => {
print_help();
Ok(())
}
_ => {
eprintln!("Unknown command: {command}");
print_help();
Err("unknown command".to_string())
}
};
if let Err(e) = result {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
fn cmd_add(args: &[String]) -> Result<(), String> {
if args.is_empty() {
return Err("Usage: taskman add \"Task title\" [--desc \"...\"] [--tags t1,t2]".into());
}
let title = &args[0];
let mut description: Option<&str> = None;
let mut tags: Vec<&str> = Vec::new();
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--desc" | "-d" => {
i += 1;
if i < args.len() {
description = Some(&args[i]);
}
}
"--tags" | "-t" => {
i += 1;
if i < args.len() {
tags = args[i].split(',').collect();
}
}
_ => {}
}
i += 1;
}
let task = Task::new(title, description, tags);
let mut store = TaskStore::open(&db_path())?;
let id = store.add_task(task)?;
store.close()?;
println!("Created task {}", &id.to_string()[..8]);
Ok(())
}
fn cmd_list(args: &[String]) -> Result<(), String> {
let mut store = TaskStore::open(&db_path())?;
let tasks = if args.first().map(|s| s.as_str()) == Some("--done") {
store.list_by_status(true)?
} else if args.first().map(|s| s.as_str()) == Some("--pending") {
store.list_by_status(false)?
} else {
store.list_all_tasks()?
};
if tasks.is_empty() {
println!("No tasks found. Add one with: taskman add \"My task\"");
} else {
println!("Tasks ({} total):", tasks.len());
println!("{}", "-".repeat(60));
for task in &tasks {
println!(" {task}");
}
}
store.close()?;
Ok(())
}
fn cmd_set_status(args: &[String], done: bool) -> Result<(), String> {
if args.is_empty() {
return Err("Usage: taskman done <task-id>".into());
}
let id = parse_task_id(&args[0])?;
let mut store = TaskStore::open(&db_path())?;
store.set_task_done(&id, done)?;
store.close()?;
let status = if done { "done" } else { "pending" };
println!("Task {} marked as {status}", &args[0]);
Ok(())
}
fn cmd_show(args: &[String]) -> Result<(), String> {
if args.is_empty() {
return Err("Usage: taskman show <task-id>".into());
}
let id = parse_task_id(&args[0])?;
let mut store = TaskStore::open(&db_path())?;
match store.get_task(&id)? {
Some(task) => {
println!("Task Details");
println!("{}", "=".repeat(40));
println!(" ID: {}", task.id);
println!(" Title: {}", task.title);
println!(
" Description: {}",
task.description.as_deref().unwrap_or("(none)")
);
println!(" Status: {}", if task.done { "Done" } else { "Pending" });
println!(" Created: {}", format_timestamp(task.created_at));
if !task.tags.is_empty() {
println!(" Tags: {}", task.tags.join(", "));
}
}
None => {
println!("Task {} not found", &args[0]);
}
}
store.close()?;
Ok(())
}
fn cmd_delete(args: &[String]) -> Result<(), String> {
if args.is_empty() {
return Err("Usage: taskman delete <task-id>".into());
}
let id = parse_task_id(&args[0])?;
let mut store = TaskStore::open(&db_path())?;
store.delete_task(&id)?;
store.close()?;
println!("Task {} deleted", &args[0]);
Ok(())
}
fn cmd_stats() -> Result<(), String> {
let mut store = TaskStore::open(&db_path())?;
let (total, done, pending) = store.stats()?;
store.close()?;
println!("Task Statistics");
println!("{}", "=".repeat(30));
println!(" Total: {total}");
println!(" Done: {done}");
println!(" Pending: {pending}");
Ok(())
}
fn cmd_export(args: &[String]) -> Result<(), String> {
let mut store = TaskStore::open(&db_path())?;
let data = store.export_tasks()?;
store.close()?;
if let Some(file_path) = args.first() {
std::fs::write(file_path, &data)
.map_err(|e| format!("Failed to write file: {e}"))?;
println!("Exported to {file_path}");
} else {
print!("{data}");
}
Ok(())
}
fn cmd_import(args: &[String]) -> Result<(), String> {
if args.is_empty() {
return Err("Usage: taskman import <file>".into());
}
let data = std::fs::read_to_string(&args[0])
.map_err(|e| format!("Failed to read file: {e}"))?;
let mut store = TaskStore::open(&db_path())?;
let count = store.import_tasks(&data)?;
store.close()?;
println!("Imported {count} tasks");
Ok(())
}
fn cmd_flush() -> Result<(), String> {
let mut store = TaskStore::open(&db_path())?;
store.flush()?;
store.close()?;
println!("Database flushed and WAL checkpointed");
Ok(())
}
fn parse_task_id(input: &str) -> Result<uuid::Uuid, String> {
if let Ok(id) = uuid::Uuid::parse_str(input) {
return Ok(id);
}
if input.len() >= 4 {
let mut store = TaskStore::open(&db_path())?;
let tasks = store.list_all_tasks()?;
store.close()?;
let matches: Vec<&Task> = tasks
.iter()
.filter(|t| t.id.to_string().starts_with(input))
.collect();
match matches.len() {
0 => return Err(format!("No task found matching '{input}'")),
1 => return Ok(matches[0].id),
n => return Err(format!("{n} tasks match '{input}' — use more characters")),
}
}
Err(format!("Invalid task ID: '{input}'"))
}
fn format_timestamp(ts: i64) -> String {
let secs = ts as u64;
let days = secs / 86400;
let years = 1970 + days / 365; let remaining_days = days % 365;
let months = remaining_days / 30 + 1;
let day = remaining_days % 30 + 1;
format!("{years:04}-{months:02}-{day:02}")
}
fn print_help() {
println!(
r#"TaskMan — A task manager powered by GrumpyDB
USAGE:
cargo run --example taskman -- <COMMAND> [OPTIONS]
COMMANDS:
add <title> [--desc "..."] [--tags t1,t2] Add a new task
list [--done | --pending] List tasks
show <id> Show task details
done <id> Mark task as done
undone <id> Mark task as pending
delete <id> Delete a task
stats Show task statistics
export [file] Export tasks (to stdout or file)
import <file> Import tasks from file
flush Flush data + WAL checkpoint
help Show this help
EXAMPLES:
cargo run --example taskman -- add "Buy groceries" --tags shopping
cargo run --example taskman -- list
cargo run --example taskman -- done a3b4c5d6
cargo run --example taskman -- export tasks.bak
cargo run --example taskman -- import tasks.bak
cargo run --example taskman -- flush
cargo run --example taskman -- stats
DATA:
Tasks are stored in .taskman/ in the current directory.
Files: data.db (documents), index.db (B+Tree index), wal.log (Write-Ahead Log)
CRASH SAFETY:
Every write is protected by the Write-Ahead Log (WAL).
If the process crashes, committed data is recovered automatically on next open.
Use 'flush' to force a checkpoint and truncate the WAL.
"#
);
}