na 1.2.86 → 1.2.88

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,7 @@
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.86.
12
+ The current version of `na` is 1.2.88.
13
13
 
14
14
  `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
15
 
@@ -76,7 +76,7 @@ SYNOPSIS
76
76
  na [global options] command [command options] [arguments...]
77
77
 
78
78
  VERSION
79
- 1.2.86
79
+ 1.2.88
80
80
 
81
81
  GLOBAL OPTIONS
82
82
  -a, --add - Add a next action (deprecated, for backwards compatibility)
@@ -112,6 +112,7 @@ COMMANDS
112
112
  move - Move an existing action to a different section
113
113
  next, show - Show next actions
114
114
  open - Open a todo file in the default editor
115
+ plugin - Run a plugin on selected actions
115
116
  projects - Show list of projects for a file
116
117
  prompt - Show or install prompt hooks for the current shell
117
118
  restore, unfinish - Find and remove @done tag from an action
@@ -152,11 +153,14 @@ DESCRIPTION
152
153
  COMMAND OPTIONS
153
154
  --at=POSITION - Add task at [s]tart or [e]nd of target project (default: none)
154
155
  -d, --depth=DEPTH - Search for files X directories deep (default: 1)
156
+ --duration=DURATION - Duration (e.g. 45m, 2h, 1d2h30m, or minutes) (default: none)
157
+ --end, --finished=DATE - End/Finished time (natural language or ISO) (default: none)
155
158
  -f, --file=PATH - Specify the file to which the task should be added (default: none)
156
159
  --finish, --done - Mark task as @done with date
157
160
  --in, --todo=TODO_FILE - Add to a known todo file, partial matches allowed (default: none)
158
161
  -n, --note - Prompt for additional notes. STDIN input (piped) will be treated as a note if present.
159
162
  -p, --priority=PRIO - Add a priority level 1-5 or h, m, l (default: 0)
163
+ --started=DATE - Started time (natural language or ISO) (default: none)
160
164
  -t, --tag=TAG - Use a tag other than the default next action tag (default: none)
161
165
  --to, --project, --proj=PROJECT - Add action to specific project (default: Inbox)
162
166
  -x - Don't add next action tag to new entry
@@ -221,18 +225,24 @@ DESCRIPTION
221
225
 
222
226
  COMMAND OPTIONS
223
227
  -d, --depth=DEPTH - Recurse to depth (default: none)
228
+ --divider=STRING - Divider string for text IO (default: none)
224
229
  --[no-]done - Include @done actions
225
230
  -e, --regex - Interpret search pattern as regular expression
231
+ --human - Format durations in human-friendly form
226
232
  --in=TODO_PATH - Show actions from a specific todo file in history. May use wildcards (* and ?) (default: none)
233
+ --input=TYPE - Plugin input format (json|yaml|csv|text) (default: none)
227
234
  --nest - Output actions nested by file
228
235
  --no_file - No filename in output
229
236
  --[no-]notes - Include notes in output
230
237
  -o, --or - Combine search tokens with OR, displaying actions matching ANY of the terms
231
238
  --omnifocus - Output actions nested by file and project
239
+ --output=TYPE - Plugin output format (json|yaml|csv|text) (default: none)
240
+ --plugin=NAME - Run a plugin on results (STDOUT only; no file writes) (default: none)
232
241
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
233
242
  --save=TITLE - Save this search for future use (default: none)
234
243
  --[no-]search_notes - Include notes in search (default: enabled)
235
244
  --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
245
+ --times - Show per-action durations and total
236
246
  -v, --invert - Show actions not matching search pattern
237
247
  -x, --exact - Match pattern exactly
238
248
 
@@ -333,16 +343,24 @@ DESCRIPTION
333
343
  COMMAND OPTIONS
334
344
  --all - Show next actions from all known todo files (in any directory)
335
345
  -d, --depth=DEPTH - Recurse to depth (default: none)
346
+ --divider=STRING - Divider string for text IO (default: none)
336
347
  --[no-]done - Include @done actions
337
348
  --exact - Search query is exact text match (not tokens)
338
349
  --file=TODO_FILE - Display matches from specific todo file ([relative] path) (default: none)
339
350
  --hidden - Include hidden directories while traversing
351
+ --human - Format durations in human-friendly form
340
352
  --in, --todo=TODO - Display matches from a known todo file anywhere in history (short name) (may be used more than once, default: none)
353
+ --input=TYPE - Plugin input format (json|yaml|csv|text) (default: none)
354
+ --json_times - Output times as JSON object (implies --times and --done)
341
355
  --nest - Output actions nested by file
342
356
  --no_file - No filename in output
343
357
  --[no-]notes - Include notes in output
344
358
  --omnifocus - Output actions nested by file and project
359
+ --only_timed - Show only actions that have a duration (@started and @done)
360
+ --only_times - Output only elapsed time totals (implies --times and --done)
361
+ --output=TYPE - Plugin output format (json|yaml|csv|text) (default: none)
345
362
  -p, --prio, --priority=PRIORITY - Match actions with priority, allows <>= comparison (may be used more than once, default: none)
363
+ --plugin=NAME - Run a plugin on results (STDOUT only; no file writes) (default: none)
346
364
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
347
365
  --regex - Search query is regular expression
348
366
  --save=TITLE - Save this search for future use (default: none)
@@ -350,6 +368,7 @@ COMMAND OPTIONS
350
368
  --[no-]search_notes - Include notes in search (default: enabled)
351
369
  -t, --tag=TAG - Alternate tag to search for (default: none)
352
370
  --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
371
+ --times - Show per-action durations and total
353
372
 
354
373
  EXAMPLES
355
374
 
@@ -489,19 +508,28 @@ DESCRIPTION
489
508
 
490
509
  COMMAND OPTIONS
491
510
  -d, --depth=DEPTH - Recurse to depth (default: 1)
511
+ --divider=STRING - Divider string for text IO (default: none)
492
512
  --[no-]done - Include @done actions
493
513
  --exact - Search query is exact text match (not tokens)
514
+ --human - Format durations in human-friendly form
494
515
  --in=TODO_PATH - Show actions from a specific todo file in history. May use wildcards (* and ?) (default: none)
516
+ --input=TYPE - Plugin input format (json|yaml|csv|text) (default: none)
517
+ --json_times - Output times as JSON object (implies --times and --done)
495
518
  --nest - Output actions nested by file
496
519
  --no_file - No filename in output
497
520
  --[no-]notes - Include notes in output
498
521
  -o, --or - Combine tags with OR, displaying actions matching ANY of the tags
499
522
  --omnifocus - Output actions nested by file and project
523
+ --only_timed - Show only actions that have a duration (@started and @done)
524
+ --only_times - Output only elapsed time totals (implies --times and --done)
525
+ --output=TYPE - Plugin output format (json|yaml|csv|text) (default: none)
526
+ --plugin=NAME - Run a plugin on results (STDOUT only; no file writes) (default: none)
500
527
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
501
528
  --regex - Search query is regular expression
502
529
  --save=TITLE - Save this search for future use (default: none)
503
530
  --search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
504
531
  --[no-]search_notes - Include notes in search (default: enabled)
532
+ --times - Show per-action durations and total
505
533
  -v, --invert - Show actions not matching tags
506
534
 
507
535
  EXAMPLES
@@ -594,21 +622,28 @@ COMMAND OPTIONS
594
622
  --at=POSITION - When moving task, add at [s]tart or [e]nd of target project (default: none)
595
623
  -d, --depth=DEPTH - Search for files X directories deep (default: 1)
596
624
  --delete - Delete an action
625
+ --divider=STRING - Divider string for text IO (default: none)
597
626
  --[no-]done - Include @done actions
627
+ --duration=DURATION - Duration (e.g. 45m, 2h, 1d2h30m, or minutes) (default: none)
598
628
  -e, --regex - Interpret search pattern as regular expression
599
629
  --edit - Open action in editor (vim). Natural language dates will be parsed and converted in date-based tags.
630
+ --end, --finished=DATE - End/Finished time (natural language or ISO) (default: none)
600
631
  -f, --finish - Add a @done tag to action
601
632
  --file=PATH - Specify the file to search for the task (default: none)
602
633
  --in, --todo=TODO_FILE - Use a known todo file, partial matches allowed (default: none)
634
+ --input=TYPE - Plugin input format (json|yaml|csv|text) (default: none)
603
635
  -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.
604
636
  -o, --overwrite - Overwrite note instead of appending
637
+ --output=TYPE - Plugin output format (json|yaml|csv|text) (default: none)
605
638
  -p, --priority=PRIO - Add/change a priority level 1-5 (default: 0)
639
+ --plugin=NAME - Run a plugin by name on selected actions (default: none)
606
640
  --proj, --project=PROJECT[/SUBPROJECT] - Affect actions from a specific project (default: none)
607
641
  -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)
608
642
  --replace=TEXT - Use with --find to find and replace with new text. Enables --exact when used (default: none)
609
643
  --restore - Remove @done tag from action
610
644
  --search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
611
645
  --[no-]search_notes - Include notes in search (default: enabled)
646
+ --started=DATE - Started time (natural language or ISO) (default: none)
612
647
  -t, --tag=TAG - Add a tag to the action, @tag(values) allowed, use multiple times or combine multiple tags with a comma (may be used more than once, default: none)
613
648
  --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
614
649
  --to, --move=PROJECT - Move action to specific project (default: none)
@@ -626,6 +661,49 @@ EXAMPLES
626
661
  na update --archive My cool action
627
662
  ```
