tasks-cli-rs 0.6.1

Markdown-based TODO task management CLI: each task is a Markdown file with YAML front matter
use std::path::PathBuf;

use clap::{Args, Parser, Subcommand};

/// Shared task filters (first-class across list/batch).
#[derive(Args, Debug, Default)]
pub struct FilterArgs {
    /// Only tasks under this subdirectory (project)
    #[arg(long)]
    pub dir: Option<String>,
    /// Filter by status
    #[arg(short, long)]
    pub status: Option<String>,
    /// Filter by tag
    #[arg(short, long)]
    pub tag: Option<String>,
    /// Filter by priority
    #[arg(short, long)]
    pub priority: Option<String>,
    /// Due on or before this date (YYYY-MM-DD)
    #[arg(long)]
    pub due_before: Option<String>,
    /// Due on or after this date (YYYY-MM-DD)
    #[arg(long)]
    pub due_after: Option<String>,
}

impl FilterArgs {
    pub fn is_empty(&self) -> bool {
        self.dir.is_none()
            && self.status.is_none()
            && self.tag.is_none()
            && self.priority.is_none()
            && self.due_before.is_none()
            && self.due_after.is_none()
    }
}

#[derive(Parser)]
#[command(name = "tasks", version, about = "Markdown-based TODO task management CLI")]
pub struct Cli {
    #[command(subcommand)]
    pub command: Command,
}

#[derive(Subcommand)]
pub enum Command {
    /// Interactive workbench: live task view with /commands
    Repl,
    /// Show version and config info
    Info,
    /// Manage task libraries
    Lib {
        #[command(subcommand)]
        command: LibCommand,
    },
    /// Create a new task
    #[command(alias = "add")]
    New {
        title: String,
        /// Longer summary or background (title stays short: it becomes the filename)
        #[arg(short = 'D', long)]
        description: Option<String>,
        /// Priority: low | medium | high | urgent
        #[arg(short, long)]
        priority: Option<String>,
        /// Tags (repeatable)
        #[arg(short, long = "tag")]
        tags: Vec<String>,
        /// Due date (YYYY-MM-DD)
        #[arg(short, long)]
        due: Option<String>,
        /// Subdirectory inside the library to place the task in
        #[arg(long)]
        dir: Option<String>,
        /// Create from a template (see 'tasks template list')
        #[arg(long)]
        template: Option<String>,
    },
    /// Manage task templates
    Template {
        #[command(subcommand)]
        command: TemplateCommand,
    },
    /// List tasks in the active library
    List {
        #[command(flatten)]
        filter: FilterArgs,
        /// Suppress the non-task file warning
        #[arg(short, long)]
        quiet: bool,
    },
    /// Batch-set status on all tasks matching the filters
    Batch {
        /// Target status: todo | in_progress | blocked | in_review | done | cancelled
        new_status: String,
        #[command(flatten)]
        filter: FilterArgs,
        /// Show matched tasks without changing them
        #[arg(long)]
        dry_run: bool,
        /// Skip confirmation
        #[arg(short, long)]
        force: bool,
    },
    /// Move tasks to archive/YYYY-MM/ (default: all done and cancelled tasks)
    Archive {
        #[command(flatten)]
        filter: FilterArgs,
        /// Show what would be archived without moving files
        #[arg(long)]
        dry_run: bool,
        /// Skip confirmation
        #[arg(short, long)]
        force: bool,
    },
    /// Convert plain markdown files into task files (adds front matter)
    Adopt {
        /// Specific files to adopt; scans the whole library when omitted
        paths: Vec<String>,
        /// Adopt everything without prompting
        #[arg(long)]
        all: bool,
        /// Show what would happen without touching files
        #[arg(long)]
        dry_run: bool,
        /// Also rename adopted files per filename_format
        #[arg(long)]
        rename: bool,
    },
    /// Search tasks by text in title and body
    Search {
        query: String,
        /// Filter by status
        #[arg(short, long)]
        status: Option<String>,
        /// Filter by tag
        #[arg(short, long)]
        tag: Option<String>,
    },
    /// Manage tags
    Tag {
        #[command(subcommand)]
        command: TagCommand,
    },
    /// Show task details
    Show { id: String },
    /// Delete a task
    Delete {
        id: String,
        /// Skip confirmation
        #[arg(short, long)]
        force: bool,
    },
    /// Mark a task as started (in_progress)
    Start { id: String },
    /// Mark a task as done
    Done { id: String },
    /// Cancel a task
    Cancel { id: String },
    /// Set an arbitrary status: todo | in_progress | blocked | in_review | done | cancelled
    Status { id: String, status: String },
    /// Modify task fields
    Set {
        id: String,
        #[arg(long)]
        title: Option<String>,
        /// Longer summary or background; pass "none" to clear
        #[arg(short = 'D', long)]
        description: Option<String>,
        /// Priority: low | medium | high | urgent
        #[arg(long)]
        priority: Option<String>,
        /// Due date (YYYY-MM-DD); pass "none" to clear
        #[arg(long)]
        due: Option<String>,
        /// Add a tag (repeatable)
        #[arg(long = "tag-add")]
        tag_add: Vec<String>,
        /// Remove a tag (repeatable)
        #[arg(long = "tag-remove")]
        tag_remove: Vec<String>,
    },
    /// Manage sub-task steps
    Step {
        #[command(subcommand)]
        command: StepCommand,
    },
    /// Edit task body content directly (no editor)
    Body {
        #[command(subcommand)]
        command: BodyCommand,
    },
    /// Show tasks as a Kanban board grouped by status
    Board {
        /// Only include tasks under this subdirectory
        #[arg(long)]
        dir: Option<String>,
        /// Filter by tag
        #[arg(short, long)]
        tag: Option<String>,
        /// Comma-separated status columns (default: todo,in_progress,done)
        #[arg(long)]
        columns: Option<String>,
    },
    /// Tasks due soon
    Remind {
        /// Window like "7d" or "7" (days); default 3d
        #[arg(long)]
        within: Option<String>,
        /// Show only overdue tasks
        #[arg(long)]
        overdue: bool,
        /// One-line summary (for shell prompt / cron)
        #[arg(long)]
        summary: bool,
    },
    /// List overdue tasks
    Overdue,
    /// Task statistics for the active library
    Stats,
    /// Generate shell completions (bash, zsh, fish, elvish, powershell)
    Completions { shell: String },
    /// Rename task files to match the current filename_format
    FixNames {
        /// Only show what would be renamed
        #[arg(long)]
        dry_run: bool,
    },
    /// Open a task file (or the library root with --lib) in an editor
    Edit {
        /// Task id; omit when using --lib
        id: Option<String>,
        /// Editor command to use
        #[arg(long)]
        editor: Option<String>,
        /// Open the library root directory instead of a task
        #[arg(long = "lib")]
        lib: bool,
    },
}

