na 1.2.88 → 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 +12 -12
- data/CHANGELOG.md +36 -0
- data/Gemfile.lock +1 -1
- data/README.md +208 -57
- data/bin/commands/plugin.rb +271 -57
- data/bin/commands/update.rb +121 -90
- data/lib/na/action.rb +6 -0
- data/lib/na/actions.rb +3 -2
- data/lib/na/next_action.rb +24 -18
- data/lib/na/plugins.rb +152 -7
- data/lib/na/version.rb +1 -1
- data/src/_README.md +142 -55
- metadata +1 -2
- data/2025-10-29-one-more-na-update.md +0 -142
data/lib/na/next_action.rb
CHANGED
|
@@ -69,12 +69,16 @@ module NA
|
|
|
69
69
|
return
|
|
70
70
|
when 'MOVE'
|
|
71
71
|
move_to = action_args.first.to_s
|
|
72
|
-
update_action(file, { target_line: line }, add: action, move: move_to, all: true)
|
|
72
|
+
update_action(file, { target_line: line }, add: action, move: move_to, all: true, suppress_prompt: true)
|
|
73
73
|
return
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
# Replace content on the existing action then write back in-place
|
|
77
77
|
original_line = action.file_line
|
|
78
|
+
original_project = action.project
|
|
79
|
+
original_parent_chain = action.parent.dup
|
|
80
|
+
|
|
81
|
+
# Update action content
|
|
78
82
|
action.action = text
|
|
79
83
|
action.note = note.to_s.split("\n")
|
|
80
84
|
action.action.gsub!(/(?<=\A| )@\S+(?:\(.*?\))?/, '')
|
|
@@ -82,18 +86,13 @@ module NA
|
|
|
82
86
|
tag_str = tags.map { |k, v| v.to_s.empty? ? "@#{k}" : "@#{k}(#{v})" }.join(' ')
|
|
83
87
|
action.action = action.action.strip + (tag_str.empty? ? "" : " #{tag_str}")
|
|
84
88
|
end
|
|
85
|
-
# Ensure we update this exact action in-place
|
|
86
|
-
update_action(file, { target_line: original_line }, add: action, all: true)
|
|
87
89
|
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
move_to = ([new_project] + new_parent_chain).join(':')
|
|
90
|
+
# Check if parents changed
|
|
91
|
+
parents_changed = new_project.to_s.strip != original_project || new_parent_chain != original_parent_chain
|
|
92
|
+
move_to = parents_changed ? ([new_project] + new_parent_chain).join(':') : nil
|
|
91
93
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
project: action.project,
|
|
95
|
-
overwrite: true,
|
|
96
|
-
move: move_to)
|
|
94
|
+
# Update in-place (with move if parents changed)
|
|
95
|
+
update_action(file, { target_line: original_line }, add: action, move: move_to, all: true, suppress_prompt: true)
|
|
97
96
|
end
|
|
98
97
|
include NA::Editor
|
|
99
98
|
|
|
@@ -143,6 +142,7 @@ module NA
|
|
|
143
142
|
# @return [Boolean] result
|
|
144
143
|
#
|
|
145
144
|
def yn(prompt, default: true)
|
|
145
|
+
return default if ENV['NA_TEST'] == '1'
|
|
146
146
|
return default unless $stdout.isatty
|
|
147
147
|
|
|
148
148
|
tty_state = `stty -g`
|
|
@@ -422,7 +422,8 @@ module NA
|
|
|
422
422
|
tagged: nil,
|
|
423
423
|
started_at: nil,
|
|
424
424
|
done_at: nil,
|
|
425
|
-
duration_seconds: nil
|
|
425
|
+
duration_seconds: nil,
|
|
426
|
+
suppress_prompt: false)
|
|
426
427
|
# Coerce date/time inputs if passed as strings
|
|
427
428
|
begin
|
|
428
429
|
started_at = NA::Types.parse_date_begin(started_at) if started_at && !started_at.is_a?(Time)
|
|
@@ -447,14 +448,19 @@ module NA
|
|
|
447
448
|
move = move.sub(/:$/, '')
|
|
448
449
|
target_proj = projects.select { |pr| pr.project =~ /#{move.gsub(':', '.*?:.*?')}/i }.first
|
|
449
450
|
if target_proj.nil?
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
)
|
|
453
|
-
if res
|
|
454
|
-
target_proj = insert_project(target, move, projects)
|
|
451
|
+
if suppress_prompt || !$stdout.isatty
|
|
452
|
+
target_proj = insert_project(target, move)
|
|
455
453
|
projects << target_proj
|
|
456
454
|
else
|
|
457
|
-
NA.
|
|
455
|
+
res = NA.yn(
|
|
456
|
+
NA::Color.template("#{NA.theme[:warning]}Project #{NA.theme[:file]}#{move}#{NA.theme[:warning]} doesn't exist, add it"), default: true
|
|
457
|
+
)
|
|
458
|
+
if res
|
|
459
|
+
target_proj = insert_project(target, move)
|
|
460
|
+
projects << target_proj
|
|
461
|
+
else
|
|
462
|
+
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1)
|
|
463
|
+
end
|
|
458
464
|
end
|
|
459
465
|
end
|
|
460
466
|
end
|
data/lib/na/plugins.rb
CHANGED
|
@@ -13,14 +13,43 @@ module NA
|
|
|
13
13
|
File.expand_path('~/.local/share/na/plugins')
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def
|
|
16
|
+
def plugins_disabled_home
|
|
17
|
+
File.expand_path('~/.local/share/na/plugins_disabled')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def samples_generated_flag
|
|
21
|
+
File.expand_path('~/.local/share/na/.samples_generated')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def samples_generated?
|
|
25
|
+
File.exist?(samples_generated_flag)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def mark_samples_generated
|
|
29
|
+
FileUtils.mkdir_p(File.dirname(samples_generated_flag))
|
|
30
|
+
File.write(samples_generated_flag, Time.now.iso8601) unless File.exist?(samples_generated_flag)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ensure_plugins_home(force_samples: false)
|
|
17
34
|
dir = plugins_home
|
|
18
|
-
|
|
35
|
+
dis = plugins_disabled_home
|
|
36
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
37
|
+
FileUtils.mkdir_p(dis) unless File.directory?(dis)
|
|
19
38
|
|
|
20
|
-
FileUtils.mkdir_p(dir)
|
|
21
39
|
readme = File.join(dir, 'README.md')
|
|
22
40
|
File.write(readme, default_readme_contents) unless File.exist?(readme)
|
|
23
|
-
|
|
41
|
+
|
|
42
|
+
return if samples_generated? || force_samples
|
|
43
|
+
|
|
44
|
+
create_sample_plugins(dis)
|
|
45
|
+
mark_samples_generated
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def generate_sample_plugins
|
|
49
|
+
dis = plugins_disabled_home
|
|
50
|
+
FileUtils.mkdir_p(dis) unless File.directory?(dis)
|
|
51
|
+
create_sample_plugins(dis, force: true)
|
|
52
|
+
mark_samples_generated
|
|
24
53
|
end
|
|
25
54
|
|
|
26
55
|
def list_plugins
|
|
@@ -39,6 +68,22 @@ module NA
|
|
|
39
68
|
end
|
|
40
69
|
end
|
|
41
70
|
|
|
71
|
+
def list_plugins_disabled
|
|
72
|
+
dir = plugins_disabled_home
|
|
73
|
+
return {} unless File.directory?(dir)
|
|
74
|
+
|
|
75
|
+
Dir.children(dir).each_with_object({}) do |entry, acc|
|
|
76
|
+
path = File.join(dir, entry)
|
|
77
|
+
next unless File.file?(path)
|
|
78
|
+
next if entry =~ /\.(md|bak)$/i
|
|
79
|
+
next unless shebang?(path)
|
|
80
|
+
|
|
81
|
+
base = File.basename(entry, File.extname(entry))
|
|
82
|
+
key = base.gsub(/[\s_]/, '')
|
|
83
|
+
acc[key.downcase] = path
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
42
87
|
def resolve_plugin(name)
|
|
43
88
|
return nil unless name && !name.to_s.strip.empty?
|
|
44
89
|
|
|
@@ -48,6 +93,10 @@ module NA
|
|
|
48
93
|
|
|
49
94
|
# Fallback: try exact filename match in dir
|
|
50
95
|
path = File.join(plugins_home, name)
|
|
96
|
+
return path if File.file?(path)
|
|
97
|
+
|
|
98
|
+
# Also check disabled folder
|
|
99
|
+
path = File.join(plugins_disabled_home, name)
|
|
51
100
|
File.file?(path) ? path : nil
|
|
52
101
|
end
|
|
53
102
|
|
|
@@ -60,6 +109,17 @@ module NA
|
|
|
60
109
|
first.start_with?('#!') ? first.sub('#!', '').strip : nil
|
|
61
110
|
end
|
|
62
111
|
|
|
112
|
+
def infer_shebang_for_extension(ext)
|
|
113
|
+
case ext.downcase
|
|
114
|
+
when '.rb' then '#!/usr/bin/env ruby'
|
|
115
|
+
when '.py' then '#!/usr/bin/env python3'
|
|
116
|
+
when '.zsh' then '#!/usr/bin/env zsh'
|
|
117
|
+
when '.fish' then '#!/usr/bin/env fish'
|
|
118
|
+
when '.js', '.mjs' then '#!/usr/bin/env node'
|
|
119
|
+
else '#!/usr/bin/env bash'
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
63
123
|
def parse_plugin_metadata(file)
|
|
64
124
|
meta = { 'input' => nil, 'output' => nil, 'name' => nil }
|
|
65
125
|
lines = File.readlines(file, chomp: true)
|
|
@@ -103,6 +163,64 @@ module NA
|
|
|
103
163
|
end
|
|
104
164
|
end
|
|
105
165
|
|
|
166
|
+
def enable_plugin(name)
|
|
167
|
+
# Try by resolved path; if already enabled, return
|
|
168
|
+
path = resolve_plugin(name)
|
|
169
|
+
return path if path && File.dirname(path) == plugins_home
|
|
170
|
+
|
|
171
|
+
# Find in disabled by normalized name
|
|
172
|
+
disabled_map = Dir.exist?(plugins_disabled_home) ? Dir.children(plugins_disabled_home) : []
|
|
173
|
+
from = disabled_map.map { |e| File.join(plugins_disabled_home, e) }
|
|
174
|
+
.find { |p| File.basename(p).downcase.start_with?(name.to_s.downcase) }
|
|
175
|
+
from ||= File.join(plugins_disabled_home, name)
|
|
176
|
+
to = File.join(plugins_home, File.basename(from))
|
|
177
|
+
FileUtils.mv(from, to)
|
|
178
|
+
to
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def disable_plugin(name)
|
|
182
|
+
path = resolve_plugin(name)
|
|
183
|
+
return path if path && File.dirname(path) == plugins_disabled_home
|
|
184
|
+
|
|
185
|
+
enabled_map = Dir.exist?(plugins_home) ? Dir.children(plugins_home) : []
|
|
186
|
+
from = enabled_map.map { |e| File.join(plugins_home, e) }
|
|
187
|
+
.find { |p| File.basename(p).downcase.start_with?(name.to_s.downcase) }
|
|
188
|
+
from ||= File.join(plugins_home, name)
|
|
189
|
+
to = File.join(plugins_disabled_home, File.basename(from))
|
|
190
|
+
FileUtils.mv(from, to)
|
|
191
|
+
to
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def create_plugin(name, language: nil)
|
|
195
|
+
base = File.basename(name)
|
|
196
|
+
ext = File.extname(base)
|
|
197
|
+
if ext.empty? && language
|
|
198
|
+
ext = language.start_with?('.') ? language : ".#{language.split('/').last}"
|
|
199
|
+
end
|
|
200
|
+
ext = '.sh' if ext.empty?
|
|
201
|
+
she = language&.start_with?('/') ? language : infer_shebang_for_extension(ext)
|
|
202
|
+
file = File.join(plugins_home, base.sub(File.extname(base), '') + ext)
|
|
203
|
+
content = []
|
|
204
|
+
content << she
|
|
205
|
+
content << "# name: #{base.sub(File.extname(base), '')}"
|
|
206
|
+
content << '# input: json'
|
|
207
|
+
content << '# output: json'
|
|
208
|
+
content << '# New plugin template'
|
|
209
|
+
content << ''
|
|
210
|
+
content << '# Read STDIN and echo back unchanged'
|
|
211
|
+
content << 'if command -v python3 >/dev/null 2>&1; then'
|
|
212
|
+
content << " python3 - \"$@\" <<'PY'"
|
|
213
|
+
content << 'import sys, json'
|
|
214
|
+
content << 'data = json.load(sys.stdin)'
|
|
215
|
+
content << 'json.dump(data, sys.stdout)'
|
|
216
|
+
content << 'PY'
|
|
217
|
+
content << 'else'
|
|
218
|
+
content << ' cat'
|
|
219
|
+
content << 'fi'
|
|
220
|
+
File.write(file, content.join("\n"))
|
|
221
|
+
file
|
|
222
|
+
end
|
|
223
|
+
|
|
106
224
|
def serialize_actions(actions, format: 'json', divider: '||')
|
|
107
225
|
case format.to_s.downcase
|
|
108
226
|
when 'json'
|
|
@@ -373,10 +491,12 @@ module NA
|
|
|
373
491
|
MD
|
|
374
492
|
end
|
|
375
493
|
|
|
376
|
-
def create_sample_plugins(dir)
|
|
494
|
+
def create_sample_plugins(dir, force: false)
|
|
377
495
|
py = File.join(dir, 'Add Foo.py')
|
|
378
496
|
sh = File.join(dir, 'Add Bar.sh')
|
|
379
|
-
|
|
497
|
+
|
|
498
|
+
if force || !File.exist?(py)
|
|
499
|
+
FileUtils.rm_f(py)
|
|
380
500
|
File.write(py, <<~PY)
|
|
381
501
|
#!/usr/bin/env python3
|
|
382
502
|
# name: Add Foo
|
|
@@ -392,8 +512,32 @@ module NA
|
|
|
392
512
|
json.dump(data, sys.stdout)
|
|
393
513
|
PY
|
|
394
514
|
end
|
|
395
|
-
|
|
515
|
+
unless File.exist?(sh)
|
|
516
|
+
File.write(sh, <<~SH)
|
|
517
|
+
#!/usr/bin/env bash
|
|
518
|
+
# name: Add Bar
|
|
519
|
+
# input: text
|
|
520
|
+
# output: text
|
|
521
|
+
while IFS= read -r line; do
|
|
522
|
+
if [[ -z "$line" ]]; then continue; fi
|
|
523
|
+
if [[ "$line" == *"||"* ]]; then
|
|
524
|
+
fileline=${line%%||*}
|
|
525
|
+
rest=${line#*||}
|
|
526
|
+
parents=${rest%%||*}; rest=${rest#*||}
|
|
527
|
+
text=${rest%%||*}; rest=${rest#*||}
|
|
528
|
+
note=${rest%%||*}; tags=${rest#*||}
|
|
529
|
+
if [[ -z "$tags" ]]; then tags="bar"; else tags="$tags;bar"; fi
|
|
530
|
+
echo "$fileline||$parents||$text||$note||$tags"
|
|
531
|
+
else
|
|
532
|
+
echo "$line"
|
|
533
|
+
fi
|
|
534
|
+
done
|
|
535
|
+
SH
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
return unless force || !File.exist?(sh)
|
|
396
539
|
|
|
540
|
+
FileUtils.rm_f(sh)
|
|
397
541
|
File.write(sh, <<~SH)
|
|
398
542
|
#!/usr/bin/env bash
|
|
399
543
|
# name: Add Bar
|
|
@@ -414,6 +558,7 @@ module NA
|
|
|
414
558
|
fi
|
|
415
559
|
done
|
|
416
560
|
SH
|
|
561
|
+
File.chmod(0o755, sh)
|
|
417
562
|
end
|
|
418
563
|
end
|
|
419
564
|
end
|
data/lib/na/version.rb
CHANGED
data/src/_README.md
CHANGED
|
@@ -9,7 +9,50 @@
|
|
|
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 <!--VER-->1.2.
|
|
12
|
+
The current version of `na` is <!--VER-->1.2.88<!--END VER-->.
|
|
13
|
+
|
|
14
|
+
<!--GITHUB-->
|
|
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
|
+
<!--END GITHUB--><!--JEKYLL
|
|
53
|
+
- Table of Contents
|
|
54
|
+
{:.toc}
|
|
55
|
+
-->
|
|
13
56
|
|
|
14
57
|
`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
58
|
|
|
@@ -140,6 +183,54 @@ To see all next actions across all known todos, use `na next "*"`. You can combi
|
|
|
140
183
|
@cli(bundle exec bin/na help next)
|
|
141
184
|
```
|
|
142
185
|
|
|
186
|
+
##### plugin
|
|
187
|
+
|
|
188
|
+
Manage and run external plugins. See also the Plugins section below.
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
@cli(bundle exec bin/na help plugin)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
###### plugin new
|
|
195
|
+
|
|
196
|
+
Create a new plugin script (aliases: `n`). Infers shebang by extension or `--language`.
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
@cli(bundle exec bin/na help plugin new)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
###### plugin edit
|
|
203
|
+
|
|
204
|
+
Open an existing plugin in your default editor. Prompts if no name is given.
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
@cli(bundle exec bin/na help plugin edit)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
###### plugin run
|
|
211
|
+
|
|
212
|
+
Run a plugin on selected actions (aliases: `x`). Supports input/output format flags and filters.
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
@cli(bundle exec bin/na help plugin run)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
###### plugin enable
|
|
219
|
+
|
|
220
|
+
Move a plugin from `plugins_disabled` to `plugins` (alias: `e`).
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
@cli(bundle exec bin/na help plugin enable)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
###### plugin disable
|
|
227
|
+
|
|
228
|
+
Move a plugin from `plugins` to `plugins_disabled` (alias: `d`).
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
@cli(bundle exec bin/na help plugin disable)
|
|
232
|
+
```
|
|
233
|
+
|
|
143
234
|
##### projects
|
|
144
235
|
|
|
145
236
|
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.
|
|
@@ -235,49 +326,6 @@ See the help output for a list of all available actions.
|
|
|
235
326
|
@cli(bundle exec bin/na help update)
|
|
236
327
|
```
|
|
237
328
|
|
|
238
|
-
#### Time tracking
|
|
239
|
-
|
|
240
|
-
`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.
|
|
241
|
-
|
|
242
|
-
- Add/Finish/Update flags:
|
|
243
|
-
- `--started TIME` set a start time when creating or finishing an item
|
|
244
|
-
- `--end TIME` (alias `--finished`) set a done time
|
|
245
|
-
- `--duration DURATION` backfill start time from the provided end time
|
|
246
|
-
- All flags accept natural language (via Chronic) and shorthand: `30m ago`, `-2h`, `2h30m`, `2:30 ago`, `yesterday 5pm`
|
|
247
|
-
|
|
248
|
-
Examples:
|
|
249
|
-
|
|
250
|
-
```bash
|
|
251
|
-
na add --started "30 minutes ago" "Investigate bug"
|
|
252
|
-
na complete --finished now --duration 2h30m "Investigate bug"
|
|
253
|
-
na update --started "yesterday 3pm" --end "yesterday 5:15pm" "Investigate bug"
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
- Display flags (next/tagged):
|
|
257
|
-
- `--times` show per‑action durations and a grand total (implies `--done`)
|
|
258
|
-
- `--human` format durations as human‑readable text instead of `DD:HH:MM:SS`
|
|
259
|
-
- `--only_timed` show only actions that have both `@started` and `@done` (implies `--times --done`)
|
|
260
|
-
- `--only_times` output only the totals section (no action lines; implies `--times --done`)
|
|
261
|
-
- `--json_times` output a JSON object with timed items, per‑tag totals, and overall total (implies `--times --done`)
|
|
262
|
-
|
|
263
|
-
Example outputs:
|
|
264
|
-
|
|
265
|
-
```bash
|
|
266
|
-
# Per‑action durations appended and totals table
|
|
267
|
-
na next --times --human
|
|
268
|
-
|
|
269
|
-
# Only totals table (Markdown), no action lines
|
|
270
|
-
na tagged "tag*=bug" --only_times
|
|
271
|
-
|
|
272
|
-
# JSON for scripting
|
|
273
|
-
na next --json_times > times.json
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
Notes:
|
|
277
|
-
|
|
278
|
-
- Any newly added or edited action text is scanned for natural‑language values in `@started(...)`/`@done(...)` and normalized to `YYYY‑MM‑DD HH:MM`.
|
|
279
|
-
- The color of durations in output is configurable via the theme key `duration` (defaults to `{y}`).
|
|
280
|
-
|
|
281
329
|
##### changelog
|
|
282
330
|
|
|
283
331
|
View recent changes with `na changelog` or `na changes`.
|
|
@@ -375,6 +423,49 @@ If you're using a single global file, you'll need `--cwd_as` to be `tag` or `pro
|
|
|
375
423
|
|
|
376
424
|
After installing a hook, you'll need to close your terminal and start a new session to initialize the new commands.
|
|
377
425
|
|
|
426
|
+
### Time tracking
|
|
427
|
+
|
|
428
|
+
`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.
|
|
429
|
+
|
|
430
|
+
- Add/Finish/Update flags:
|
|
431
|
+
- `--started TIME` set a start time when creating or finishing an item
|
|
432
|
+
- `--end TIME` (alias `--finished`) set a done time
|
|
433
|
+
- `--duration DURATION` backfill start time from the provided end time
|
|
434
|
+
- All flags accept natural language (via Chronic) and shorthand: `30m ago`, `-2h`, `2h30m`, `2:30 ago`, `yesterday 5pm`
|
|
435
|
+
|
|
436
|
+
Examples:
|
|
437
|
+
|
|
438
|
+
```bash
|
|
439
|
+
na add --started "30 minutes ago" "Investigate bug"
|
|
440
|
+
na complete --finished now --duration 2h30m "Investigate bug"
|
|
441
|
+
na update --started "yesterday 3pm" --end "yesterday 5:15pm" "Investigate bug"
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
- Display flags (next/tagged):
|
|
445
|
+
- `--times` show per‑action durations and a grand total (implies `--done`)
|
|
446
|
+
- `--human` format durations as human‑readable text instead of `DD:HH:MM:SS`
|
|
447
|
+
- `--only_timed` show only actions that have both `@started` and `@done` (implies `--times --done`)
|
|
448
|
+
- `--only_times` output only the totals section (no action lines; implies `--times --done`)
|
|
449
|
+
- `--json_times` output a JSON object with timed items, per‑tag totals, and overall total (implies `--times --done`)
|
|
450
|
+
|
|
451
|
+
Example outputs:
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
# Per‑action durations appended and totals table
|
|
455
|
+
na next --times --human
|
|
456
|
+
|
|
457
|
+
# Only totals table (Markdown), no action lines
|
|
458
|
+
na tagged "tag*=bug" --only_times
|
|
459
|
+
|
|
460
|
+
# JSON for scripting
|
|
461
|
+
na next --json_times > times.json
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Notes:
|
|
465
|
+
|
|
466
|
+
- Any newly added or edited action text is scanned for natural‑language values in `@started(...)`/`@done(...)` and normalized to `YYYY‑MM‑DD HH:MM`.
|
|
467
|
+
- The color of durations in output is configurable via the theme key `duration` (defaults to `{y}`).
|
|
468
|
+
|
|
378
469
|
### Plugins
|
|
379
470
|
|
|
380
471
|
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.
|
|
@@ -389,19 +480,15 @@ You can delete or modify these sample plugins as needed.
|
|
|
389
480
|
|
|
390
481
|
#### Running Plugins
|
|
391
482
|
|
|
392
|
-
|
|
393
|
-
```bash
|
|
394
|
-
na plugin PLUGIN_NAME
|
|
395
|
-
```
|
|
483
|
+
You can manage and run plugins using subcommands under `na plugin`:
|
|
396
484
|
|
|
397
|
-
|
|
485
|
+
- `new`/`n`: scaffold a new plugin script
|
|
486
|
+
- `edit`: open an existing plugin
|
|
487
|
+
- `run`/`x`: run a plugin against selected actions
|
|
488
|
+
- `enable`/`e`: move from disabled to enabled
|
|
489
|
+
- `disable`/`d`: move from enabled to disabled
|
|
398
490
|
|
|
399
|
-
|
|
400
|
-
na update --plugin PLUGIN_NAME # Run plugin on selected actions
|
|
401
|
-
na next --plugin PLUGIN_NAME # Transform output only (no file writes)
|
|
402
|
-
na tagged bug --plugin PLUGIN_NAME # Filter and transform
|
|
403
|
-
na find "search term" --plugin PLUGIN_NAME
|
|
404
|
-
```
|
|
491
|
+
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).
|
|
405
492
|
|
|
406
493
|
#### Plugin Metadata
|
|
407
494
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: na
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.89
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brett Terpstra
|
|
@@ -264,7 +264,6 @@ files:
|
|
|
264
264
|
- ".rubocop.yml"
|
|
265
265
|
- ".rubocop_todo.yml"
|
|
266
266
|
- ".travis.yml"
|
|
267
|
-
- 2025-10-29-one-more-na-update.md
|
|
268
267
|
- CHANGELOG.md
|
|
269
268
|
- Gemfile
|
|
270
269
|
- Gemfile.lock
|