628
663
 
664
+ #### Time tracking
665
+
666
+ `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.
667
+
668
+ - Add/Finish/Update flags:
669
+ - `--started TIME` set a start time when creating or finishing an item
670
+ - `--end TIME` (alias `--finished`) set a done time
671
+ - `--duration DURATION` backfill start time from the provided end time
672
+ - All flags accept natural language (via Chronic) and shorthand: `30m ago`, `-2h`, `2h30m`, `2:30 ago`, `yesterday 5pm`
673
+
674
+ Examples:
675
+
676
+ ```bash
677
+ na add --started "30 minutes ago" "Investigate bug"
678
+ na complete --finished now --duration 2h30m "Investigate bug"
679
+ na update --started "yesterday 3pm" --end "yesterday 5:15pm" "Investigate bug"
680
+ ```
681
+
682
+ - Display flags (next/tagged):
683
+ - `--times` show per???action durations and a grand total (implies `--done`)
684
+ - `--human` format durations as human???readable text instead of `DD:HH:MM:SS`
685
+ - `--only_timed` show only actions that have both `@started` and `@done` (implies `--times --done`)
686
+ - `--only_times` output only the totals section (no action lines; implies `--times --done`)
687
+ - `--json_times` output a JSON object with timed items, per???tag totals, and overall total (implies `--times --done`)
688
+
689
+ Example outputs:
690
+
691
+ ```bash
692
+ # Per???action durations appended and totals table
693
+ na next --times --human
694
+
695
+ # Only totals table (Markdown), no action lines
696
+ na tagged "tag*=bug" --only_times
697
+
698
+ # JSON for scripting
699
+ na next --json_times > times.json
700
+ ```
701
+
702
+ Notes:
703
+
704
+ - Any newly added or edited action text is scanned for natural???language values in `@started(...)`/`@done(...)` and normalized to `YYYY???MM???DD HH:MM`.
705
+ - The color of durations in output is configurable via the theme key `duration` (defaults to `{y}`).
706
+
629
707
  ##### changelog
630
708
 
631
709
  View recent changes with `na changelog` or `na changes`.
@@ -655,7 +733,9 @@ COMMAND OPTIONS
655
733
  -a, --archive - Add a @done tag to action and move to Archive
656
734
  --all - Act on all matches immediately (no menu)
657
735
  -d, --depth=DEPTH - Search for files X directories deep (default: 1)
736
+ --duration=DURATION - Duration (e.g. 45m, 2h, 1d2h30m, or minutes) (default: none)
658
737
  -e, --regex - Interpret search pattern as regular expression
738
+ --end, --finished=DATE - End/Finished time (natural language or ISO) (default: none)
659
739
  --file=PATH - Specify the file to search for the task (default: none)
660
740
  --in, --todo=TODO_FILE - Use a known todo file, partial matches allowed (default: none)
661
741
  -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.
@@ -663,6 +743,7 @@ COMMAND OPTIONS
663
743
  --proj, --project=PROJECT[/SUBPROJECT] - Affect actions from a specific project (default: none)
664
744
  --search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
665
745
  --[no-]search_notes - Include notes in search (default: enabled)
746
+ --started=DATE - Started time (natural language or ISO) (default: none)
666
747
  --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
667
748
  --to, --move=PROJECT - Add a @done tag and move action to specific project (default: none)
668
749
  -x, --exact - Match pattern exactly
@@ -834,6 +915,115 @@ If you're using a single global file, you'll need `--cwd_as` to be `tag` or `pro
834
915
 
