1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
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,
},
/// Manage recurring task rules
Recur {
#[command(subcommand)]
command: RecurCommand,
},
/// 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,
},
/// Append a line to a task's body
#[command(alias = "note")]
Log {
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 a task's body (must match exactly 1 line)
Rewrite {
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 a task's body (starts_with match, must match exactly 1)
Strike {
id: String,
/// Prefix that the target line must start with (after trim)
prefix: String,
/// Suppress output
#[arg(short, long)]
quiet: bool,
},
/// 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 RecurCommand {
/// Create a recurrence rule
#[command(after_help = "Examples:\n \
tasks recur add \"每日站会\" --rule daily --tag daily\n \
tasks recur add \"周报\" --rule weekly:fri --until 2026-12-31\n \
tasks recur add \"月度对账\" --rule monthly:1,15 --max-count 6\n \
tasks recur add \"浇花\" --rule every:3d --start 2026-08-01")]
Add {
/// Title used for every generated task
title: String,
/// Recurrence: daily | weekly:mon,thu | monthly:1,15 | every:3d
#[arg(long)]
rule: String,
/// Longer summary carried onto every generated task
#[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>,
/// First date the rule may fire (YYYY-MM-DD); defaults to today
#[arg(long)]
start: Option<String>,
/// Stop after this date (YYYY-MM-DD)
#[arg(long)]
until: Option<String>,
/// Stop after generating this many tasks
#[arg(long)]
max_count: Option<u64>,
},
/// List recurrence rules
List,
/// Show a rule's details and body template
Show { rule: String },
/// Edit a rule file in an editor (body becomes the task body template)
Edit {
rule: String,
/// Editor command to use
#[arg(long)]
editor: Option<String>,
},
/// Stop a rule from firing without deleting it
Pause { rule: String },
/// Re-enable a paused rule
Resume { rule: String },
/// Delete a rule; tasks it already generated are kept
Remove {
rule: String,
/// Skip confirmation
#[arg(short, long)]
force: bool,
},
/// Generate the tasks that are due now
Tick,
}
#[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 },
}