#[derive(Subcommand)]
pub enum TemplateCommand {
    /// List templates (library templates shadow global ones)
    List,
    /// Print a template's content
    Show { name: String },
    /// Create a template and open it in the editor
    New {
        name: String,
        /// Create in the current library instead of globally
        #[arg(long)]
        lib: bool,
        /// Editor command to use
        #[arg(long)]
        editor: Option<String>,
    },
    /// Edit a template
    Edit {
        name: String,
        /// Editor command to use
        #[arg(long)]
        editor: Option<String>,
    },
    /// Remove a template
    Remove { name: String },
}

#[derive(Subcommand)]
pub enum BodyCommand {
    /// Append a line to the task body
    Append {
        id: String,
        /// Text to append, joined with spaces. Prefix with `--` when the text
        /// starts with a dash (e.g. `-- - [ ] item`), or use --stdin.
        text: Vec<String>,
        /// Insert after the line containing this text (must match exactly 1 line)
        #[arg(long)]
        after: Option<String>,
        /// Read text from stdin
        #[arg(long)]
        stdin: bool,
        /// Suppress output
        #[arg(short, long)]
        quiet: bool,
    },
    /// Replace a line in the task body (must match exactly 1 line)
    Replace {
        id: String,
        /// Text that the target line must contain
        old: String,
        /// Replacement text, joined with spaces. Prefix with `--` when it
        /// starts with a dash, or use --stdin.
        new: Vec<String>,
        /// Read replacement text from stdin
        #[arg(long)]
        stdin: bool,
        /// Suppress output
        #[arg(short, long)]
        quiet: bool,
    },
    /// Delete a line from the task body (starts_with match, must match exactly 1)
    Delete {
        id: String,
        /// Prefix that the target line must start with (after trim)
        prefix: String,
        /// Suppress output
        #[arg(short, long)]
        quiet: bool,
    },
}

#[derive(Subcommand)]
pub enum StepCommand {
    /// Show a task's TODO list with step ids
    #[command(alias = "list")]
    Get { id: String },
    /// Add a step (checkbox) to a task's body
    Add { id: String, title: String },
    /// Mark a step as done (s1, s2, ...)
    Done { id: String, step: String },
    /// Remove a step from a task
    Remove { id: String, step: String },
}

#[derive(Subcommand)]
pub enum TagCommand {
    /// Add tags to a task
    Add {
        id: String,
        #[arg(required = true)]
        tags: Vec<String>,
    },
    /// Remove tags from a task
    Remove {
        id: String,
        #[arg(required = true)]
        tags: Vec<String>,
    },
    /// List all tags used in the active library
    List,
}

#[derive(Subcommand)]
pub enum LibCommand {
    /// Register a new library (creates the directory if needed)
    Add { name: String, path: PathBuf },
    /// List all registered libraries
    List,
    /// Switch the active library
    Use { name: String },
    /// Show the currently active library
    Current,
    /// Unregister a library (does not delete files)
    Remove { name: String },
}