835
916
  After installing a hook, you'll need to close your terminal and start a new session to initialize the new commands.
836
917
 
918
+ ### Plugins
919
+
920
+ 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.
921
+
922
+ #### Getting Started
923
+
924
+ The first time NA runs, it will create the plugins directory with a README and two sample plugins:
925
+ - `Add Foo.py` - Adds a `@foo` tag with a timestamp
926
+ - `Add Bar.sh` - Adds a `@bar` tag
927
+
928
+ You can delete or modify these sample plugins as needed.
929
+
930
+ #### Running Plugins
931
+
932
+ Run a plugin with:
933
+ ```bash
934
+ na plugin PLUGIN_NAME
935
+ ```
936
+
937
+ Or use plugins through the `update` command's interactive menu, or pipe actions through plugins on display commands:
938
+
939
+ ```bash
940
+ na update --plugin PLUGIN_NAME # Run plugin on selected actions
941
+ na next --plugin PLUGIN_NAME # Transform output only (no file writes)
942
+ na tagged bug --plugin PLUGIN_NAME # Filter and transform
943
+ na find "search term" --plugin PLUGIN_NAME
944
+ ```
945
+
946
+ #### Plugin Metadata
947
+
948
+ Plugins can specify their behavior in a metadata block after the shebang:
949
+
950
+ ```bash
951
+ #!/usr/bin/env python3
952
+ # name: My Plugin
953
+ # input: json
954
+ # output: json
955
+ ```
956
+
957
+ Available metadata keys (case-insensitive):
958
+ - `input`: Input format (`json`, `yaml`, `csv`, `text`)
959
+ - `output`: Output format
960
+ - `name` or `title`: Display name (defaults to filename)
961
+
962
+ #### Input/Output Formats
963
+
964
+ Plugins accept and return action data. Use `--input` and `--output` flags to override metadata:
965
+
966
+ ```bash
967
+ na plugin MY_PLUGIN --input text --output json --divider "||"
968
+ ```
969
+
970
+ **JSON/YAML Schema:**
971
+ ```json
972
+ [
973
+ {
974
+ "file_path": "todo.taskpaper",
975
+ "line": 15,
976
+ "parents": ["Project", "Subproject"],
977
+ "text": "- Action text @tag(value)",
978
+ "note": "Note content",
979
+ "tags": [
980
+ { "name": "tag", "value": "value" }
981
+ ]
982
+ }
983
+ ]
984
+ ```
985
+
986
+ **Text Format:**
987
+ ```
988
+ ACTION||ARGS||file_path:line||parents||text||note||tags
989
+ ```
990
+
991
+ Default divider is `||` (configurable with `--divider`).
992
+ - `parents`: `Parent>Child>Leaf`
993
+ - `tags`: `name(value);name;other(value)`
994
+
995
+ If the first token isn???t a known action, it???s treated as `file_path:line` and the action defaults to UPDATE.
996
+
997
+ #### Actions
998
+
999
+ Plugins may return an optional ACTION with arguments. Supported (case-insensitive):
1000
+ - UPDATE (default; replace text/note/tags/parents)
1001
+ - DELETE
1002
+ - COMPLETE/FINISH
1003
+ - RESTORE/UNFINISH
1004
+ - ARCHIVE
1005
+ - ADD_TAG (args: one or more tags)
1006
+ - DELETE_TAG/REMOVE_TAG (args: one or more tags)
1007
+ - MOVE (args: target project path)
1008
+
1009
+ #### Plugin Behavior
1010
+
1011
+ **On `update` or `plugin` command:**
1012
+ - Plugins can modify text, notes, tags, and parents
1013
+ - Changing `parents` will move the action to the new project location
1014
+ - `file_path` and `line` cannot be changed
1015
+
1016
+ **On display commands (`next`, `tagged`, `find`):**
1017
+ - Plugins only transform STDOUT (no file writes)
1018
+ - Use returned text/note/tags/parents for rendering
1019
+ - Parent changes affect display but not file structure
1020
+
1021
+ #### Override Formats
1022
+
1023
+ You can override plugin defaults with flags on any command that supports `--plugin`:
1024
+ ```bash
1025
+ na next --plugin FOO --input csv --output text
1026
+ ```
837
1027
 
