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.
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.87.
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.87
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],