Skip to main content

rustodo/
cli.rs

1//! Command-line interface definitions.
2//!
3//! This module uses [`clap`] to define the full CLI surface of `rustodo`:
4//! the top-level [`Cli`] struct, the [`Commands`] enum with one variant per
5//! subcommand, and the [`AddArgs`] helper struct for the `add` subcommand.
6//!
7//! Consumers of the library can use these types to embed `rustodo` commands
8//! in their own applications.
9
10use clap::{Args, Parser, Subcommand};
11
12use crate::models::{DueFilter, Priority, Recurrence, RecurrenceFilter, SortBy, StatusFilter};
13
14/// Top-level CLI entry point.
15///
16/// Parse with [`Cli::parse`] in `main`, then match on [`Cli::command`] to
17/// dispatch to the appropriate handler.
18#[derive(Parser)]
19#[command(name = "rustodo")]
20#[command(author = "github.com/joaofelipegalvao")]
21#[command(version = "2.8.5")]
22#[command(about = "A modern, powerful task manager built with Rust", long_about = None)]
23#[command(after_help = "EXAMPLES:\n    \
24    # Add a task to a project with a natural language date\n    \
25    todo add \"Fix login bug\" --project \"Backend\" --priority high --due \"next friday\"\n\n    \
26    # Add a task due in 3 days\n    \
27    todo add \"Review PR\" --due \"in 3 days\"\n\n    \
28    # Add a task with strict date format\n    \
29    todo add \"Project deadline\" --due 2026-03-15\n\n    \
30    # List all tasks in a project\n    \
31    todo list --project \"Backend\"\n\n    \
32    # List pending tasks in a project, sorted by due date\n    \
33    todo list --project \"Backend\" --status pending --sort due\n\n    \
34    # List all projects\n    \
35    todo projects\n\n    \
36    # Search within a project\n    \
37    todo search \"bug\" --project \"Backend\"\n\n    \
38    # Edit a task project\n    \
39    todo edit 3 --project \"Frontend\"\n\n    \
40    # Remove a task from its project\n    \
41    todo edit 3 --clear-project\n\n    \
42    # Add a recurring task in a project\n    \
43    todo add \"Weekly review\" --project \"Management\" --due \"next monday\" --recurrence weekly\n\n    \
44    # List overdue tasks sorted by due date\n    \
45    todo list --due overdue --sort due\n\n    \
46    # Search for tasks\n    \
47    todo search rust\n\n    \
48    # Mark task as completed (auto-creates next recurrence)\n    \
49    todo done 3\n\n    \
50    # Set recurrence pattern\n    \
51    todo recur 5 daily\n\n\
52For more information, visit: https://github.com/joaofelipegalvao/rustodo
53")]
54pub struct Cli {
55    /// The subcommand to execute.
56    #[command(subcommand)]
57    pub command: Commands,
58}
59
60/// All  available subcommands.
61#[derive(Subcommand)]
62pub enum Commands {
63    /// Add a new task to your todo list
64    #[command(visible_alias = "a")]
65    #[command(long_about = "Add a new task to your todo list\n\n\
66        Creates a new task with the specified text and optional metadata like priority,\n\
67        tags, and due date. Tasks are saved immediately to todos.json.\n\n\
68        Due dates accept both natural language and strict YYYY-MM-DD format:\n  \
69        todo add \"Meeting\" --due tomorrow\n  \
70        todo add \"Deploy\" --due \"next friday\"\n  \
71        todo add \"Report\" --due \"in 3 days\"\n  \
72        todo add \"Deadline\" --due 2026-03-15\n\n\
73        Assign to a project:\n  \
74        todo add \"Fix bug\" --project \"Backend\"\n  \
75        todo add \"Write docs\" --project \"Documentation\" --tag work\n\n\
76        Use --recurrence to make the task repeat automatically when completed.")]
77    Add(AddArgs),
78
79    /// List and filter tasks
80    #[command(visible_alias = "ls")]
81    #[command(
82        long_about = "List and filter tasks with powerful filtering options\n\n\
83        Examples:\n  \
84        todo list --project \"Backend\"\n  \
85        todo list --project \"Backend\" --status pending\n  \
86        todo list --recurrence daily\n  \
87        todo list --status pending --priority high --sort due"
88    )]
89    List {
90        /// Show all, pending or done tasks.
91        #[arg(long, value_enum, default_value_t = StatusFilter::All)]
92        status: StatusFilter,
93        /// Filter by priority level.
94        #[arg(long, value_enum)]
95        priority: Option<Priority>,
96        /// Filter by due-date window
97        #[arg(long, value_enum)]
98        due: Option<DueFilter>,
99        /// Sort results.
100        #[arg(long, short = 's', value_enum)]
101        sort: Option<SortBy>,
102        /// Filter by tag name.
103        #[arg(long, short = 't')]
104        tag: Option<String>,
105        /// Filter by project name (case-insensitive)
106        #[arg(long, short = 'p')]
107        project: Option<String>,
108        /// Filter by recurrence pattern
109        #[arg(long, short = 'r', value_enum)]
110        recurrence: Option<RecurrenceFilter>,
111    },
112
113    /// Mark a task as completed
114    #[command(visible_alias = "complete")]
115    Done {
116        /// 1-based task ID
117        #[arg(value_name = "ID")]
118        id: usize,
119    },
120
121    /// Mark a completed task as pending
122    #[command(visible_alias = "undo")]
123    Undone {
124        /// 1-based task ID
125        #[arg(value_name = "ID")]
126        id: usize,
127    },
128
129    /// Remove a task permanently
130    #[command(visible_aliases = ["rm", "delete"])]
131    Remove {
132        /// 1-based Task ID.
133        #[arg(value_name = "ID")]
134        id: usize,
135        /// Skip the interactive confirmation prompt.
136        #[arg(long, short = 'y')]
137        yes: bool,
138    },
139
140    /// Edit an existing task
141    #[command(visible_alias = "e")]
142    #[command(long_about = "Edit an existing task\n\n\
143        Modify task properties like text, priority, tags, or due date.\n\
144        Only specify the fields you want to change.\n\n\
145        Due dates accept natural language or YYYY-MM-DD:\n  \
146        todo edit 3 --due tomorrow\n  \
147        todo edit 3 --due \"next monday\"\n  \
148        todo edit 3 --due \"in 5 days\"\n  \
149        todo edit 3 --due 2026-04-01\n\n\
150        Tag operations:\n  \
151        todo edit 1 --add-tag urgent,critical     # Add multiple tags\n  \
152        todo edit 1 --remove-tag work,team        # Remove multiple tags\n  \
153        todo edit 1 --add-tag urgent --remove-tag team  # Combine operations\n\n\
154        Project operations:\n  \
155        todo edit 3 --project \"Backend\"   # Assign to a project\n  \
156        todo edit 3 --clear-project         # Remove from project")]
157    Edit {
158        /// 1-based task ID.
159        #[arg(value_name = "ID")]
160        id: usize,
161
162        /// New task description.
163        #[arg(long)]
164        text: Option<String>,
165        /// New priority level.
166        #[arg(long, value_enum)]
167        priority: Option<Priority>,
168        /// Tags to add (comma-separated or repeat flag).
169        #[arg(long, value_delimiter = ',')]
170        add_tag: Vec<String>,
171        /// Tags to remove tags (comma-separated or repeat flag).
172        #[arg(long, value_delimiter = ',')]
173        remove_tag: Vec<String>,
174        /// Assign task to a project.
175        #[arg(long, short = 'p', conflicts_with = "clear_project")]
176        project: Option<String>,
177        /// Remove task from its current project.
178        #[arg(long, conflicts_with = "project")]
179        clear_project: bool,
180        /// New due date - accepts natural language or YYYY-MM-DD.
181        #[arg(long, value_name = "DATE|EXPRESSION")]
182        due: Option<String>,
183        /// Remove the due date.
184        #[arg(long, conflicts_with = "due")]
185        clear_due: bool,
186        /// Remove all tags.
187        #[arg(long, conflicts_with_all = ["add_tag", "remove_tag"])]
188        clear_tags: bool,
189        /// Task IDs to add as dependencies.
190        #[arg(long, value_name = "ID", conflicts_with = "clear_deps")]
191        add_dep: Vec<usize>,
192        /// Task IDs Remove task IDs from dependencies.
193        #[arg(long, value_name = "ID", conflicts_with = "clear_deps")]
194        remove_dep: Vec<usize>,
195        /// Remove all dependencies from this task.
196        #[arg(long, conflicts_with_all = ["add_dep", "remove_dep"])]
197        clear_deps: bool,
198    },
199
200    /// Clear all tasks
201    #[command(visible_alias = "reset")]
202    Clear {
203        /// Skip the interactive confirmation prompt.
204        #[arg(long, short = 'y')]
205        yes: bool,
206    },
207
208    /// Search for tasks by text content
209    #[command(visible_alias = "find")]
210    Search {
211        /// Case-insensitive substring to match against task descriptions.
212        #[arg(value_name = "QUERY")]
213        query: String,
214        /// Narrow results to a specific tag.
215        #[arg(long, short = 't')]
216        tag: Option<String>,
217        /// Narrow results to a specific project.
218        #[arg(long, short = 'p')]
219        project: Option<String>,
220        /// Narrow results by completion status.
221        #[arg(long, value_enum, default_value_t = StatusFilter::All)]
222        status: StatusFilter,
223    },
224
225    /// Show productivity statistics and activity chart.
226    Stats,
227
228    /// List all tags with task counts.
229    Tags,
230
231    /// List all projects with task counts.
232    #[command(long_about = "List all projects used across your tasks\n\n\
233        Shows each project name with the count of pending and completed tasks.\n\n\
234        Use 'todo list --project <NAME>' to see tasks within a specific project.")]
235    Projects,
236
237    /// Show dependency graph for a task.
238    #[command(long_about = "Show the dependency graph for a task\n\n
239        Displays:\n  \
240        • Tasks this task depends on (with completion status)\n  \
241        • Tasks that depend on this one\n  \
242        • Whether the task is currently blocked\n\n\
243        Examples:\n  \
244        todo deps 5\n  \
245        todo deps 1")]
246    Deps {
247        /// 1-based task ID.
248        #[arg(value_name = "ID")]
249        id: usize,
250    },
251
252    /// Show information about data file location.
253    Info,
254
255    /// Set or change recurrence pattern for a task.
256    Recur {
257        /// 1-based task ID.
258        #[arg(value_name = "ID")]
259        id: usize,
260        /// Desired recurrence pattern.
261        #[arg(value_enum)]
262        pattern: Recurrence,
263    },
264
265    /// Remove recurrence pattern from a task.
266    #[command(visible_alias = "norecur")]
267    #[command(long_about = "Remove recurrence pattern from a task\n\n\
268        Stops a task from repeating automatically. The task will remain\n\
269        but won't create new occurrences when completed.\n\n\
270        Aliases: norecur")]
271    ClearRecur {
272        /// 1-based task ID.
273        #[arg(value_name = "ID")]
274        id: usize,
275    },
276}
277
278/// Arguments for the `add` subcommand.
279///
280/// Extracted into its own struct so clap can derive the full argument set
281/// cleanly, and so callers can construct it programmatically in tests.
282#[derive(Args)]
283pub struct AddArgs {
284    /// Task description text.
285    #[arg(value_name = "DESCRIPTION")]
286    pub text: String,
287    /// Priority level (default: medium).
288    #[arg(long, value_enum, default_value_t = Priority::Medium)]
289    pub priority: Priority,
290    /// Tags  to attach (comma-separated or repeat flag).
291    #[arg(long, short = 't', value_name = "TAG", value_delimiter = ',')]
292    pub tag: Vec<String>,
293    /// Project to assign the task to.
294    #[arg(long, short = 'p', value_name = "PROJECT")]
295    pub project: Option<String>,
296    /// Due date — accepts natural language or YYYY-MM-DD.
297    ///
298    /// Examples: `tomorrow`, `"next friday"`, `"in 3 days"`, `2026-03-15`
299    #[arg(long, value_name = "DATE|EXPRESSION")]
300    pub due: Option<String>,
301    /// Recurrence pattern. Requires `--due` to be set.
302    #[arg(long, value_enum)]
303    pub recurrence: Option<Recurrence>,
304    /// Task IDs this task depends on (must be completed first).
305    #[arg(long, value_name = "ID")]
306    pub depends_on: Vec<usize>,
307}