838
1028
  [fzf]: https://github.com/junegunn/fzf
839
1029
  [gum]: https://github.com/charmbracelet/gum
data/Rakefile CHANGED
@@ -1,70 +1,70 @@
1
- require "rake/clean"
2
- require "rubygems"
3
- require "rubygems/package_task"
4
- require "rdoc/task"
5
- require "bump/tasks"
6
- require "bundler/gem_tasks"
7
- require "rspec/core/rake_task"
8
- require "rubocop/rake_task"
9
- require "yard"
10
- require "tty-spinner"
11
- require "English"
1
+ require 'rake/clean'
2
+ require 'rubygems'
3
+ require 'rubygems/package_task'
4
+ require 'rdoc/task'
5
+ require 'bump/tasks'
6
+ require 'bundler/gem_tasks'
7
+ require 'rspec/core/rake_task'
8
+ require 'rubocop/rake_task'
9
+ require 'yard'
10
+ require 'tty-spinner'
11
+ require 'English'
12
12
 
13
13
  YARD::Rake::YardocTask.new do |t|
14
- t.files = ["lib/na/*.rb"]
15
- t.options = ["--markup-provider=redcarpet", "--markup=markdown", "--no-private", "-p", "yard_templates"]
16
- t.stats_options = ["--list-undoc"] # Uncommented this line for stats options
14
+ t.files = ['lib/na/*.rb']
15
+ t.options = ['--markup-provider=redcarpet', '--markup=markdown', '--no-private', '-p', 'yard_templates']
16
+ t.stats_options = ['--list-undoc'] # Uncommented this line for stats options
17
17
  end
