todomd 0.3.0

A simple markdown-based todo list CLI and TUI - Added Kanban
Documentation
use todomd::core::model::{Category, State};
use todomd::core::ops;
use todomd::core::store::Store;
use todomd::persist::atomic::FileStore;
use todomd::tui;
use todomd::tui::app::ViewMode;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use chrono::{Local, TimeZone};

#[derive(Parser)]
#[command(name = "todomd")]
#[command(about = "A simple markdown-based todo list CLI and TUI", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    /// Launch the interactive TUI (default)
    Tui,
    /// Launch the interactive TUI in Kanban view
    Kanban,
    /// Add a new todo item
    Add {
        category: String,
        title: String,
        #[arg(short, long)]
        notes: Option<String>,
    },
    /// List items in a category
    List {
        category: String,
    },
    /// Edit a todo item by ID
    Edit {
        id: u64,
        #[arg(long)]
        title: Option<String>,
        #[arg(long)]
        notes: Option<String>,
        // clear notes flag?
    },
    /// Move a todo item to a different category
    Move {
        id: u64,
        category: String,
    },
    /// Mark a todo item as completed (moves to Completed)
    Done {
        id: u64,
    },
    /// Undo a completed item (moves to specified category)
    Undo {
        id: u64,
        category: String,
    },
    /// Remove a todo item
    Rm {
        id: u64,
    },
    /// Manage subtasks
    Sub {
        #[command(subcommand)]
        cmd: SubCommands,
    },
    /// Show summary (optional feature)
    Summary,
    /// Reorder a todo item (up/down)
    Reorder {
        id: u64,
        #[arg(long)]
        up: bool,
        #[arg(long)]
        down: bool,
    },
}

#[derive(Subcommand)]
enum SubCommands {
    Add {
        parent_id: u64,
        title: String,
        #[arg(short, long)]
        notes: Option<String>,
    },
    Edit {
        parent_id: u64,
        sub_id: u64,
        #[arg(long)]
        title: Option<String>,
        #[arg(long)]
        done: Option<bool>,
        #[arg(long)]
        notes: Option<String>,
    },
    Rm {
        parent_id: u64,
        sub_id: u64,
    },
    Done {
        parent_id: u64,
        sub_id: u64,
    },
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();
    let path = PathBuf::from("todo.md");
    let db_path = PathBuf::from("todo.parquet");
    
    let file_store = FileStore::new(path);
    let trueno_store = todomd::persist::trueno_store::TruenoStore::new(db_path);
    let store: Box<dyn Store> = Box::new(todomd::persist::dual_store::DualStore::new(file_store, trueno_store));

