na 1.2.87 → 1.2.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.cursor/commands/changelog.md +3 -1
- data/.rubocop_todo.yml +24 -16
- data/CHANGELOG.md +75 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +4 -1
- data/README.md +322 -45
- data/bin/commands/find.rb +62 -0
- data/bin/commands/next.rb +65 -0
- data/bin/commands/plugin.rb +289 -0
- data/bin/commands/tagged.rb +63 -0
- data/bin/commands/update.rb +169 -85
- data/bin/na +1 -0
- data/lib/na/action.rb +19 -0
- data/lib/na/actions.rb +3 -2
- data/lib/na/next_action.rb +105 -7
- data/lib/na/plugins.rb +564 -0
- data/lib/na/string.rb +6 -4
- data/lib/na/version.rb +1 -1
- data/lib/na.rb +1 -0
- data/na/Test.todo.markdown +32 -0
- data/na/test.md +21 -0
- data/na.gemspec +1 -0
- data/plugins.md +38 -0
- data/src/_README.md +240 -44
- metadata +20 -1
data/README.md
CHANGED
|
@@ -9,7 +9,47 @@
|
|
|
9
9
|
_If you're one of the rare people like me who find this useful, feel free to
|
|
10
10
|
[buy me some coffee][donate]._
|
|
11
11
|
|
|
12
|
-
The current version of `na` is 1.2.
|
|
12
|
+
The current version of `na` is 1.2.89.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Table of contents
|
|
16
|
+
|
|
17
|
+
- [Installation](#installation)
|
|
18
|
+
- [Optional Dependencies](#optional-dependencies)
|
|
19
|
+
- [Features](#features)
|
|
20
|
+
- [Easy matching](#easy-matching)
|
|
21
|
+
- [Recursion](#recursion)
|
|
22
|
+
- [Adding todos](#adding-todos)
|
|
23
|
+
- [Updating todos](#updating-todos)
|
|
24
|
+
- [Terminology](#terminology)
|
|
25
|
+
- [Usage](#usage)
|
|
26
|
+
- [Commands](#commands)
|
|
27
|
+
- [add](#add)
|
|
28
|
+
- [edit](#edit)
|
|
29
|
+
- [find](#find)
|
|
30
|
+
- [init, create](#init-create)
|
|
31
|
+
- [move](#move)
|
|
32
|
+
- [next, show](#next-show)
|
|
33
|
+
- [plugin](#plugin)
|
|
34
|
+
- [projects](#projects)
|
|
35
|
+
- [saved](#saved)
|
|
36
|
+
- [scan](#scan)
|
|
37
|
+
- [tagged](#tagged)
|
|
38
|
+
- [todos](#todos)
|
|
39
|
+
- [update](#update)
|
|
40
|
+
- [changelog](#changelog)
|
|
41
|
+
- [complete](#complete)
|
|
42
|
+
- [archive](#archive)
|
|
43
|
+
- [tag](#tag)
|
|
44
|
+
- [undo](#undo)
|
|
45
|
+
- [Configuration](#configuration)
|
|
46
|
+
- [Working with a single global file](#working-with-a-single-global-file)
|
|
47
|
+
- [Add tasks at the end of a project](#add-tasks-at-the-end-of-a-project)
|
|
48
|
+
- [Prompt Hooks](#prompt-hooks)
|
|
49
|
+
- [Time tracking](#time-tracking)
|
|
50
|
+
- [Plugins](#plugins)
|
|
51
|
+
- [Changelog](#changelog)
|
|
52
|
+
|
|
13
53
|
|
|
14
54
|
`na` ("next action") is a command line tool designed to make it easy to see what your next actions are for any project, right from the command line. It works with TaskPaper-formatted files (but any plain text format will do), looking for `@na` tags (or whatever you specify) in todo files in your current folder.
|
|
15
55
|
|
|
@@ -76,7 +116,7 @@ SYNOPSIS
|
|
|
76
116
|
na [global options] command [command options] [arguments...]
|
|
77
117
|
|
|
78
118
|
VERSION
|
|
79
|
-
1.2.
|
|
119
|
+
1.2.89
|
|
80
120
|
|
|
81
121
|
GLOBAL OPTIONS
|
|
82
122
|
-a, --add - Add a next action (deprecated, for backwards compatibility)
|
|
@@ -112,6 +152,7 @@ COMMANDS
|
|
|
112
152
|
move - Move an existing action to a different section
|
|
113
153
|
next, show - Show next actions
|
|
114
154
|
open - Open a todo file in the default editor
|
|
155
|
+
plugin - Manage and run plugins
|
|
115
156
|
projects - Show list of projects for a file
|
|
116
157
|
prompt - Show or install prompt hooks for the current shell
|
|
117
158
|
restore, unfinish - Find and remove @done tag from an action
|
|
@@ -224,15 +265,19 @@ DESCRIPTION
|
|
|
224
265
|
|
|
225
266
|
COMMAND OPTIONS
|
|
226
267
|
-d, --depth=DEPTH - Recurse to depth (default: none)
|
|
268
|
+
--divider=STRING - Divider string for text IO (default: none)
|
|
227
269
|
--[no-]done - Include @done actions
|
|
228
270
|
-e, --regex - Interpret search pattern as regular expression
|
|
229
271
|
--human - Format durations in human-friendly form
|
|
230
272
|
--in=TODO_PATH - Show actions from a specific todo file in history. May use wildcards (* and ?) (default: none)
|
|
273
|
+
--input=TYPE - Plugin input format (json|yaml|csv|text) (default: none)
|
|
231
274
|
--nest - Output actions nested by file
|
|
232
275
|
--no_file - No filename in output
|
|
233
276
|
--[no-]notes - Include notes in output
|
|
234
277
|
-o, --or - Combine search tokens with OR, displaying actions matching ANY of the terms
|
|
235
278
|
--omnifocus - Output actions nested by file and project
|
|
279
|
+
--output=TYPE - Plugin output format (json|yaml|csv|text) (default: none)
|
|
280
|
+
--plugin=NAME - Run a plugin on results (STDOUT only; no file writes) (default: none)
|
|
236
281
|
--proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
|
|
237
282
|
--save=TITLE - Save this search for future use (default: none)
|
|
238
283
|
--[no-]search_notes - Include notes in search (default: enabled)
|
|
@@ -338,12 +383,14 @@ DESCRIPTION
|
|
|
338
383
|
COMMAND OPTIONS
|
|
339
384
|
--all - Show next actions from all known todo files (in any directory)
|
|
340
385
|
-d, --depth=DEPTH - Recurse to depth (default: none)
|
|
386
|
+
--divider=STRING - Divider string for text IO (default: none)
|
|
341
387
|
--[no-]done - Include @done actions
|
|
342
388
|
--exact - Search query is exact text match (not tokens)
|
|
343
389
|
--file=TODO_FILE - Display matches from specific todo file ([relative] path) (default: none)
|
|
344
390
|
--hidden - Include hidden directories while traversing
|
|
345
391
|
--human - Format durations in human-friendly form
|
|
346
392
|
--in, --todo=TODO - Display matches from a known todo file anywhere in history (short name) (may be used more than once, default: none)
|
|
393
|
+
--input=TYPE - Plugin input format (json|yaml|csv|text) (default: none)
|
|
347
394
|
--json_times - Output times as JSON object (implies --times and --done)
|
|
348
395
|
--nest - Output actions nested by file
|
|
349
396
|
--no_file - No filename in output
|
|
@@ -351,7 +398,9 @@ COMMAND OPTIONS
|
|
|
351
398
|
--omnifocus - Output actions nested by file and project
|
|
352
399
|
--only_timed - Show only actions that have a duration (@started and @done)
|
|
353
400
|
--only_times - Output only elapsed time totals (implies --times and --done)
|
|
401
|
+
--output=TYPE - Plugin output format (json|yaml|csv|text) (default: none)
|
|
354
402
|
-p, --prio, --priority=PRIORITY - Match actions with priority, allows <>= comparison (may be used more than once, default: none)
|
|
403
|
+
--plugin=NAME - Run a plugin on results (STDOUT only; no file writes) (default: none)
|
|
355
404
|
--proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
|
|
356
405
|
--regex - Search query is regular expression
|
|
357
406
|
--save=TITLE - Save this search for future use (default: none)
|
|
@@ -373,6 +422,121 @@ EXAMPLES
|
|
|
373
422
|
na next marked
|
|
374
423
|
```
|
|
375
424
|
|
|
425
|
+
##### plugin
|
|
426
|
+
|
|
427
|
+
Manage and run external plugins. See also the Plugins section below.
|
|
428
|
+
|
|
429
|
+
```
|
|
430
|
+
NAME
|
|
431
|
+
plugin - Manage and run plugins
|
|
432
|
+
|
|
433
|
+
SYNOPSIS
|
|
434
|
+
|
|
435
|
+
na [global options] plugin [command options]
|
|
436
|
+
|
|
437
|
+
na [global options] plugin [command options] disable NAME
|
|
438
|
+
|
|
439
|
+
na [global options] plugin [command options] edit NAME
|
|
440
|
+
|
|
441
|
+
na [global options] plugin [command options] enable NAME
|
|
442
|
+
|
|
443
|
+
na [global options] plugin [command options] list [--type TYPE|-t TYPE]
|
|
444
|
+
|
|
445
|
+
na [global options] plugin [command options] new [--language LANG|--lang LANG] NAME
|
|
446
|
+
|
|
447
|
+
na [global options] plugin [command options] run [--divider STRING] [--done] [--file PATH|--in PATH] [--input TYPE] [--output TYPE] [--search QUERY|--find QUERY|--grep QUERY] [--tagged TAG] [-d DEPTH|--depth DEPTH] NAME
|
|
448
|
+
|
|
449
|
+
COMMAND OPTIONS
|
|
450
|
+
--[no-]generate-examples - Regenerate sample plugins and README
|
|
451
|
+
|
|
452
|
+
COMMANDS
|
|
453
|
+
<default> -
|
|
454
|
+
disable, d - Disable an enabled plugin
|
|
455
|
+
edit - Edit an existing plugin
|
|
456
|
+
enable, e - Enable a disabled plugin
|
|
457
|
+
list, ls - List available plugins
|
|
458
|
+
new, n - Create a new plugin
|
|
459
|
+
run, x - Run a plugin on selected actions
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
###### plugin new
|
|
463
|
+
|
|
464
|
+
Create a new plugin script (aliases: `n`). Infers shebang by extension or `--language`.
|
|
465
|
+
|
|
466
|
+
```
|
|
467
|
+
NAME
|
|
468
|
+
new - Create a new plugin
|
|
469
|
+
|
|
470
|
+
SYNOPSIS
|
|
471
|
+
|
|
472
|
+
na [global options] plugin new [command options] NAME
|
|
473
|
+
|
|
474
|
+
COMMAND OPTIONS
|
|
475
|
+
--language, --lang=LANG - Language/ext (e.g. rb, py, /usr/bin/env bash) (default: none)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
###### plugin edit
|
|
479
|
+
|
|
480
|
+
Open an existing plugin in your default editor. Prompts if no name is given.
|
|
481
|
+
|
|
482
|
+
```
|
|
483
|
+
NAME
|
|
484
|
+
edit - Edit an existing plugin
|
|
485
|
+
|
|
486
|
+
SYNOPSIS
|
|
487
|
+
|
|
488
|
+
na [global options] plugin edit NAME
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
###### plugin run
|
|
492
|
+
|
|
493
|
+
Run a plugin on selected actions (aliases: `x`). Supports input/output format flags and filters.
|
|
494
|
+
|
|
495
|
+
```
|
|
496
|
+
NAME
|
|
497
|
+
run - Run a plugin on selected actions
|
|
498
|
+
|
|
499
|
+
SYNOPSIS
|
|
500
|
+
|
|
501
|
+
na [global options] plugin run [command options] NAME
|
|
502
|
+
|
|
503
|
+
COMMAND OPTIONS
|
|
504
|
+
-d, --depth=DEPTH - Search for files X directories deep (default: 1)
|
|
505
|
+
--divider=STRING - Text divider when using --input/--output text (default: none)
|
|
506
|
+
--[no-]done - Include @done actions
|
|
507
|
+
--file, --in=PATH - Specify the file to search for the task (default: none)
|
|
508
|
+
--input=TYPE - Input format (json|yaml|csv|text) (default: none)
|
|
509
|
+
--output=TYPE - Output format (json|yaml|csv|text) (default: none)
|
|
510
|
+
--search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
|
|
511
|
+
--tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
###### plugin enable
|
|
515
|
+
|
|
516
|
+
Move a plugin from `plugins_disabled` to `plugins` (alias: `e`).
|
|
517
|
+
|
|
518
|
+
```
|
|
519
|
+
NAME
|
|
520
|
+
enable - Enable a disabled plugin
|
|
521
|
+
|
|
522
|
+
SYNOPSIS
|
|
523
|
+
|
|
524
|
+
na [global options] plugin enable NAME
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
###### plugin disable
|
|
528
|
+
|
|
529
|
+
Move a plugin from `plugins` to `plugins_disabled` (alias: `d`).
|
|
530
|
+
|
|
531
|
+
```
|
|
532
|
+
NAME
|
|
533
|
+
disable - Disable an enabled plugin
|
|
534
|
+
|
|
535
|
+
SYNOPSIS
|
|
536
|
+
|
|
537
|
+
na [global options] plugin disable NAME
|
|
538
|
+
```
|
|
539
|
+
|
|
376
540
|
##### projects
|
|
377
541
|
|
|
378
542
|
List all projects in a file. If arguments are provided, they're used to match a todo file from history, otherwise the todo file(s) in the current directory will be used.
|
|
@@ -499,10 +663,12 @@ DESCRIPTION
|
|
|
499
663
|
|
|
500
664
|
COMMAND OPTIONS
|
|
501
665
|
-d, --depth=DEPTH - Recurse to depth (default: 1)
|
|
666
|
+
--divider=STRING - Divider string for text IO (default: none)
|
|
502
667
|
--[no-]done - Include @done actions
|
|
503
668
|
--exact - Search query is exact text match (not tokens)
|
|
504
669
|
--human - Format durations in human-friendly form
|
|
505
670
|
--in=TODO_PATH - Show actions from a specific todo file in history. May use wildcards (* and ?) (default: none)
|
|
671
|
+
--input=TYPE - Plugin input format (json|yaml|csv|text) (default: none)
|
|
506
672
|
--json_times - Output times as JSON object (implies --times and --done)
|
|
507
673
|
--nest - Output actions nested by file
|
|
508
674
|
--no_file - No filename in output
|
|
@@ -511,6 +677,8 @@ COMMAND OPTIONS
|
|
|
511
677
|
--omnifocus - Output actions nested by file and project
|
|
512
678
|
--only_timed - Show only actions that have a duration (@started and @done)
|
|
513
679
|
--only_times - Output only elapsed time totals (implies --times and --done)
|
|
680
|
+
--output=TYPE - Plugin output format (json|yaml|csv|text) (default: none)
|
|
681
|
+
--plugin=NAME - Run a plugin on results (STDOUT only; no file writes) (default: none)
|
|
514
682
|
--proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
|
|
515
683
|
--regex - Search query is regular expression
|
|
516
684
|
--save=TITLE - Save this search for future use (default: none)
|
|
@@ -609,6 +777,7 @@ COMMAND OPTIONS
|
|
|
609
777
|
--at=POSITION - When moving task, add at [s]tart or [e]nd of target project (default: none)
|
|
610
778
|
-d, --depth=DEPTH - Search for files X directories deep (default: 1)
|
|
611
779
|
--delete - Delete an action
|
|
780
|
+
--divider=STRING - Divider string for text IO (default: none)
|
|
612
781
|
--[no-]done - Include @done actions
|
|
613
782
|
--duration=DURATION - Duration (e.g. 45m, 2h, 1d2h30m, or minutes) (default: none)
|
|
614
783
|
-e, --regex - Interpret search pattern as regular expression
|
|
@@ -617,9 +786,12 @@ COMMAND OPTIONS
|
|
|
617
786
|
-f, --finish - Add a @done tag to action
|
|
618
787
|
--file=PATH - Specify the file to search for the task (default: none)
|
|
619
788
|
--in, --todo=TODO_FILE - Use a known todo file, partial matches allowed (default: none)
|
|
789
|
+
--input=TYPE - Plugin input format (json|yaml|csv|text) (default: none)
|
|
620
790
|
-n, --note - Prompt for additional notes. Input will be appended to any existing note. If STDIN input (piped) is detected, it will be used as a note.
|
|
621
791
|
-o, --overwrite - Overwrite note instead of appending
|
|
792
|
+
--output=TYPE - Plugin output format (json|yaml|csv|text) (default: none)
|
|
622
793
|
-p, --priority=PRIO - Add/change a priority level 1-5 (default: 0)
|
|
794
|
+
--plugin=NAME - Run a plugin by name on selected actions (default: none)
|
|
623
795
|
--proj, --project=PROJECT[/SUBPROJECT] - Affect actions from a specific project (default: none)
|
|
624
796
|
-r, --remove=TAG - Remove a tag from the action, use multiple times or combine multiple tags with a comma, wildcards (* and ?) allowed (may be used more than once, default: none)
|
|
625
797
|
--replace=TEXT - Use with --find to find and replace with new text. Enables --exact when used (default: none)
|
|
@@ -644,49 +816,6 @@ EXAMPLES
|
|
|
644
816
|
na update --archive My cool action
|
|
645
817
|
```
|
|
646
818
|
|
|
647
|
-
#### Time tracking
|
|
648
|
-
|
|
649
|
-
`na` supports tracking elapsed time between a start and finish for actions using `@started(YYYY-MM-DD HH:MM)` and `@done(YYYY-MM-DD HH:MM)` tags. Durations are not stored; they are calculated on the fly from these tags.
|
|
650
|
-
|
|
651
|
-
- Add/Finish/Update flags:
|
|
652
|
-
- `--started TIME` set a start time when creating or finishing an item
|
|
653
|
-
- `--end TIME` (alias `--finished`) set a done time
|
|
654
|
-
- `--duration DURATION` backfill start time from the provided end time
|
|
655
|
-
- All flags accept natural language (via Chronic) and shorthand: `30m ago`, `-2h`, `2h30m`, `2:30 ago`, `yesterday 5pm`
|
|
656
|
-
|
|
657
|
-
Examples:
|
|
658
|
-
|
|
659
|
-
```bash
|
|
660
|
-
na add --started "30 minutes ago" "Investigate bug"
|
|
661
|
-
na complete --finished now --duration 2h30m "Investigate bug"
|
|
662
|
-
na update --started "yesterday 3pm" --end "yesterday 5:15pm" "Investigate bug"
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
- Display flags (next/tagged):
|
|
666
|
-
- `--times` show per???action durations and a grand total (implies `--done`)
|
|
667
|
-
- `--human` format durations as human???readable text instead of `DD:HH:MM:SS`
|
|
668
|
-
- `--only_timed` show only actions that have both `@started` and `@done` (implies `--times --done`)
|
|
669
|
-
- `--only_times` output only the totals section (no action lines; implies `--times --done`)
|
|
670
|
-
- `--json_times` output a JSON object with timed items, per???tag totals, and overall total (implies `--times --done`)
|
|
671
|
-
|
|
672
|
-
Example outputs:
|
|
673
|
-
|
|
674
|
-
```bash
|
|
675
|
-
# Per???action durations appended and totals table
|
|
676
|
-
na next --times --human
|
|
677
|
-
|
|
678
|
-
# Only totals table (Markdown), no action lines
|
|
679
|
-
na tagged "tag*=bug" --only_times
|
|
680
|
-
|
|
681
|
-
# JSON for scripting
|
|
682
|
-
na next --json_times > times.json
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
Notes:
|
|
686
|
-
|
|
687
|
-
- Any newly added or edited action text is scanned for natural???language values in `@started(...)`/`@done(...)` and normalized to `YYYY???MM???DD HH:MM`.
|
|
688
|
-
- The color of durations in output is configurable via the theme key `duration` (defaults to `{y}`).
|
|
689
|
-
|
|
690
819
|
##### changelog
|
|
691
820
|
|
|
692
821
|
View recent changes with `na changelog` or `na changes`.
|
|
@@ -898,6 +1027,154 @@ If you're using a single global file, you'll need `--cwd_as` to be `tag` or `pro
|
|
|
898
1027
|
|
|
899
1028
|
After installing a hook, you'll need to close your terminal and start a new session to initialize the new commands.
|
|
900
1029
|
|
|
1030
|
+
### Time tracking
|
|
1031
|
+
|
|
1032
|
+
`na` supports tracking elapsed time between a start and finish for actions using `@started(YYYY-MM-DD HH:MM)` and `@done(YYYY-MM-DD HH:MM)` tags. Durations are not stored; they are calculated on the fly from these tags.
|
|
1033
|
+
|
|
1034
|
+
- Add/Finish/Update flags:
|
|
1035
|
+
- `--started TIME` set a start time when creating or finishing an item
|
|
1036
|
+
- `--end TIME` (alias `--finished`) set a done time
|
|
1037
|
+
- `--duration DURATION` backfill start time from the provided end time
|
|
1038
|
+
- All flags accept natural language (via Chronic) and shorthand: `30m ago`, `-2h`, `2h30m`, `2:30 ago`, `yesterday 5pm`
|
|
1039
|
+
|
|
1040
|
+
Examples:
|
|
1041
|
+
|
|
1042
|
+
```bash
|
|
1043
|
+
na add --started "30 minutes ago" "Investigate bug"
|
|
1044
|
+
na complete --finished now --duration 2h30m "Investigate bug"
|
|
1045
|
+
na update --started "yesterday 3pm" --end "yesterday 5:15pm" "Investigate bug"
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
- Display flags (next/tagged):
|
|
1049
|
+
- `--times` show per???action durations and a grand total (implies `--done`)
|
|
1050
|
+
- `--human` format durations as human???readable text instead of `DD:HH:MM:SS`
|
|
1051
|
+
- `--only_timed` show only actions that have both `@started` and `@done` (implies `--times --done`)
|
|
1052
|
+
- `--only_times` output only the totals section (no action lines; implies `--times --done`)
|
|
1053
|
+
- `--json_times` output a JSON object with timed items, per???tag totals, and overall total (implies `--times --done`)
|
|
1054
|
+
|
|
1055
|
+
Example outputs:
|
|
1056
|
+
|
|
1057
|
+
```bash
|
|
1058
|
+
# Per???action durations appended and totals table
|
|
1059
|
+
na next --times --human
|
|
1060
|
+
|
|
1061
|
+
# Only totals table (Markdown), no action lines
|
|
1062
|
+
na tagged "tag*=bug" --only_times
|
|
1063
|
+
|
|
1064
|
+
# JSON for scripting
|
|
1065
|
+
na next --json_times > times.json
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
Notes:
|
|
1069
|
+
|
|
1070
|
+
- Any newly added or edited action text is scanned for natural???language values in `@started(...)`/`@done(...)` and normalized to `YYYY???MM???DD HH:MM`.
|
|
1071
|
+
- The color of durations in output is configurable via the theme key `duration` (defaults to `{y}`).
|
|
1072
|
+
|
|
1073
|
+
### Plugins
|
|
1074
|
+
|
|
1075
|
+
NA supports a plugin system that allows you to run external scripts to transform or process actions. Plugins are stored in `~/.local/share/na/plugins` and can be written in any language with a shebang.
|
|
1076
|
+
|
|
1077
|
+
#### Getting Started
|
|
1078
|
+
|
|
1079
|
+
The first time NA runs, it will create the plugins directory with a README and two sample plugins:
|
|
1080
|
+
- `Add Foo.py` - Adds a `@foo` tag with a timestamp
|
|
1081
|
+
- `Add Bar.sh` - Adds a `@bar` tag
|
|
1082
|
+
|
|
1083
|
+
You can delete or modify these sample plugins as needed.
|
|
1084
|
+
|
|
1085
|
+
#### Running Plugins
|
|
1086
|
+
|
|
1087
|
+
You can manage and run plugins using subcommands under `na plugin`:
|
|
1088
|
+
|
|
1089
|
+
- `new`/`n`: scaffold a new plugin script
|
|
1090
|
+
- `edit`: open an existing plugin
|
|
1091
|
+
- `run`/`x`: run a plugin against selected actions
|
|
1092
|
+
- `enable`/`e`: move from disabled to enabled
|
|
1093
|
+
- `disable`/`d`: move from enabled to disabled
|
|
1094
|
+
|
|
1095
|
+
Plugins are executed with actions on STDIN and must return actions on STDOUT. Display commands can still pipe through plugins via `--plugin`, which only affects STDOUT (no writes).
|
|
1096
|
+
|
|
1097
|
+
#### Plugin Metadata
|
|
1098
|
+
|
|
1099
|
+
Plugins can specify their behavior in a metadata block after the shebang:
|
|
1100
|
+
|
|
1101
|
+
```bash
|
|
1102
|
+
#!/usr/bin/env python3
|
|
1103
|
+
# name: My Plugin
|
|
1104
|
+
# input: json
|
|
1105
|
+
# output: json
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
Available metadata keys (case-insensitive):
|
|
1109
|
+
- `input`: Input format (`json`, `yaml`, `csv`, `text`)
|
|
1110
|
+
- `output`: Output format
|
|
1111
|
+
- `name` or `title`: Display name (defaults to filename)
|
|
1112
|
+
|
|
1113
|
+
#### Input/Output Formats
|
|
1114
|
+
|
|
1115
|
+
Plugins accept and return action data. Use `--input` and `--output` flags to override metadata:
|
|
1116
|
+
|
|
1117
|
+
```bash
|
|
1118
|
+
na plugin MY_PLUGIN --input text --output json --divider "||"
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
**JSON/YAML Schema:**
|
|
1122
|
+
```json
|
|
1123
|
+
[
|
|
1124
|
+
{
|
|
1125
|
+
"file_path": "todo.taskpaper",
|
|
1126
|
+
"line": 15,
|
|
1127
|
+
"parents": ["Project", "Subproject"],
|
|
1128
|
+
"text": "- Action text @tag(value)",
|
|
1129
|
+
"note": "Note content",
|
|
1130
|
+
"tags": [
|
|
1131
|
+
{ "name": "tag", "value": "value" }
|
|
1132
|
+
]
|
|
1133
|
+
}
|
|
1134
|
+
]
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
**Text Format:**
|
|
1138
|
+
```
|
|
1139
|
+
ACTION||ARGS||file_path:line||parents||text||note||tags
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
Default divider is `||` (configurable with `--divider`).
|
|
1143
|
+
- `parents`: `Parent>Child>Leaf`
|
|
1144
|
+
- `tags`: `name(value);name;other(value)`
|
|
1145
|
+
|
|
1146
|
+
If the first token isn???t a known action, it???s treated as `file_path:line` and the action defaults to UPDATE.
|
|
1147
|
+
|
|
1148
|
+
#### Actions
|
|
1149
|
+
|
|
1150
|
+
Plugins may return an optional ACTION with arguments. Supported (case-insensitive):
|
|
1151
|
+
- UPDATE (default; replace text/note/tags/parents)
|
|
1152
|
+
- DELETE
|
|
1153
|
+
- COMPLETE/FINISH
|
|
1154
|
+
- RESTORE/UNFINISH
|
|
1155
|
+
- ARCHIVE
|
|
1156
|
+
- ADD_TAG (args: one or more tags)
|
|
1157
|
+
- DELETE_TAG/REMOVE_TAG (args: one or more tags)
|
|
1158
|
+
- MOVE (args: target project path)
|
|
1159
|
+
|
|
1160
|
+
#### Plugin Behavior
|
|
1161
|
+
|
|
1162
|
+
**On `update` or `plugin` command:**
|
|
1163
|
+
- Plugins can modify text, notes, tags, and parents
|
|
1164
|
+
- Changing `parents` will move the action to the new project location
|
|
1165
|
+
- `file_path` and `line` cannot be changed
|
|
1166
|
+
|
|
1167
|
+
**On display commands (`next`, `tagged`, `find`):**
|
|
1168
|
+
- Plugins only transform STDOUT (no file writes)
|
|
1169
|
+
- Use returned text/note/tags/parents for rendering
|
|
1170
|
+
- Parent changes affect display but not file structure
|
|
1171
|
+
|
|
1172
|
+
#### Override Formats
|
|
1173
|
+
|
|
1174
|
+
You can override plugin defaults with flags on any command that supports `--plugin`:
|
|
1175
|
+
```bash
|
|
1176
|
+
na next --plugin FOO --input csv --output text
|
|
1177
|
+
```
|
|
901
1178
|
|
|
902
1179
|
[fzf]: https://github.com/junegunn/fzf
|
|
903
1180
|
[gum]: https://github.com/charmbracelet/gum
|
data/bin/commands/find.rb
CHANGED
|
@@ -68,6 +68,22 @@ class App
|
|
|
68
68
|
c.desc "Output actions nested by file and project"
|
|
69
69
|
c.switch %[omnifocus], negatable: false
|
|
70
70
|
|
|
71
|
+
c.desc 'Run a plugin on results (STDOUT only; no file writes)'
|
|
72
|
+
c.arg_name 'NAME'
|
|
73
|
+
c.flag %i[plugin]
|
|
74
|
+
|
|
75
|
+
c.desc 'Plugin input format (json|yaml|csv|text)'
|
|
76
|
+
c.arg_name 'TYPE'
|
|
77
|
+
c.flag %i[input]
|
|
78
|
+
|
|
79
|
+
c.desc 'Plugin output format (json|yaml|csv|text)'
|
|
80
|
+
c.arg_name 'TYPE'
|
|
81
|
+
c.flag %i[output]
|
|
82
|
+
|
|
83
|
+
c.desc 'Divider string for text IO'
|
|
84
|
+
c.arg_name 'STRING'
|
|
85
|
+
c.flag %i[divider]
|
|
86
|
+
|
|
71
87
|
c.action do |global_options, options, args|
|
|
72
88
|
options[:nest] = true if options[:omnifocus]
|
|
73
89
|
|
|
@@ -175,6 +191,52 @@ class App
|
|
|
175
191
|
[tokens]
|
|
176
192
|
end
|
|
177
193
|
|
|
194
|
+
# Plugin piping (display-only)
|
|
195
|
+
if options[:plugin]
|
|
196
|
+
NA::Plugins.ensure_plugins_home
|
|
197
|
+
plugin_path = options[:plugin]
|
|
198
|
+
unless File.exist?(plugin_path)
|
|
199
|
+
resolved = NA::Plugins.resolve_plugin(plugin_path)
|
|
200
|
+
plugin_path = resolved if resolved
|
|
201
|
+
end
|
|
202
|
+
if plugin_path && File.exist?(plugin_path)
|
|
203
|
+
meta = NA::Plugins.parse_plugin_metadata(plugin_path)
|
|
204
|
+
input_fmt = (options[:input] || meta['input'] || 'json').to_s
|
|
205
|
+
output_fmt = (options[:output] || meta['output'] || input_fmt).to_s
|
|
206
|
+
divider = (options[:divider] || '||')
|
|
207
|
+
|
|
208
|
+
io_actions = todo.actions.map(&:to_plugin_io_hash)
|
|
209
|
+
stdin_str = NA::Plugins.serialize_actions(io_actions, format: input_fmt, divider: divider)
|
|
210
|
+
stdout = NA::Plugins.run_plugin(plugin_path, stdin_str)
|
|
211
|
+
returned = Array(NA::Plugins.parse_actions(stdout, format: output_fmt, divider: divider))
|
|
212
|
+
index = {}
|
|
213
|
+
todo.actions.each { |a| index["#{a.file_path}:#{a.file_line}"] = a }
|
|
214
|
+
returned.each do |h|
|
|
215
|
+
key = "#{h['file_path']}:#{h['line'].to_i}"
|
|
216
|
+
a = index[key]
|
|
217
|
+
next unless a
|
|
218
|
+
new_text = h['text'].to_s
|
|
219
|
+
new_note = h['note'].to_s
|
|
220
|
+
new_tags = Array(h['tags']).map { |t| [t['name'].to_s, t['value'].to_s] }
|
|
221
|
+
new_text = new_text.gsub(/(?<=\A| )@\S+(?:\(.*?\))?/, '')
|
|
222
|
+
unless new_tags.empty?
|
|
223
|
+
tag_str = new_tags.map { |k, v| v.to_s.empty? ? "@#{k}" : "@#{k}(#{v})" }.join(' ')
|
|
224
|
+
new_text = new_text.strip + (tag_str.empty? ? '' : " #{tag_str}")
|
|
225
|
+
end
|
|
226
|
+
a.action = new_text
|
|
227
|
+
a.note = new_note.empty? ? [] : new_note.split("\n")
|
|
228
|
+
a.instance_variable_set(:@tags, a.scan_tags)
|
|
229
|
+
parents = Array(h['parents']).map(&:to_s)
|
|
230
|
+
if parents.any?
|
|
231
|
+
new_proj = parents.first.to_s
|
|
232
|
+
new_chain = parents[1..] || []
|
|
233
|
+
a.instance_variable_set(:@project, new_proj)
|
|
234
|
+
a.parent = new_chain
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
178
240
|
todo.actions.output(depth,
|
|
179
241
|
{ files: todo.files,
|
|
180
242
|
regexes: regexes,
|
data/bin/commands/next.rb
CHANGED
|
@@ -83,6 +83,22 @@ class App
|
|
|
83
83
|
c.desc "Include @done actions"
|
|
84
84
|
c.switch %i[done]
|
|
85
85
|
|
|
86
|
+
c.desc "Run a plugin on results (STDOUT only; no file writes)"
|
|
87
|
+
c.arg_name 'NAME'
|
|
88
|
+
c.flag %i[plugin]
|
|
89
|
+
|
|
90
|
+
c.desc 'Plugin input format (json|yaml|csv|text)'
|
|
91
|
+
c.arg_name 'TYPE'
|
|
92
|
+
c.flag %i[input]
|
|
93
|
+
|
|
94
|
+
c.desc 'Plugin output format (json|yaml|csv|text)'
|
|
95
|
+
c.arg_name 'TYPE'
|
|
96
|
+
c.flag %i[output]
|
|
97
|
+
|
|
98
|
+
c.desc 'Divider string for text IO'
|
|
99
|
+
c.arg_name 'STRING'
|
|
100
|
+
c.flag %i[divider]
|
|
101
|
+
|
|
86
102
|
c.desc "Output actions nested by file"
|
|
87
103
|
c.switch %i[nest], negatable: false
|
|
88
104
|
|
|
@@ -262,6 +278,55 @@ class App
|
|
|
262
278
|
Run `na todos` to see available todo files.")
|
|
263
279
|
end
|
|
264
280
|
NA::Pager.paginate = false if options[:omnifocus]
|
|
281
|
+
|
|
282
|
+
# If a plugin is specified, transform actions in memory for display only
|
|
283
|
+
if options[:plugin]
|
|
284
|
+
NA::Plugins.ensure_plugins_home
|
|
285
|
+
plugin_path = options[:plugin]
|
|
286
|
+
unless File.exist?(plugin_path)
|
|
287
|
+
resolved = NA::Plugins.resolve_plugin(plugin_path)
|
|
288
|
+
plugin_path = resolved if resolved
|
|
289
|
+
end
|
|
290
|
+
if plugin_path && File.exist?(plugin_path)
|
|
291
|
+
meta = NA::Plugins.parse_plugin_metadata(plugin_path)
|
|
292
|
+
input_fmt = (options[:input] || meta['input'] || 'json').to_s
|
|
293
|
+
output_fmt = (options[:output] || meta['output'] || input_fmt).to_s
|
|
294
|
+
divider = (options[:divider] || '||')
|
|
295
|
+
|
|
296
|
+
io_actions = todo.actions.map(&:to_plugin_io_hash)
|
|
297
|
+
stdin_str = NA::Plugins.serialize_actions(io_actions, format: input_fmt, divider: divider)
|
|
298
|
+
stdout = NA::Plugins.run_plugin(plugin_path, stdin_str)
|
|
299
|
+
returned = Array(NA::Plugins.parse_actions(stdout, format: output_fmt, divider: divider))
|
|
300
|
+
index = {}
|
|
301
|
+
todo.actions.each { |a| index["#{a.file_path}:#{a.file_line}"] = a }
|
|
302
|
+
returned.each do |h|
|
|
303
|
+
key = "#{h['file_path']}:#{h['line'].to_i}"
|
|
304
|
+
a = index[key]
|
|
305
|
+
next unless a
|
|
306
|
+
# Update for display: text, note, tags
|
|
307
|
+
new_text = h['text'].to_s
|
|
308
|
+
new_note = h['note'].to_s
|
|
309
|
+
new_tags = Array(h['tags']).map { |t| [t['name'].to_s, t['value'].to_s] }
|
|
310
|
+
# replace tags in text
|
|
311
|
+
new_text = new_text.gsub(/(?<=\A| )@\S+(?:\(.*?\))?/, '')
|
|
312
|
+
unless new_tags.empty?
|
|
313
|
+
tag_str = new_tags.map { |k, v| v.to_s.empty? ? "@#{k}" : "@#{k}(#{v})" }.join(' ')
|
|
314
|
+
new_text = new_text.strip + (tag_str.empty? ? '' : " #{tag_str}")
|
|
315
|
+
end
|
|
316
|
+
a.action = new_text
|
|
317
|
+
a.note = new_note.empty? ? [] : new_note.split("\n")
|
|
318
|
+
a.instance_variable_set(:@tags, a.scan_tags)
|
|
319
|
+
# parents -> possibly change project and parent chain for display
|
|
320
|
+
parents = Array(h['parents']).map(&:to_s)
|
|
321
|
+
if parents.any?
|
|
322
|
+
new_proj = parents.first.to_s
|
|
323
|
+
new_chain = parents[1..] || []
|
|
324
|
+
a.instance_variable_set(:@project, new_proj)
|
|
325
|
+
a.parent = new_chain
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
265
330
|
todo.actions.output(depth,
|
|
266
331
|
{ files: todo.files,
|
|
267
332
|
nest: options[:nest],
|