18
18
 
19
19
  ## Docker error class
20
20
  class DockerError < StandardError
21
21
  def initialize(msg = nil)
22
- msg = msg ? "Docker error: #{msg}" : "Docker error"
23
- super(msg)
22
+ msg = msg ? "Docker error: #{msg}" : 'Docker error'
23
+ super
24
24
  end
25
25
  end
26
26
 
27
27
  task default: %i[test yard]
28
28
 
29
- desc "Run test suite"
29
+ desc 'Run test suite'
30
30
  task test: %i[rubocop spec]
31
31
 
32
32
  RSpec::Core::RakeTask.new do |t|
33
- t.rspec_opts = "--format documentation"
33
+ t.rspec_opts = '--format documentation'
34
34
  end
35
35
 
36
36
  RuboCop::RakeTask.new do |t|
37
- t.formatters = ["progress"]
37
+ t.formatters = ['progress']
38
38
  end
39
39
 
40
40
  task :doc, [*Rake.application[:yard].arg_names] => [:yard]
41
41
 
42
42
  Rake::RDocTask.new do |rd|
43
- rd.main = "README.rdoc"
44
- rd.rdoc_files.include("README.rdoc", "lib/**/*.rb", "bin/**/*")
45
- rd.title = "na"
43
+ rd.main = 'README.rdoc'
44
+ rd.rdoc_files.include('README.rdoc', 'lib/**/*.rb', 'bin/**/*')
45
+ rd.title = 'na'
46
46
  end