    match cli.command {
        Some(Commands::Tui) | None => {
            let mut app = tui::app::App::new(store)?;
            app.run()?;
        }
        Some(Commands::Kanban) => {
            let mut app = tui::app::App::new_with_view(store, ViewMode::Kanban)?;
            app.run()?;
        }
        Some(Commands::Add { category, title, notes }) => {
            if let Some(cat) = Category::from_str(&category) {
                let id = ops::add_todo(&store, cat, title, notes, None, None)?;
                println!("Added todo {} to {}", id, cat.as_str());
            } else {
                println!("Invalid category: {}", category);
            }
        }
        Some(Commands::List { category }) => {
            if let Some(cat) = Category::from_str(&category) {
                 let state = store.load()?;
                 let mut todos: Vec<_> = state.get_category(cat).iter().cloned().collect();
                 todos.sort_by(|a, b| b.created_at.cmp(&a.created_at));
                 println!("## {}", cat.as_str());
                 for todo in todos {
                     println!("- [{}] ({}) {}", if cat == Category::Completed { "x" } else { " " }, todo.id, todo.title);
                     let mut sorted_subs = todo.subtasks.clone();
                     sorted_subs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
                     for sub in sorted_subs {
                         println!("  - [{}] ({}) {}", if sub.done { "x" } else { " " }, sub.id, sub.title);
                     }
                 }
            } else {
                println!("Invalid category: {}", category);
            }
        }
        Some(Commands::Edit { id, title, notes }) => {
            ops::edit_todo(&store, id, title, notes, None, None)?;
            println!("Updated todo {}", id);
        }
        Some(Commands::Move { id, category }) => {
            if let Some(cat) = Category::from_str(&category) {
                ops::move_todo(&store, id, cat)?;
                println!("Moved todo {} to {}", id, cat.as_str());
            } else {
                println!("Invalid category: {}", category);
            }
        }
        Some(Commands::Done { id }) => {
             ops::toggle_done(&store, id)?;
             println!("Marked todo {} as done", id);
        }
        Some(Commands::Undo { id, category }) => {
            if let Some(cat) = Category::from_str(&category) {
                ops::move_todo(&store, id, cat)?;
                 println!("Moved todo {} back to {}", id, cat.as_str());
            } else {
                 println!("Invalid category: {}", category);
            }
        }
        Some(Commands::Rm { id }) => {
            ops::remove_todo(&store, id)?;
            println!("Removed todo {}", id);
        }
        Some(Commands::Sub { cmd }) => {
            match cmd {
                SubCommands::Add { parent_id, title, notes } => {
                    let sid = ops::add_subtask(&store, parent_id, title, notes, None, None)?;
                    println!("Added subtask {} to todo {}", sid, parent_id);
                }
                SubCommands::Edit { parent_id, sub_id, title, done, notes } => {
                    ops::edit_subtask(&store, parent_id, sub_id, title, done, notes, None, None)?;
                    println!("Updated subtask {}", sub_id);
                }
                SubCommands::Rm { parent_id, sub_id } => {
                    ops::remove_subtask(&store, parent_id, sub_id)?;
                    println!("Removed subtask {}", sub_id);
                }
                SubCommands::Done { parent_id, sub_id } => {
                    ops::edit_subtask(&store, parent_id, sub_id, None, Some(true), None, None, None)?;
                    println!("Marked subtask {} as done", sub_id);
                }
            }
        }
        Some(Commands::Summary) => {
            let state = store.load()?;
            let now = Local::now().timestamp();
            
            for cat in State::all_categories() {
                let mut todos: Vec<_> = state.get_category(cat).iter().cloned().collect();
                if todos.is_empty() { continue; }
                todos.sort_by(|a, b| b.created_at.cmp(&a.created_at));
                
                println!("\n{} ({})", cat.as_str().to_uppercase(), todos.len());
                for todo in todos {
                    let date_c = Local.timestamp_opt(todo.created_at, 0).unwrap().format("%Y/%m/%d %H:%M:%S");
                    let date_u = Local.timestamp_opt(todo.updated_at, 0).unwrap().format("%Y/%m/%d %H:%M:%S");
                    let age_u_days = (now - todo.updated_at).max(0) / 86400;
                    
                    let color_str = todo.color.as_str();
                    
                    // Recommendation logic (still based on days)
                    let rec = if cat != Category::Completed {
                         if age_u_days > 14 { "rec:red" }
                         else if age_u_days > 7 { "rec:yellow" }
                         else { "" }
                    } else { "" };
                    
                    let rec_str = if !rec.is_empty() { format!("  {}", rec) } else { String::new() };

                    println!("  {:3} [{:7}] c:{} u:{}   {}{}", 
                        todo.id, color_str, date_c, date_u, todo.title, rec_str);
                        
                    // Subtasks summary
                    if !todo.subtasks.is_empty() {
                         let done = todo.subtasks.iter().filter(|s| s.done).count();
                         println!("      subs: {}/{}", done, todo.subtasks.len());
                    }
                }
            }
        }
        Some(Commands::Reorder { id, up, down }) => {
            if up && down {
                println!("Error: Cannot move up and down at the same time");
            } else if !up && !down {
                println!("Error: Specify --up or --down");
            } else {
                ops::reorder_item(&store, id, up)?;
                println!("Reordered item {}", id);
            }
        }
    }
    Ok(())
}