na 1.2.87 → 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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +17 -9
- data/2025-10-29-one-more-na-update.md +142 -0
- data/CHANGELOG.md +39 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +4 -1
- data/README.md +128 -2
- data/bin/commands/find.rb +62 -0
- data/bin/commands/next.rb +65 -0
- data/bin/commands/plugin.rb +75 -0
- data/bin/commands/tagged.rb +63 -0
- data/bin/commands/update.rb +54 -1
- data/bin/na +1 -0
- data/lib/na/action.rb +13 -0
- data/lib/na/next_action.rb +92 -0
- data/lib/na/plugins.rb +419 -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 +110 -1
- metadata +21 -1
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],
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class App
|
|
4
|
+
extend GLI::App
|
|
5
|
+
|
|
6
|
+
desc 'Run a plugin on selected actions'
|
|
7
|
+
arg_name 'NAME'
|
|
8
|
+
command %i[plugin] do |c|
|
|
9
|
+
c.desc 'Input format (json|yaml|text)'
|
|
10
|
+
c.arg_name 'TYPE'
|
|
11
|
+
c.flag %i[input]
|
|
12
|
+
|
|
13
|
+
c.desc 'Output format (json|yaml|text)'
|
|
14
|
+
c.arg_name 'TYPE'
|
|
15
|
+
c.flag %i[output]
|
|
16
|
+
|
|
17
|
+
c.desc 'Text divider when using --input/--output text'
|
|
18
|
+
c.arg_name 'STRING'
|
|
19
|
+
c.flag %i[divider]
|
|
20
|
+
|
|
21
|
+
c.desc 'Specify the file to search for the task'
|
|
22
|
+
c.arg_name 'PATH'
|
|
23
|
+
c.flag %i[file in]
|
|
24
|
+
|
|
25
|
+
c.desc 'Search for files X directories deep'
|
|
26
|
+
c.arg_name 'DEPTH'
|
|
27
|
+
c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
|
|
28
|
+
|
|
29
|
+
c.desc 'Filter results using search terms'
|
|
30
|
+
c.arg_name 'QUERY'
|
|
31
|
+
c.flag %i[search find grep], multiple: true
|
|
32
|
+
|
|
33
|
+
c.desc 'Include @done actions'
|
|
34
|
+
c.switch %i[done]
|
|
35
|
+
|
|
36
|
+
c.desc 'Match actions containing tag. Allows value comparisons'
|
|
37
|
+
c.arg_name 'TAG'
|
|
38
|
+
c.flag %i[tagged], multiple: true
|
|
39
|
+
|
|
40
|
+
c.action do |_global, options, args|
|
|
41
|
+
plugin_name = args.first
|
|
42
|
+
NA.notify("#{NA.theme[:error]}Plugin name required", exit_code: 1) unless plugin_name
|
|
43
|
+
|
|
44
|
+
NA::Plugins.ensure_plugins_home
|
|
45
|
+
path = NA::Plugins.resolve_plugin(plugin_name)
|
|
46
|
+
NA.notify("#{NA.theme[:error]}Plugin not found: #{plugin_name}", exit_code: 1) unless path
|
|
47
|
+
|
|
48
|
+
meta = NA::Plugins.parse_plugin_metadata(path)
|
|
49
|
+
input_fmt = (options[:input] || meta['input'] || 'json').to_s
|
|
50
|
+
output_fmt = (options[:output] || meta['output'] || input_fmt).to_s
|
|
51
|
+
divider = (options[:divider] || '||')
|
|
52
|
+
|
|
53
|
+
# Build selection using the same plumbing as update/find
|
|
54
|
+
actions = NA.select_actions(
|
|
55
|
+
file: options[:file],
|
|
56
|
+
depth: options[:depth],
|
|
57
|
+
search: options[:search],
|
|
58
|
+
tagged: options[:tagged],
|
|
59
|
+
include_done: options[:done]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
io_actions = actions.map(&:to_plugin_io_hash)
|
|
63
|
+
stdin_str = NA::Plugins.serialize_actions(io_actions, format: input_fmt, divider: divider)
|
|
64
|
+
stdout = NA::Plugins.run_plugin(path, stdin_str)
|
|
65
|
+
returned = NA::Plugins.parse_actions(stdout, format: output_fmt, divider: divider)
|
|
66
|
+
|
|
67
|
+
# Apply updates
|
|
68
|
+
returned.each do |h|
|
|
69
|
+
NA.apply_plugin_result(h)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
data/bin/commands/tagged.rb
CHANGED
|
@@ -83,6 +83,22 @@ class App
|
|
|
83
83
|
c.desc 'Output actions nested by file and project'
|
|
84
84
|
c.switch %i[omnifocus], negatable: false
|
|
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.action do |global_options, options, args|
|
|
87
103
|
options[:nest] = true if options[:omnifocus]
|
|
88
104
|
|
|
@@ -184,6 +200,53 @@ class App
|
|
|
184
200
|
else
|
|
185
201
|
[tokens]
|
|
186
202
|
end
|
|
203
|
+
|
|
204
|
+
# Plugin piping (display only)
|
|
205
|
+
if options[:plugin]
|
|
206
|
+
NA::Plugins.ensure_plugins_home
|
|
207
|
+
plugin_path = options[:plugin]
|
|
208
|
+
unless File.exist?(plugin_path)
|
|
209
|
+
resolved = NA::Plugins.resolve_plugin(plugin_path)
|
|
210
|
+
plugin_path = resolved if resolved
|
|
211
|
+
end
|
|
212
|
+
if plugin_path && File.exist?(plugin_path)
|
|
213
|
+
meta = NA::Plugins.parse_plugin_metadata(plugin_path)
|
|
214
|
+
input_fmt = (options[:input] || meta['input'] || 'json').to_s
|
|
215
|
+
output_fmt = (options[:output] || meta['output'] || input_fmt).to_s
|
|
216
|
+
divider = (options[:divider] || '||')
|
|
217
|
+
|
|
218
|
+
io_actions = todo.actions.map(&:to_plugin_io_hash)
|
|
219
|
+
stdin_str = NA::Plugins.serialize_actions(io_actions, format: input_fmt, divider: divider)
|
|
220
|
+
stdout = NA::Plugins.run_plugin(plugin_path, stdin_str)
|
|
221
|
+
returned = Array(NA::Plugins.parse_actions(stdout, format: output_fmt, divider: divider))
|
|
222
|
+
index = {}
|
|
223
|
+
todo.actions.each { |a| index["#{a.file_path}:#{a.file_line}"] = a }
|
|
224
|
+
returned.each do |h|
|
|
225
|
+
key = "#{h['file_path']}:#{h['line'].to_i}"
|
|
226
|
+
a = index[key]
|
|
227
|
+
next unless a
|
|
228
|
+
new_text = h['text'].to_s
|
|
229
|
+
new_note = h['note'].to_s
|
|
230
|
+
new_tags = Array(h['tags']).map { |t| [t['name'].to_s, t['value'].to_s] }
|
|
231
|
+
new_text = new_text.gsub(/(?<=\A| )@\S+(?:\(.*?\))?/, '')
|
|
232
|
+
unless new_tags.empty?
|
|
233
|
+
tag_str = new_tags.map { |k, v| v.to_s.empty? ? "@#{k}" : "@#{k}(#{v})" }.join(' ')
|
|
234
|
+
new_text = new_text.strip + (tag_str.empty? ? '' : " #{tag_str}")
|
|
235
|
+
end
|
|
236
|
+
a.action = new_text
|
|
237
|
+
a.note = new_note.empty? ? [] : new_note.split("\n")
|
|
238
|
+
a.instance_variable_set(:@tags, a.scan_tags)
|
|
239
|
+
parents = Array(h['parents']).map(&:to_s)
|
|
240
|
+
if parents.any?
|
|
241
|
+
new_proj = parents.first.to_s
|
|
242
|
+
new_chain = parents[1..] || []
|
|
243
|
+
a.instance_variable_set(:@project, new_proj)
|
|
244
|
+
a.parent = new_chain
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
187
250
|
todo.actions.output(depth,
|
|
188
251
|
{ files: todo.files,
|
|
189
252
|
regexes: regexes,
|
data/bin/commands/update.rb
CHANGED
|
@@ -9,6 +9,21 @@ class App
|
|
|
9
9
|
allow you to pick which file to act on.'
|
|
10
10
|
arg_name 'ACTION'
|
|
11
11
|
command %i[update] do |c|
|
|
12
|
+
c.desc 'Run a plugin by name on selected actions'
|
|
13
|
+
c.arg_name 'NAME'
|
|
14
|
+
c.flag %i[plugin]
|
|
15
|
+
|
|
16
|
+
c.desc 'Plugin input format (json|yaml|csv|text)'
|
|
17
|
+
c.arg_name 'TYPE'
|
|
18
|
+
c.flag %i[input]
|
|
19
|
+
|
|
20
|
+
c.desc 'Plugin output format (json|yaml|csv|text)'
|
|
21
|
+
c.arg_name 'TYPE'
|
|
22
|
+
c.flag %i[output]
|
|
23
|
+
|
|
24
|
+
c.desc 'Divider string for text IO'
|
|
25
|
+
c.arg_name 'STRING'
|
|
26
|
+
c.flag %i[divider]
|
|
12
27
|
c.desc 'Started time (natural language or ISO)'
|
|
13
28
|
c.arg_name 'DATE'
|
|
14
29
|
c.flag %i[started], type: :date_begin
|
|
@@ -329,6 +344,17 @@ class App
|
|
|
329
344
|
{ key: :archive, label: 'Archive', param: nil },
|
|
330
345
|
{ key: :note, label: 'Add Note', param: 'Note' }
|
|
331
346
|
]
|
|
347
|
+
# Append available plugins
|
|
348
|
+
begin
|
|
349
|
+
NA::Plugins.ensure_plugins_home
|
|
350
|
+
NA::Plugins.list_plugins.each do |_key, path|
|
|
351
|
+
meta = NA::Plugins.parse_plugin_metadata(path)
|
|
352
|
+
disp = meta['name'] || File.basename(path, File.extname(path))
|
|
353
|
+
actions_menu << { key: :_plugin, label: "Plugin: #{disp}", param: nil, plugin_path: path }
|
|
354
|
+
end
|
|
355
|
+
rescue StandardError
|
|
356
|
+
# ignore plugin discovery errors in menu
|
|
357
|
+
end
|
|
332
358
|
selector = nil
|
|
333
359
|
if TTY::Which.exist?('fzf')
|
|
334
360
|
selector = 'fzf --prompt="Select action> "'
|
|
@@ -441,6 +467,9 @@ class App
|
|
|
441
467
|
when :note
|
|
442
468
|
options[:note] = true
|
|
443
469
|
note = [param_value]
|
|
470
|
+
when :_plugin
|
|
471
|
+
# Set plugin path directly
|
|
472
|
+
options[:plugin] = action_obj[:plugin_path]
|
|
444
473
|
end
|
|
445
474
|
end
|
|
446
475
|
did_direct_update = false
|
|
@@ -453,7 +482,31 @@ class App
|
|
|
453
482
|
actions_by_file[file] << targets_for_selection[idx][:action]
|
|
454
483
|
end
|
|
455
484
|
|
|
456
|
-
#
|
|
485
|
+
# If a plugin is specified, run it on all selected actions and apply results
|
|
486
|
+
if options[:plugin]
|
|
487
|
+
plugin_path = options[:plugin]
|
|
488
|
+
unless File.exist?(plugin_path)
|
|
489
|
+
# Resolve by name via registry
|
|
490
|
+
resolved = NA::Plugins.resolve_plugin(plugin_path)
|
|
491
|
+
plugin_path = resolved if resolved
|
|
492
|
+
end
|
|
493
|
+
meta = NA::Plugins.parse_plugin_metadata(plugin_path)
|
|
494
|
+
input_fmt = (options[:input] || meta['input'] || 'json').to_s
|
|
495
|
+
output_fmt = (options[:output] || meta['output'] || input_fmt).to_s
|
|
496
|
+
divider = (options[:divider] || '||')
|
|
497
|
+
|
|
498
|
+
all_actions = []
|
|
499
|
+
actions_by_file.each_value { |list| all_actions.concat(list) }
|
|
500
|
+
io_actions = all_actions.map(&:to_plugin_io_hash)
|
|
501
|
+
stdin_str = NA::Plugins.serialize_actions(io_actions, format: input_fmt, divider: divider)
|
|
502
|
+
stdout = NA::Plugins.run_plugin(plugin_path, stdin_str)
|
|
503
|
+
returned = NA::Plugins.parse_actions(stdout, format: output_fmt, divider: divider)
|
|
504
|
+
Array(returned).each { |h| NA.apply_plugin_result(h) }
|
|
505
|
+
did_direct_update = true
|
|
506
|
+
next
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Process each file's actions (non-plugin paths)
|
|
457
510
|
actions_by_file.each do |file, action_list|
|
|
458
511
|
# Rebuild all derived variables from options after menu-driven assignment
|
|
459
512
|
add_tags = options[:tag] ? options[:tag].join(',').split(/ *, */).map { |t| t.sub(/^@/, '') } : []
|
data/bin/na
CHANGED
|
@@ -113,6 +113,7 @@ class App
|
|
|
113
113
|
|
|
114
114
|
pre do |global, _command, _options, _args|
|
|
115
115
|
NA.move_deprecated_backups
|
|
116
|
+
NA::Plugins.ensure_plugins_home
|
|
116
117
|
NA.verbose = global[:debug]
|
|
117
118
|
NA::Pager.paginate = global[:pager] && $stdout.isatty
|
|
118
119
|
NA::Color.coloring = global[:color] && $stdout.isatty
|
data/lib/na/action.rb
CHANGED
|
@@ -28,6 +28,19 @@ module NA
|
|
|
28
28
|
@note = note
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Convert action to plugin IO hash
|
|
32
|
+
# @return [Hash]
|
|
33
|
+
def to_plugin_io_hash
|
|
34
|
+
{
|
|
35
|
+
'file_path' => file_path,
|
|
36
|
+
'line' => file_line,
|
|
37
|
+
'parents' => [@project].concat(@parent),
|
|
38
|
+
'text' => @action.dup,
|
|
39
|
+
'note' => @note.join("\n"),
|
|
40
|
+
'tags' => @tags.map { |k, v| { 'name' => k, 'value' => (v || '').to_s } }
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
31
44
|
# Extract file path and line number from PATH:LINE format
|
|
32
45
|
#
|
|
33
46
|
# @return [Array] [file_path, line_number]
|
data/lib/na/next_action.rb
CHANGED
|
@@ -3,6 +3,98 @@
|
|
|
3
3
|
# Next Action methods
|
|
4
4
|
module NA
|
|
5
5
|
class << self
|
|
6
|
+
# Select actions across files using existing search pipeline
|
|
7
|
+
# @return [Array<NA::Action>]
|
|
8
|
+
def select_actions(file: nil, depth: 1, search: [], tagged: [], include_done: false)
|
|
9
|
+
files = if file
|
|
10
|
+
[file]
|
|
11
|
+
else
|
|
12
|
+
find_files(depth: depth)
|
|
13
|
+
end
|
|
14
|
+
out = []
|
|
15
|
+
files.each do |f|
|
|
16
|
+
_projects, actions = find_actions(f, search, tagged, done: include_done, all: true)
|
|
17
|
+
out.concat(actions) if actions
|
|
18
|
+
end
|
|
19
|
+
out
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Apply a plugin result hash back to the underlying file
|
|
23
|
+
# - Move if parents changed (project path differs)
|
|
24
|
+
# - Update text/note/tags
|
|
25
|
+
def apply_plugin_result(io_hash)
|
|
26
|
+
file = io_hash['file_path']
|
|
27
|
+
line = io_hash['line'].to_i
|
|
28
|
+
parents = Array(io_hash['parents']).map(&:to_s)
|
|
29
|
+
text = io_hash['text'].to_s
|
|
30
|
+
note = io_hash['note'].to_s
|
|
31
|
+
tags = Array(io_hash['tags']).to_h { |t| [t['name'].to_s, t['value'].to_s] }
|
|
32
|
+
action_block = io_hash['action'] || { 'action' => 'UPDATE', 'arguments' => [] }
|
|
33
|
+
action_name = action_block['action'].to_s.upcase
|
|
34
|
+
action_args = Array(action_block['arguments'])
|
|
35
|
+
|
|
36
|
+
# Load current action
|
|
37
|
+
_projects, actions = find_actions(file, nil, nil, all: true, done: true, project: nil, search_note: true, target_line: line)
|
|
38
|
+
action = actions&.first
|
|
39
|
+
return unless action
|
|
40
|
+
|
|
41
|
+
# Determine new project path from parents array
|
|
42
|
+
new_project = ''
|
|
43
|
+
new_parent_chain = []
|
|
44
|
+
if parents.any?
|
|
45
|
+
new_project = parents.first.to_s
|
|
46
|
+
new_parent_chain = parents[1..] || []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
case action_name
|
|
50
|
+
when 'DELETE'
|
|
51
|
+
update_action(file, { target_line: line }, delete: true, all: true)
|
|
52
|
+
return
|
|
53
|
+
when 'COMPLETE'
|
|
54
|
+
update_action(file, { target_line: line }, finish: true, all: true)
|
|
55
|
+
return
|
|
56
|
+
when 'RESTORE'
|
|
57
|
+
update_action(file, { target_line: line }, restore: true, all: true)
|
|
58
|
+
return
|
|
59
|
+
when 'ARCHIVE'
|
|
60
|
+
update_action(file, { target_line: line }, finish: true, move: 'Archive', all: true)
|
|
61
|
+
return
|
|
62
|
+
when 'ADD_TAG'
|
|
63
|
+
add_tags = action_args.map { |t| t.sub(/^@/, '') }
|
|
64
|
+
update_action(file, { target_line: line }, add: action, add_tag: add_tags, all: true)
|
|
65
|
+
return
|
|
66
|
+
when 'DELETE_TAG', 'REMOVE_TAG'
|
|
67
|
+
remove_tags = action_args.map { |t| t.sub(/^@/, '') }
|
|
68
|
+
update_action(file, { target_line: line }, add: action, remove_tag: remove_tags, all: true)
|
|
69
|
+
return
|
|
70
|
+
when 'MOVE'
|
|
71
|
+
move_to = action_args.first.to_s
|
|
72
|
+
update_action(file, { target_line: line }, add: action, move: move_to, all: true)
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Replace content on the existing action then write back in-place
|
|
77
|
+
original_line = action.file_line
|
|
78
|
+
action.action = text
|
|
79
|
+
action.note = note.to_s.split("\n")
|
|
80
|
+
action.action.gsub!(/(?<=\A| )@\S+(?:\(.*?\))?/, '')
|
|
81
|
+
unless tags.empty?
|
|
82
|
+
tag_str = tags.map { |k, v| v.to_s.empty? ? "@#{k}" : "@#{k}(#{v})" }.join(' ')
|
|
83
|
+
action.action = action.action.strip + (tag_str.empty? ? "" : " #{tag_str}")
|
|
84
|
+
end
|
|
85
|
+
# Ensure we update this exact action in-place
|
|
86
|
+
update_action(file, { target_line: original_line }, add: action, all: true)
|
|
87
|
+
|
|
88
|
+
# If parents changed, set move target
|
|
89
|
+
move_to = nil
|
|
90
|
+
move_to = ([new_project] + new_parent_chain).join(':') if new_project.to_s.strip != action.project || new_parent_chain != action.parent
|
|
91
|
+
|
|
92
|
+
update_action(file, nil,
|
|
93
|
+
add: action,
|
|
94
|
+
project: action.project,
|
|
95
|
+
overwrite: true,
|
|
96
|
+
move: move_to)
|
|
97
|
+
end
|
|
6
98
|
include NA::Editor
|
|
7
99
|
|
|
8
100
|
attr_accessor :verbose, :extension, :include_ext, :na_tag, :command_line, :command, :globals, :global_file,
|