47
47
 
48
- spec = eval(File.read("na.gemspec"))
48
+ spec = eval(File.read('na.gemspec'))
49
49
 
50
50
  Gem::PackageTask.new(spec) do |pkg|
51
51
  end
52
- require "rake/testtask"
52
+ require 'rake/testtask'
53
53
  Rake::TestTask.new do |t|
54
- t.libs << "test"
55
- t.test_files = FileList["test/*_test.rb"]
54
+ t.libs << 'test'
55
+ t.test_files = FileList['test/*_test.rb']
56
56
  end
57
57
 
58
- desc "Install current gem in all versions of asdf-controlled ruby"
58
+ desc 'Install current gem in all versions of asdf-controlled ruby'
59
59
  task :install do
60
- Rake::Task["clobber"].invoke
61
- Rake::Task["package"].invoke
62
- Dir.chdir "pkg"
63
- file = Dir.glob("*.gem").last
60
+ Rake::Task['clobber'].invoke
61
+ Rake::Task['package'].invoke
62
+ Dir.chdir 'pkg'
63
+ file = Dir.glob('*.gem').last
64
64
 
65
65
  current_ruby = `asdf current ruby`.match(/(\d.\d+.\d+)/)[1]
66
66
 
67
- `asdf list ruby`.split.map { |ruby| ruby.strip.sub(/^*/, "") }.each do |ruby|
67
+ `asdf list ruby`.split.map { |ruby| ruby.strip.sub(/^*/, '') }.each do |ruby|
68
68
  `asdf shell ruby #{ruby}`
69
69
  puts `gem install #{file}`
70
70
  end
@@ -72,10 +72,10 @@ task :install do
72
72
  `asdf shell ruby #{current_ruby}`
73
73
  end
74
74
 
75
- desc "Development version check"
75
+ desc 'Development version check'
76
76
  task :ver do
77
77
  gver = `git ver`
78
- cver = IO.read(File.join(File.dirname(__FILE__), "CHANGELOG.md")).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1]
78
+ cver = IO.read(File.join(File.dirname(__FILE__), 'CHANGELOG.md')).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1]
79
79
  res = `grep VERSION lib/na/version.rb`
80
80
  version = res.match(/VERSION *= *['"](\d+\.\d+\.\d+(\w+)?)/)[1]
81
81
  puts "git tag: #{gver}"
@@ -83,22 +83,22 @@ task :ver do
83
83
  puts "changelog: #{cver}"
84
84
  end
85
85
 
86
- desc "Changelog version check"
86
+ desc 'Changelog version check'
87
87
  task :cver do
88
- puts IO.read(File.join(File.dirname(__FILE__), "CHANGELOG.md")).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1]
88
+ puts IO.read(File.join(File.dirname(__FILE__), 'CHANGELOG.md')).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1]
89
89
  end
90
90
 
91
- desc "Bump incremental version number"
91
+ desc 'Bump incremental version number'
92
92
  task :bump, :type do |_, args|
93
- args.with_defaults(type: "inc")
94
- version_file = "lib/na/version.rb"
93
+ args.with_defaults(type: 'inc')
94
+ version_file = 'lib/na/version.rb'
95
95
  content = IO.read(version_file)
96
96
  content.sub!(/VERSION = '(?<major>\d+)\.(?<minor>\d+)\.(?<inc>\d+)(?<pre>\S+)?'/) do
97
97
  m = Regexp.last_match
98
- major = m["major"].to_i
99
- minor = m["minor"].to_i
100
- inc = m["inc"].to_i
101
- pre = m["pre"]
98
+ major = m['major'].to_i
99
+ minor = m['minor'].to_i
100
+ inc = m['inc'].to_i
101
+ pre = m['pre']
102
102
 
103
103
  case args[:type]
104
104
  when /^maj/
@@ -115,73 +115,73 @@ task :bump, :type do |_, args|
115
115
  $stdout.puts "At version #{major}.#{minor}.#{inc}#{pre}"
116
116
  "VERSION = '#{major}.#{minor}.#{inc}#{pre}'"
117
117
  end
