use chrono::Datelike;
use clap::{Parser, Subcommand};
use crate::domain::todo::Todo;
use crate::domain::{SortMode, TodoService, TodoStatus};
use crate::error::AppError;
fn pending_todos(todos: Vec<Todo>) -> Vec<Todo> {
todos
.into_iter()
.filter(|t| matches!(t.status, TodoStatus::Pending))
.collect()
}
#[derive(Parser)]
#[command(name = "todo")]
#[command(about = "A minimalist terminal todo manager", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(short, long, help = "List all todos")]
pub list: bool,
#[arg(short = 'j', long, help = "Export todos as JSON")]
pub json: bool,
#[arg(short = 'i', long, help = "Import todos from JSON (stdin)")]
pub import: bool,
}
#[derive(Subcommand)]
pub enum Commands {
#[command(about = "List all todos")]
List,
#[command(about = "Add a new todo", alias = "a")]
Add {
#[arg(help = "Todo title")]
title: String,
#[arg(short = 'c', long, help = "Comment/description")]
comment: Option<String>,
#[arg(short = 'p', long, help = "Priority (S, A, B, C)")]
priority: Option<String>,
},
#[command(about = "Mark todo as done")]
Done {
#[arg(help = "Todo index (1-based)")]
index: usize,
},
#[command(about = "Export todos as JSON")]
Json,
#[command(about = "Delete todo by index")]
Delete {
#[arg(help = "Todo index (1-based)")]
index: usize,
},
#[command(name = "import", hide = true)]
Import,
#[command(about = "Add comment to todo by index")]
Comment {
#[arg(help = "Todo index (1-based)")]
index: usize,
#[arg(help = "Comment text to append")]
text: String,
},
#[command(about = "Export completed todos as JSON")]
Export {
#[arg(long, help = "Export todos completed this week (Monday to now)")]
week: bool,
},
}
pub async fn run_list(service: &TodoService) -> Result<(), AppError> {
let todos = service.list_active_sorted(SortMode::Priority).await?;
let pending = pending_todos(todos);
if pending.is_empty() {
println!("No todos");
return Ok(());
}
for (i, todo) in pending.iter().enumerate() {
let status = "â—‹";
let priority = todo.priority.to_char();
let priority_display = if priority.is_empty() { " " } else { priority };
let title = &todo.title;
let created = todo.created_at.format("%Y-%m-%d %H:%M:%S");
println!(
"{} [{}] {:>1} {} ({})",
i + 1,
status,
priority_display,
title,
created
);
}
Ok(())
}
pub async fn run_add(
service: &TodoService,
title: &str,
comment: Option<&str>,
priority: Option<&str>,
) -> Result<(), AppError> {
service.create(title, "").await?;
let note = comment.unwrap_or("");
let priority_char_str = priority.unwrap_or("");
if !note.is_empty() || !priority_char_str.is_empty() {
let todos = service.list_active_sorted(SortMode::Priority).await?;
if let Some(todo) = todos.iter().find(|t| t.title == title.trim()) {
let note_to_use = if note.is_empty() { &todo.note } else { note };
let prio_to_use = if priority_char_str.is_empty() {
""
} else {
priority_char_str
};
service.update(&todo.id, &todo.title, note_to_use).await?;
if !prio_to_use.is_empty() {
service.set_priority(&todo.id, prio_to_use).await?;
}
}
}
println!("Added: {}", title.trim());
Ok(())
}
pub async fn run_done(service: &TodoService, index: usize) -> Result<(), AppError> {
let todos = service.list_active_sorted(SortMode::Priority).await?;
let pending = pending_todos(todos);
if index == 0 || index > pending.len() {
eprintln!("Error: Invalid index {}", index);
return Ok(());
}
let todo = &pending[index - 1];
service.toggle(&todo.id).await?;
println!("Completed: {}", todo.title);
Ok(())
}
pub async fn run_delete(service: &TodoService, index: usize) -> Result<(), AppError> {
let todos = service.list_active_sorted(SortMode::Priority).await?;
let pending = pending_todos(todos);
if index == 0 || index > pending.len() {
eprintln!("Error: Invalid index {}", index);
return Ok(());
}
let todo = &pending[index - 1];
service.delete(&todo.id).await?;
println!("Deleted: {}", todo.title);
Ok(())
}
pub async fn run_comment(service: &TodoService, index: usize, text: &str) -> Result<(), AppError> {
let todos = service.list_active_sorted(SortMode::Priority).await?;
let pending = pending_todos(todos);
if index == 0 || index > pending.len() {
eprintln!("Error: Invalid index {}", index);
return Ok(());
}
let todo = &pending[index - 1];
let new_note = if todo.note.is_empty() {
text.to_string()
} else {
format!("{}\n{}", todo.note, text)
};
service.update(&todo.id, &todo.title, &new_note).await?;
println!("Comment added to: {}", todo.title);
Ok(())
}
pub async fn run_export_week(service: &TodoService) -> Result<(), AppError> {
let now = chrono::Local::now();
let offset = *now.offset();
let iso = now.date_naive().iso_week();
let monday = chrono::NaiveDate::from_isoywd_opt(iso.year(), iso.week(), chrono::Weekday::Mon)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
.and_utc()
.with_timezone(&offset);
let todos = service.list_completed_since(monday).await?;
let json =
serde_json::to_string_pretty(&todos).map_err(|e| AppError::Validation(e.to_string()))?;
println!("{}", json);
Ok(())
}
pub async fn run_json_export(service: &TodoService) -> Result<(), AppError> {
let todos = service.list_active_sorted(SortMode::Priority).await?;
let json =
serde_json::to_string_pretty(&todos).map_err(|e| AppError::Validation(e.to_string()))?;
println!("{}", json);
Ok(())
}
pub async fn run_json_import(service: &TodoService) -> Result<(), AppError> {
use std::io::Read;
let mut input = String::new();
std::io::stdin()
.read_to_string(&mut input)
.map_err(|e| AppError::Validation(e.to_string()))?;
let todos: Vec<crate::entity::todo_item::Model> =
serde_json::from_str(&input).map_err(|e| AppError::Validation(e.to_string()))?;
let count = todos.len();
for todo in todos {
service.create(&todo.title, "").await?;
let todos = service.list_active_sorted(SortMode::Priority).await?;
if let Some(created) = todos.iter().find(|t| t.title == todo.title) {
if !todo.note.is_empty() {
service
.update(&created.id, &created.title, &todo.note)
.await?;
}
if todo.priority != 5 {
let prio_char = match todo.priority {
1 => "S",
2 => "A",
3 => "B",
4 => "C",
_ => "",
};
if !prio_char.is_empty() {
service.set_priority(&created.id, prio_char).await?;
}
}
if todo.status == TodoStatus::COMPLETED {
service.toggle(&created.id).await?;
}
}
}
println!("Imported {} todos", count);
Ok(())
}