118
- File.open(version_file, "w+") { |f| f.puts content }
118
+ File.open(version_file, 'w+') { |f| f.puts content }
119
119
  end
120
120
 
121
121
  # task default: %i[test clobber package]
122
122
 
123
- desc "Remove packages"
123
+ desc 'Remove packages'
124
124
  task :clobber_packages do
125
- FileUtils.rm_f "pkg/*"
125
+ FileUtils.rm_f 'pkg/*'
126
126
  end
127
127
  # Make a prerequisite of the preexisting clobber task
128
- desc "Clobber files"
128
+ desc 'Clobber files'
129
129
  task clobber: :clobber_packages
130
130
 
131
- desc "Get Script Version"
131
+ desc 'Get Script Version'
132
132
  task :sver do
133
133
  res = `grep VERSION lib/na/version.rb`
134
134
  version = res.match(/VERSION *= *['"](\d+\.\d+\.\d+(\w+)?)/)[1]
135
135
  print version
136
136
  end
137
137
 
138
- desc "Run tests in Docker"
138
+ desc 'Run tests in Docker'
139
139
  task :dockertest, :version, :login, :attempt do |_, args|
140
- args.with_defaults(version: "all", login: false, attempt: 1)
140
+ args.with_defaults(version: 'all', login: false, attempt: 1)
141
141
  `open -a Docker`
142
142
 
143
- Rake::Task["clobber"].reenable
144
- Rake::Task["clobber"].invoke
145
- Rake::Task["build"].reenable
146
- Rake::Task["build"].invoke
143
+ Rake::Task['clobber'].reenable
144
+ Rake::Task['clobber'].invoke
145
+ Rake::Task['build'].reenable
146
+ Rake::Task['build'].invoke
147
147
 
148
148
  case args[:version]
149
149
  when /^a/
150
150
  %w[6 7 3].each do |v|
151
- Rake::Task["dockertest"].reenable
152
- Rake::Task["dockertest"].invoke(v, false)
151
+ Rake::Task['dockertest'].reenable
152
+ Rake::Task['dockertest'].invoke(v, false)
153
153
  end
154
154
  Process.exit 0
155
155
  when /^3\.?3/
156
- img = "natest33"
157
- file = "docker/Dockerfile-3.3"
156
+ img = 'natest33'
157
+ file = 'docker/Dockerfile-3.3'
158
158
  when /^3/
159
- version = "3.0"
160
- img = "natest3"
161
- file = "docker/Dockerfile-3.0"
159
+ version = '3.0'
160
+ img = 'natest3'
161
+ file = 'docker/Dockerfile-3.0'
162
162
  when /6$/
163
- version = "2.6"
164
- img = "natest26"
165
- file = "docker/Dockerfile-2.6"
163
+ version = '2.6'
164
+ img = 'natest26'
165
+ file = 'docker/Dockerfile-2.6'
166
166
  when /(^2|7$)/
167
- version = "2.7"
168
- img = "natest27"
169
- file = "docker/Dockerfile-2.7"
167
+ version = '2.7'
168
+ img = 'natest27'
169
+ file = 'docker/Dockerfile-2.7'
170
170
  else
171
- version = "3.0.1"
172
- img = "natest"
173
- file = "docker/Dockerfile"
171
+ version = '3.0.1'
172
+ img = 'natest'
173
+ file = 'docker/Dockerfile'
174
174
  end
175
175
 
176
176
  puts `docker build . --file #{file} -t #{img}`
177
177
 
178
- raise DockerError, "Error building docker image" unless $CHILD_STATUS.success?
178
+ raise DockerError, 'Error building docker image' unless $CHILD_STATUS.success?
179
179
 
180
180
  dirs = {
181
- File.dirname(__FILE__) => "/na",
182
- File.expand_path("~/.config") => "/root/.config"
181
+ File.dirname(__FILE__) => '/na',
182
+ File.expand_path('~/.config') => '/root/.config'
183
183
  }
184
- dir_args = dirs.map { |s, d| " -v '#{s}:#{d}'" }.join(" ")
184
+ dir_args = dirs.map { |s, d| " -v '#{s}:#{d}'" }.join(' ')
185
185
  exec "docker run #{dir_args} -it #{img} /bin/bash -l" if args[:login]
186
186
 
187
187
  spinner = TTY::Spinner.new("[:spinner] Running tests (#{version})...", hide_cursor: true)
@@ -197,15 +197,15 @@ task :dockertest, :version, :login, :attempt do |_, args|
197
197
  # puts res
198
198
  # puts commit&.empty? ? "Error commiting Docker tag #{img}" : "Committed Docker tag #{img}"
199
199
  rescue DockerError
200
- raise StandardError.new("Docker not responding") if args[:attempt] > 3
200
+ raise StandardError.new('Docker not responding') if args[:attempt] > 3
201
201
 
202
202
  `open -a Docker`
203
203
  sleep 3
204
- Rake::Task["dockertest"].reenable
205
- Rake::Task["dockertest"].invoke(args[:version], args[:login], args[:attempt] + 1)
204
+ Rake::Task['dockertest'].reenable
205
+ Rake::Task['dockertest'].invoke(args[:version], args[:login], args[:attempt] + 1)
206
206
  end
207
207
 
208
- desc "alias for build"
208
+ desc 'alias for build'
209
209
  task package: :build
210
210
 
211
211
  desc 'Run tests with coverage'
data/bin/commands/add.rb CHANGED
@@ -11,6 +11,17 @@ class App
11
11
  allow you to pick to which file the action gets added.'
12
12
  arg_name "ACTION"
13
13
  command :add do |c|
14
+ c.desc "Started time (natural language or ISO)"
15
+ c.arg_name "DATE"
16
+ c.flag %i[started], type: :date_begin
17
+
18
+ c.desc "End/Finished time (natural language or ISO)"
19
+ c.arg_name "DATE"
20
+ c.flag %i[end finished], type: :date_end
21
+
22
+ c.desc "Duration (e.g. 45m, 2h, 1d2h30m, or minutes)"
23
+ c.arg_name "DURATION"
24
+ c.flag %i[duration], type: :duration
14
25
  c.example 'na add "A cool feature I thought of @idea"', desc: "Add a new action to the Inbox, including a tag"
15
26
  c.example 'na add "A bug I need to fix" -p 4 -n',
16
27
  desc: "Add a new action to the Inbox, set its @priority to 4, and prompt for an additional note."
@@ -181,7 +192,26 @@ class App
181
192
  note.<< split_note unless split_note.nil?
182
193
  note.concat(line_note) unless line_note.nil?
183
194
 
184
- NA.add_action(target, options[:project], action, note, finish: options[:finish], append: append)
195
+ # Compute started/done based on flags
196
+ started_at = options[:started]
197
+ started_at = NA::Types.parse_date_begin(started_at) if started_at && !started_at.is_a?(Time)
198
+ done_at = options[:end] || options[:finished]
199
+ done_at = NA::Types.parse_date_end(done_at) if done_at && !done_at.is_a?(Time)
200
+ duration_seconds = options[:duration]
201
+
202
+ NA.notify("ADD parsed started_at=#{started_at.inspect} done_at=#{done_at.inspect} duration=#{duration_seconds.inspect}", debug: true)
203
+
204
+ # Ensure @started is present in the action text if a start time was provided
205
+ if started_at
206
+ started_str = started_at.strftime('%Y-%m-%d %H:%M')
207
+ # remove any existing @start/@started tag before appending
208
+ action = action.gsub(/(?<=\A| )@start(?:ed)?\(.*?\)/i, '').strip
209
+ action = "#{action} @started(#{started_str})"
210
+ end
211
+
212
+ NA.add_action(target, options[:project], action, note,
213
+ finish: options[:finish], append: append,
214
+ started_at: started_at, done_at: done_at, duration_seconds: duration_seconds)
185
215
  end
186
216
  end
187
217
  end
@@ -9,6 +9,7 @@ class App
9
9
  pagers = [
10
10
  'mdless',
11
11
  'mdcat',
12
+ 'glow',
12
13
  'bat',
13
14
  ENV['PAGER'],
14
15
  'less -FXr',