na 1.2.85 → 1.2.86
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/priority35m36m335m32m.md +0 -0
- data/.rubocop_todo.yml +29 -29
- data/CHANGELOG.md +4 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +9 -1
- data/README.md +2 -2
- data/bin/commands/update.rb +120 -37
- data/lib/na/action.rb +64 -14
- data/lib/na/editor.rb +80 -1
- data/lib/na/next_action.rb +48 -17
- data/lib/na/string.rb +7 -3
- data/lib/na/theme.rb +50 -41
- data/lib/na/version.rb +1 -1
- data/lib/na.rb +11 -1
- data/src/_README.md +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 05b84c85234711e95bc6fae91b279577d0908ba7bc0b1fb254729d65c2823f99
|
|
4
|
+
data.tar.gz: acf70b87b92d32a81ae7802397e3f72fd72b28410c50556c680ad0f05c6ea21f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 627981de849f31ca58ba38ff7e40915baf7cbf32f4a78d005f35a2590b853a477e28e71c209b5974cb9f4eae311d3f9e4c14c4fd690a253c16584bcac7d48fb1
|
|
7
|
+
data.tar.gz: 362e2719a19069a27cad8937d3ed7cabd81a7fd57a2b9f6a1dfa72ff439adf3fd64e691f651ad46020cdd4cdd08d0b6161b93adfcaf75c96040cf75451397a2f
|
|
File without changes
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2025-10-
|
|
3
|
+
# on 2025-10-27 21:36:52 UTC using RuboCop version 1.75.7.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -21,46 +21,53 @@ Lint/UnusedMethodArgument:
|
|
|
21
21
|
Exclude:
|
|
22
22
|
- 'lib/na/string.rb'
|
|
23
23
|
|
|
24
|
-
# Offense count:
|
|
24
|
+
# Offense count: 36
|
|
25
25
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
|
26
26
|
Metrics/AbcSize:
|
|
27
|
-
Max:
|
|
27
|
+
Max: 243
|
|
28
28
|
|
|
29
|
-
# Offense count:
|
|
29
|
+
# Offense count: 9
|
|
30
30
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
31
31
|
# AllowedMethods: refine
|
|
32
32
|
Metrics/BlockLength:
|
|
33
33
|
Max: 144
|
|
34
34
|
|
|
35
|
-
# Offense count:
|
|
35
|
+
# Offense count: 5
|
|
36
36
|
# Configuration parameters: CountComments, CountAsOne.
|
|
37
37
|
Metrics/ClassLength:
|
|
38
|
-
Max:
|
|
38
|
+
Max: 777
|
|
39
39
|
|
|
40
|
-
# Offense count:
|
|
40
|
+
# Offense count: 25
|
|
41
41
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
42
42
|
Metrics/CyclomaticComplexity:
|
|
43
|
-
Max:
|
|
43
|
+
Max: 70
|
|
44
44
|
|
|
45
|
-
# Offense count:
|
|
45
|
+
# Offense count: 40
|
|
46
46
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
47
47
|
Metrics/MethodLength:
|
|
48
48
|
Max: 146
|
|
49
49
|
|
|
50
|
-
# Offense count:
|
|
50
|
+
# Offense count: 3
|
|
51
51
|
# Configuration parameters: CountComments, CountAsOne.
|
|
52
52
|
Metrics/ModuleLength:
|
|
53
|
-
Max:
|
|
53
|
+
Max: 779
|
|
54
54
|
|
|
55
55
|
# Offense count: 4
|
|
56
56
|
# Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
|
|
57
57
|
Metrics/ParameterLists:
|
|
58
58
|
Max: 19
|
|
59
59
|
|
|
60
|
-
# Offense count:
|
|
60
|
+
# Offense count: 24
|
|
61
61
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
62
62
|
Metrics/PerceivedComplexity:
|
|
63
|
-
Max:
|
|
63
|
+
Max: 81
|
|
64
|
+
|
|
65
|
+
# Offense count: 1
|
|
66
|
+
# Configuration parameters: ForbiddenDelimiters.
|
|
67
|
+
# ForbiddenDelimiters: (?i-mx:(^|\s)(EO[A-Z]{1}|END)(\s|$))
|
|
68
|
+
Naming/HeredocDelimiterNaming:
|
|
69
|
+
Exclude:
|
|
70
|
+
- 'lib/na/editor.rb'
|
|
64
71
|
|
|
65
72
|
# Offense count: 3
|
|
66
73
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
@@ -69,21 +76,6 @@ Security/YAMLLoad:
|
|
|
69
76
|
- 'lib/na/next_action.rb'
|
|
70
77
|
- 'lib/na/theme.rb'
|
|
71
78
|
|
|
72
|
-
# Offense count: 8
|
|
73
|
-
# Configuration parameters: AllowedConstants.
|
|
74
|
-
Style/Documentation:
|
|
75
|
-
Exclude:
|
|
76
|
-
- 'spec/**/*'
|
|
77
|
-
- 'test/**/*'
|
|
78
|
-
- 'lib/na/action.rb'
|
|
79
|
-
- 'lib/na/array.rb'
|
|
80
|
-
- 'lib/na/benchmark.rb'
|
|
81
|
-
- 'lib/na/editor.rb'
|
|
82
|
-
- 'lib/na/hash.rb'
|
|
83
|
-
- 'lib/na/project.rb'
|
|
84
|
-
- 'lib/na/theme.rb'
|
|
85
|
-
- 'lib/na/todo.rb'
|
|
86
|
-
|
|
87
79
|
# Offense count: 2
|
|
88
80
|
# This cop supports safe autocorrection (--autocorrect).
|
|
89
81
|
Style/IfUnlessModifier:
|
|
@@ -99,13 +91,21 @@ Style/ModuleFunction:
|
|
|
99
91
|
Exclude:
|
|
100
92
|
- 'lib/na/colors.rb'
|
|
101
93
|
|
|
94
|
+
# Offense count: 1
|
|
95
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
96
|
+
# Configuration parameters: EnforcedStyle.
|
|
97
|
+
# SupportedStyles: single_quotes, double_quotes
|
|
98
|
+
Style/StringLiteralsInInterpolation:
|
|
99
|
+
Exclude:
|
|
100
|
+
- 'lib/na/action.rb'
|
|
101
|
+
|
|
102
102
|
# Offense count: 1
|
|
103
103
|
# This cop supports safe autocorrection (--autocorrect).
|
|
104
104
|
Style/YAMLFileRead:
|
|
105
105
|
Exclude:
|
|
106
106
|
- 'lib/na/theme.rb'
|
|
107
107
|
|
|
108
|
-
# Offense count:
|
|
108
|
+
# Offense count: 22
|
|
109
109
|
# This cop supports safe autocorrection (--autocorrect).
|
|
110
110
|
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
|
|
111
111
|
# URISchemes: http, https
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
na (1.2.
|
|
4
|
+
na (1.2.86)
|
|
5
5
|
chronic (~> 0.10, >= 0.10.2)
|
|
6
6
|
git (~> 3.0.0)
|
|
7
7
|
gli (~> 2.21.0)
|
|
@@ -36,6 +36,7 @@ GEM
|
|
|
36
36
|
concurrent-ruby (1.3.5)
|
|
37
37
|
connection_pool (2.5.4)
|
|
38
38
|
diff-lcs (1.6.2)
|
|
39
|
+
docile (1.4.1)
|
|
39
40
|
drb (2.2.3)
|
|
40
41
|
git (3.0.2)
|
|
41
42
|
activesupport (>= 5.0)
|
|
@@ -69,6 +70,12 @@ GEM
|
|
|
69
70
|
rspec-support (~> 3.13.0)
|
|
70
71
|
rspec-support (3.13.6)
|
|
71
72
|
securerandom (0.4.1)
|
|
73
|
+
simplecov (0.22.0)
|
|
74
|
+
docile (~> 1.1)
|
|
75
|
+
simplecov-html (~> 0.11)
|
|
76
|
+
simplecov_json_formatter (~> 0.1)
|
|
77
|
+
simplecov-html (0.13.2)
|
|
78
|
+
simplecov_json_formatter (0.1.4)
|
|
72
79
|
tty-cursor (0.7.1)
|
|
73
80
|
tty-reader (0.9.0)
|
|
74
81
|
tty-cursor (~> 0.7)
|
|
@@ -103,6 +110,7 @@ DEPENDENCIES
|
|
|
103
110
|
rake
|
|
104
111
|
rdoc (~> 4.3)
|
|
105
112
|
rspec (~> 3.0)
|
|
113
|
+
simplecov (~> 0.22.0)
|
|
106
114
|
tty-spinner (~> 0.9, >= 0.9.0)
|
|
107
115
|
|
|
108
116
|
BUNDLED WITH
|
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.
|
|
12
|
+
The current version of `na` is 1.2.86.
|
|
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.
|
|
79
|
+
1.2.86
|
|
80
80
|
|
|
81
81
|
GLOBAL OPTIONS
|
|
82
82
|
-a, --add - Add a next action (deprecated, for backwards compatibility)
|
data/bin/commands/update.rb
CHANGED
|
@@ -164,13 +164,33 @@ class App
|
|
|
164
164
|
|
|
165
165
|
options[:exact] = true unless options[:replace].nil?
|
|
166
166
|
|
|
167
|
+
# Check for PATH:LINE format in arguments
|
|
168
|
+
target_file = nil
|
|
169
|
+
target_line = nil
|
|
170
|
+
if args.count.positive?
|
|
171
|
+
pathline_match = args.join(' ').strip.match(/^(.+):(\d+)$/)
|
|
172
|
+
if pathline_match
|
|
173
|
+
target_file = pathline_match[1]
|
|
174
|
+
target_line = pathline_match[2].to_i
|
|
175
|
+
|
|
176
|
+
# Verify file exists
|
|
177
|
+
if File.exist?(target_file)
|
|
178
|
+
options[:file] = target_file
|
|
179
|
+
options[:target_line] = target_line
|
|
180
|
+
action = nil # Skip search processing
|
|
181
|
+
else
|
|
182
|
+
NA.notify("#{NA.theme[:error]}File not found: #{target_file}", exit_code: 1)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
167
187
|
if args.count.positive?
|
|
168
188
|
action = args.join(' ').strip
|
|
169
189
|
else
|
|
170
190
|
action = nil
|
|
171
191
|
end
|
|
172
192
|
tokens = nil
|
|
173
|
-
if action && !action.empty?
|
|
193
|
+
if action && !action.empty? && !target_line
|
|
174
194
|
if options[:exact]
|
|
175
195
|
tokens = action
|
|
176
196
|
elsif options[:regex]
|
|
@@ -218,10 +238,11 @@ class App
|
|
|
218
238
|
done: options[:done]
|
|
219
239
|
})
|
|
220
240
|
todo.actions.each do |action_obj|
|
|
221
|
-
# Format: filename:
|
|
222
|
-
display
|
|
241
|
+
# Format: filename:LINENUM:parent > action
|
|
242
|
+
# Include line number in display for unique matching
|
|
243
|
+
display = "#{File.basename(action_obj.file_path)}:#{action_obj.file_line}:#{action_obj.parent.join('>')} | #{action_obj.action}"
|
|
223
244
|
candidate_actions << display
|
|
224
|
-
targets_for_selection << { file: action_obj.
|
|
245
|
+
targets_for_selection << { file: action_obj.file_path, line: action_obj.file_line, action: action_obj }
|
|
225
246
|
end
|
|
226
247
|
end
|
|
227
248
|
|
|
@@ -237,9 +258,24 @@ class App
|
|
|
237
258
|
if selector
|
|
238
259
|
require 'open3'
|
|
239
260
|
input = candidate_actions.join("\n")
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
261
|
+
|
|
262
|
+
# Use popen3 to properly handle stdin for fzf
|
|
263
|
+
Open3.popen3(selector) do |stdin, stdout, stderr, wait_thr|
|
|
264
|
+
stdin.write(input)
|
|
265
|
+
stdin.close
|
|
266
|
+
|
|
267
|
+
output = stdout.read
|
|
268
|
+
|
|
269
|
+
selected = output.split("\n").map(&:strip).reject(&:empty?)
|
|
270
|
+
|
|
271
|
+
# Track which candidates have been matched to avoid duplicates
|
|
272
|
+
selected_indices = []
|
|
273
|
+
candidate_actions.each_index do |i|
|
|
274
|
+
if selected.include?(candidate_actions[i])
|
|
275
|
+
selected_indices << i unless selected_indices.include?(i)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
243
279
|
else
|
|
244
280
|
# Fallback: select all or prompt for search string
|
|
245
281
|
selected_indices = (0...candidate_actions.size).to_a
|
|
@@ -326,12 +362,7 @@ class App
|
|
|
326
362
|
when :finish
|
|
327
363
|
options[:finish] = true
|
|
328
364
|
when :edit
|
|
329
|
-
#
|
|
330
|
-
edit_action = targets_for_selection[selected_indices.first][:action]
|
|
331
|
-
editor_content = "#{edit_action.action}\n#{edit_action.note.join("\n")}"
|
|
332
|
-
new_action, new_note = NA::Editor.format_input(NA::Editor.fork_editor(editor_content))
|
|
333
|
-
edit_action.action = new_action
|
|
334
|
-
edit_action.note = new_note
|
|
365
|
+
# Just set the flag - multi-action editor will handle it below
|
|
335
366
|
options[:edit] = true
|
|
336
367
|
when :priority
|
|
337
368
|
options[:priority] = param_value
|
|
@@ -384,7 +415,17 @@ class App
|
|
|
384
415
|
end
|
|
385
416
|
end
|
|
386
417
|
did_direct_update = false
|
|
418
|
+
|
|
419
|
+
# Group selected actions by file for batch processing
|
|
420
|
+
actions_by_file = {}
|
|
387
421
|
selected_indices.each do |idx|
|
|
422
|
+
file = targets_for_selection[idx][:file]
|
|
423
|
+
actions_by_file[file] ||= []
|
|
424
|
+
actions_by_file[file] << targets_for_selection[idx][:action]
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Process each file's actions
|
|
428
|
+
actions_by_file.each do |file, action_list|
|
|
388
429
|
# Rebuild all derived variables from options after menu-driven assignment
|
|
389
430
|
add_tags = options[:tag] ? options[:tag].join(',').split(/ *, */).map { |t| t.sub(/^@/, '') } : []
|
|
390
431
|
remove_tags = options[:remove] ? options[:remove].join(',').split(/ *, */).map { |t| t.sub(/^@/, '') } : []
|
|
@@ -399,29 +440,64 @@ class App
|
|
|
399
440
|
if options[:note] && defined?(param_value) && param_value
|
|
400
441
|
note_val = [param_value]
|
|
401
442
|
end
|
|
402
|
-
|
|
403
|
-
#
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
443
|
+
|
|
444
|
+
# Handle edit with multiple actions
|
|
445
|
+
if options[:edit]
|
|
446
|
+
# Open editor once with all actions for this file
|
|
447
|
+
editor_content = NA::Editor.format_multi_action_input(action_list)
|
|
448
|
+
edited_content = NA::Editor.fork_editor(editor_content)
|
|
449
|
+
edited_actions = NA::Editor.parse_multi_action_output(edited_content)
|
|
450
|
+
|
|
451
|
+
# If markers were removed but we have the same number of actions, match by position
|
|
452
|
+
if edited_actions.empty? && action_list.size > 0
|
|
453
|
+
# Parse content line by line, skipping comments and blanks
|
|
454
|
+
non_comment_lines = edited_content.lines.map(&:strip).reject { |l| l.empty? || l.start_with?('#') }
|
|
455
|
+
|
|
456
|
+
# Match each non-comment line to an action by position
|
|
457
|
+
action_list.each_with_index do |action_obj, idx|
|
|
458
|
+
if non_comment_lines[idx]
|
|
459
|
+
# Split into action and notes
|
|
460
|
+
lines = non_comment_lines[idx..-1]
|
|
461
|
+
action_text = lines[0]
|
|
462
|
+
note_lines = lines[1..-1] || []
|
|
463
|
+
|
|
464
|
+
# Store by file:line key
|
|
465
|
+
key = "#{action_obj.file_path}:#{action_obj.file_line}"
|
|
466
|
+
edited_actions[key] = [action_text, note_lines]
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Update each action with edited content
|
|
472
|
+
action_list.each do |action_obj|
|
|
473
|
+
key = "#{action_obj.file_path}:#{action_obj.file_line}"
|
|
474
|
+
if edited_actions[key]
|
|
475
|
+
action_obj.action, action_obj.note = edited_actions[key]
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Update each action
|
|
481
|
+
action_list.each do |action_obj|
|
|
482
|
+
NA.update_action(file, nil,
|
|
483
|
+
add: action_obj,
|
|
484
|
+
add_tag: add_tags,
|
|
485
|
+
all: true,
|
|
486
|
+
append: append,
|
|
487
|
+
delete: options[:delete],
|
|
488
|
+
done: options[:done],
|
|
489
|
+
edit: false, # Already handled above
|
|
490
|
+
finish: options[:finish],
|
|
491
|
+
move: target_proj,
|
|
492
|
+
note: note_val,
|
|
493
|
+
overwrite: options[:overwrite],
|
|
494
|
+
priority: priority,
|
|
495
|
+
project: options[:project],
|
|
496
|
+
remove_tag: remove_tags,
|
|
497
|
+
replace: options[:replace],
|
|
498
|
+
search_note: options[:search_notes],
|
|
499
|
+
tagged: nil)
|
|
500
|
+
end
|
|
425
501
|
did_direct_update = true
|
|
426
502
|
end
|
|
427
503
|
if did_direct_update
|
|
@@ -539,8 +615,15 @@ class App
|
|
|
539
615
|
|
|
540
616
|
NA.notify("#{NA.theme[:error]}No search terms provided", exit_code: 1) if tokens.nil? && options[:tagged].empty?
|
|
541
617
|
|
|
618
|
+
# Handle target_line if provided (from PATH:LINE format)
|
|
619
|
+
search_tokens = if options[:target_line]
|
|
620
|
+
{ target_line: options[:target_line] }
|
|
621
|
+
else
|
|
622
|
+
tokens
|
|
623
|
+
end
|
|
624
|
+
|
|
542
625
|
targets.each do |target|
|
|
543
|
-
NA.update_action(target,
|
|
626
|
+
NA.update_action(target, search_tokens,
|
|
544
627
|
add_tag: add_tags,
|
|
545
628
|
all: options[:all],
|
|
546
629
|
append: append,
|
data/lib/na/action.rb
CHANGED
|
@@ -14,7 +14,12 @@ module NA
|
|
|
14
14
|
def initialize(file, project, parent, action, idx, note = [])
|
|
15
15
|
super()
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
# Store file in PATH:LINE format if line is available
|
|
18
|
+
@file = if idx.is_a?(Integer)
|
|
19
|
+
"#{file}:#{idx}"
|
|
20
|
+
else
|
|
21
|
+
file
|
|
22
|
+
end
|
|
18
23
|
@project = project
|
|
19
24
|
@parent = parent
|
|
20
25
|
@action = action.gsub('{', '\\{')
|
|
@@ -23,6 +28,32 @@ module NA
|
|
|
23
28
|
@note = note
|
|
24
29
|
end
|
|
25
30
|
|
|
31
|
+
# Extract file path and line number from PATH:LINE format
|
|
32
|
+
#
|
|
33
|
+
# @return [Array] [file_path, line_number]
|
|
34
|
+
def file_line_parts
|
|
35
|
+
if @file.to_s.include?(':')
|
|
36
|
+
path, line = @file.split(':', 2)
|
|
37
|
+
[path, line.to_i]
|
|
38
|
+
else
|
|
39
|
+
[@file, @line]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get just the file path without line number
|
|
44
|
+
#
|
|
45
|
+
# @return [String] File path
|
|
46
|
+
def file_path
|
|
47
|
+
file_line_parts.first
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get the line number
|
|
51
|
+
#
|
|
52
|
+
# @return [Integer] Line number
|
|
53
|
+
def file_line
|
|
54
|
+
file_line_parts.last
|
|
55
|
+
end
|
|
56
|
+
|
|
26
57
|
# Update the action string and note with priority, tags, and completion status
|
|
27
58
|
#
|
|
28
59
|
# @param priority [Integer] Priority value to set
|
|
@@ -70,7 +101,7 @@ module NA
|
|
|
70
101
|
else
|
|
71
102
|
''
|
|
72
103
|
end
|
|
73
|
-
"(#{
|
|
104
|
+
"(#{file_path}:#{file_line}) #{@project}:#{@parent.join(">")} | #{@action}#{note}"
|
|
74
105
|
end
|
|
75
106
|
|
|
76
107
|
# Pretty string representation of the action with color formatting
|
|
@@ -82,7 +113,7 @@ module NA
|
|
|
82
113
|
else
|
|
83
114
|
''
|
|
84
115
|
end
|
|
85
|
-
"{x}#{NA.theme[:filename]}#{File.basename(
|
|
116
|
+
"{x}#{NA.theme[:filename]}#{File.basename(file_path)}:#{file_line}{x}#{NA.theme[:bracket]}[{x}#{NA.theme[:project]}#{@project}:#{@parent.join('>')}{x}#{NA.theme[:bracket]}]{x} | #{NA.theme[:action]}#{@action}{x}#{NA.theme[:note]}#{note}"
|
|
86
117
|
end
|
|
87
118
|
|
|
88
119
|
# Inspect the action object
|
|
@@ -112,49 +143,67 @@ module NA
|
|
|
112
143
|
NA::Benchmark.measure('Action.pretty') do
|
|
113
144
|
# Use cached theme instead of loading every time
|
|
114
145
|
theme = NA.theme
|
|
115
|
-
|
|
146
|
+
# Merge templates if provided
|
|
147
|
+
if template[:templates]
|
|
148
|
+
theme = theme.dup
|
|
149
|
+
theme[:templates] = theme[:templates].merge(template[:templates])
|
|
150
|
+
template = theme.merge(template.reject { |k| k == :templates })
|
|
151
|
+
else
|
|
152
|
+
template = theme.merge(template)
|
|
153
|
+
end
|
|
116
154
|
|
|
117
155
|
# Pre-compute common template parts to avoid repeated processing
|
|
118
156
|
output_template = template[:templates][:output]
|
|
119
157
|
needs_filename = output_template.include?('%filename')
|
|
158
|
+
needs_line = output_template.include?('%line')
|
|
120
159
|
needs_parents = output_template.include?('%parents') || output_template.include?('%parent')
|
|
121
160
|
needs_project = output_template.include?('%project')
|
|
122
161
|
|
|
123
162
|
# Create the hierarchical parent string (optimized)
|
|
124
163
|
parents = if needs_parents && @parent.any?
|
|
125
164
|
parent_parts = @parent.map { |par| "#{template[:parent]}#{par}" }.join(template[:parent_divider])
|
|
126
|
-
|
|
165
|
+
# parent_parts already has color codes embedded, create final string directly
|
|
166
|
+
"#{NA::Color.template("{x}#{template[:bracket]}[")}#{parent_parts}#{NA::Color.template("{x}#{template[:bracket]}]{x}")}"
|
|
127
167
|
else
|
|
128
168
|
''
|
|
129
169
|
end
|
|
130
170
|
|
|
131
|
-
# Create the project string (optimized)
|
|
171
|
+
# Create the project string (optimized) - Ensure color reset before project
|
|
132
172
|
project = if needs_project && !@project.empty?
|
|
133
|
-
NA::Color.template("{x}#{template[:project]}#{@project}{x}
|
|
173
|
+
NA::Color.template("{x}#{template[:project]}#{@project}{x}")
|
|
134
174
|
else
|
|
135
175
|
''
|
|
136
176
|
end
|
|
137
177
|
|
|
138
178
|
# Create the source filename string (optimized)
|
|
139
179
|
filename = if needs_filename
|
|
140
|
-
|
|
180
|
+
path_only = file_path # Extract just the path from PATH:LINE
|
|
181
|
+
path = path_only.sub(%r{^\./}, '').sub(/#{Dir.home}/, '~')
|
|
141
182
|
if File.dirname(path) == '.'
|
|
142
183
|
fname = NA.include_ext ? File.basename(path) : File.basename(path, ".#{extension}")
|
|
143
184
|
fname = "./#{fname}" if NA.show_cwd_indicator
|
|
144
|
-
NA::Color.template("#{template[:filename]}#{fname}
|
|
185
|
+
NA::Color.template("#{template[:filename]}#{fname}{x}")
|
|
145
186
|
else
|
|
146
187
|
colored = (NA.include_ext ? path : path.sub(/\.#{extension}$/, '')).highlight_filename
|
|
147
|
-
NA::Color.template("#{template[:filename]}#{colored}
|
|
188
|
+
NA::Color.template("#{template[:filename]}#{colored}{x}")
|
|
148
189
|
end
|
|
149
190
|
else
|
|
150
191
|
''
|
|
151
192
|
end
|
|
152
193
|
|
|
194
|
+
# Create the line number string (optimized)
|
|
195
|
+
line_num = if needs_line && @line
|
|
196
|
+
NA::Color.template("#{template[:line]}:#{@line} {x}")
|
|
197
|
+
else
|
|
198
|
+
''
|
|
199
|
+
end
|
|
200
|
+
|
|
153
201
|
# colorize the action and highlight tags (optimized)
|
|
154
202
|
action_text = @action.dup
|
|
155
203
|
action_text.gsub!(/\{(.*?)\}/, '\\{\1\\}')
|
|
156
204
|
action_text = action_text.sub(/ @#{NA.na_tag}\b/, '')
|
|
157
|
-
action
|
|
205
|
+
# Reset colors before action to prevent bleeding from parents/project
|
|
206
|
+
action = NA::Color.template("{x}#{template[:action]}#{action_text}{x}")
|
|
158
207
|
action = action.highlight_tags(color: template[:tags],
|
|
159
208
|
parens: template[:value_parens],
|
|
160
209
|
value: template[:values],
|
|
@@ -169,8 +218,8 @@ module NA
|
|
|
169
218
|
width = @cached_width ||= TTY::Screen.columns
|
|
170
219
|
# Calculate indent more efficiently - avoid repeated template processing
|
|
171
220
|
base_template = output_template.gsub('%action', '').gsub('%note', '')
|
|
172
|
-
base_output = base_template.gsub('%filename', filename).gsub('%project', project).gsub(/%parents?/,
|
|
173
|
-
|
|
221
|
+
base_output = base_template.gsub('%filename', filename).gsub('%line', line_num).gsub('%project', project).gsub(/%parents?/,
|
|
222
|
+
parents)
|
|
174
223
|
indent = NA::Color.uncolor(NA::Color.template(base_output)).length
|
|
175
224
|
note = NA::Color.template(@note.wrap(width, indent, template[:note]))
|
|
176
225
|
else
|
|
@@ -185,7 +234,7 @@ module NA
|
|
|
185
234
|
if detect_width && !action.empty?
|
|
186
235
|
width = @cached_width ||= TTY::Screen.columns
|
|
187
236
|
base_template = output_template.gsub('%action', '').gsub('%note', '')
|
|
188
|
-
base_output = base_template.gsub('%filename', filename).gsub('%project', project).gsub(/%parents?/, parents)
|
|
237
|
+
base_output = base_template.gsub('%filename', filename).gsub('%line', line_num).gsub('%project', project).gsub(/%parents?/, parents)
|
|
189
238
|
indent = NA::Color.uncolor(NA::Color.template(base_output)).length
|
|
190
239
|
action = action.wrap(width, indent)
|
|
191
240
|
end
|
|
@@ -193,6 +242,7 @@ module NA
|
|
|
193
242
|
# Replace variables in template string and output colorized (optimized)
|
|
194
243
|
final_output = output_template.dup
|
|
195
244
|
final_output.gsub!('%filename', filename)
|
|
245
|
+
final_output.gsub!('%line', line_num)
|
|
196
246
|
final_output.gsub!('%project', project)
|
|
197
247
|
final_output.gsub!(/%parents?/, parents)
|
|
198
248
|
final_output.gsub!('%action', action.highlight_search(regexes))
|
data/lib/na/editor.rb
CHANGED
|
@@ -100,7 +100,12 @@ module NA
|
|
|
100
100
|
tmpfile.unlink
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
# Don't strip comments if this looks like multi-action format (has # ------ markers)
|
|
104
|
+
if input.include?('# ------ ')
|
|
105
|
+
input
|
|
106
|
+
else
|
|
107
|
+
input.split("\n").delete_if(&:ignore?).join("\n")
|
|
108
|
+
end
|
|
104
109
|
end
|
|
105
110
|
|
|
106
111
|
# Takes a multi-line string and formats it as an entry
|
|
@@ -129,6 +134,80 @@ module NA
|
|
|
129
134
|
|
|
130
135
|
[title, note]
|
|
131
136
|
end
|
|
137
|
+
|
|
138
|
+
# Format multiple actions for multi-edit
|
|
139
|
+
# @param actions [Array<Action>] Actions to edit
|
|
140
|
+
# @return [String] Formatted editor content
|
|
141
|
+
def format_multi_action_input(actions)
|
|
142
|
+
header = <<~EOF
|
|
143
|
+
# Instructions:
|
|
144
|
+
# - Edit the action text (the lines WITHOUT # comment markers)
|
|
145
|
+
# - DO NOT remove or edit the lines starting with "# ------"
|
|
146
|
+
# - Add notes on new lines after the action
|
|
147
|
+
# - Blank lines are ignored
|
|
148
|
+
#
|
|
149
|
+
|
|
150
|
+
EOF
|
|
151
|
+
|
|
152
|
+
# Use + to create a mutable string
|
|
153
|
+
content = +header
|
|
154
|
+
|
|
155
|
+
actions.each do |action|
|
|
156
|
+
# Use file_path to get the path and file_line to get the line number
|
|
157
|
+
content << "# ------ #{action.file_path}:#{action.file_line}\n"
|
|
158
|
+
content << "#{action.action}\n"
|
|
159
|
+
content << "#{action.note.join("\n")}\n" if action.note.any?
|
|
160
|
+
content << "\n" # Blank line separator
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
content
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Parse multi-action editor output
|
|
167
|
+
# @param content [String] Editor output
|
|
168
|
+
# @return [Hash] Hash mapping file:line to [action, note]
|
|
169
|
+
def parse_multi_action_output(content)
|
|
170
|
+
results = {}
|
|
171
|
+
current_file = nil
|
|
172
|
+
current_action = nil
|
|
173
|
+
current_note = []
|
|
174
|
+
|
|
175
|
+
content.lines.each do |line|
|
|
176
|
+
stripped = line.strip
|
|
177
|
+
|
|
178
|
+
# Check for file marker: # ------ path:line
|
|
179
|
+
match = stripped.match(/^# ------ (.+?):(\d+)$/)
|
|
180
|
+
if match
|
|
181
|
+
# Save previous action if exists
|
|
182
|
+
results[current_file] = [current_action, current_note] if current_file && current_action
|
|
183
|
+
|
|
184
|
+
# Start new action
|
|
185
|
+
current_file = "#{match[1]}:#{match[2]}"
|
|
186
|
+
current_action = nil
|
|
187
|
+
current_note = []
|
|
188
|
+
next
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Skip other comment lines
|
|
192
|
+
next if stripped.start_with?('#')
|
|
193
|
+
|
|
194
|
+
# Skip blank lines
|
|
195
|
+
next if stripped.empty?
|
|
196
|
+
|
|
197
|
+
# Store as action or note based on what we've seen so far
|
|
198
|
+
if current_action.nil?
|
|
199
|
+
current_action = stripped
|
|
200
|
+
else
|
|
201
|
+
# Subsequent lines are notes
|
|
202
|
+
current_note << stripped
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Save last action
|
|
207
|
+
results[current_file] = [current_action, current_note] if current_file && current_action
|
|
208
|
+
|
|
209
|
+
results
|
|
210
|
+
end
|
|
132
211
|
end
|
|
133
212
|
end
|
|
134
213
|
end
|
data/lib/na/next_action.rb
CHANGED
|
@@ -169,8 +169,9 @@ module NA
|
|
|
169
169
|
# @param done [Boolean] Include done actions
|
|
170
170
|
# @param project [String, nil] Project name
|
|
171
171
|
# @param search_note [Boolean] Search notes
|
|
172
|
+
# @param target_line [Integer] Specific line number to target
|
|
172
173
|
# @return [Array] Projects and actions
|
|
173
|
-
def find_actions(target, search, tagged = nil, all: false, done: false, project: nil, search_note: true)
|
|
174
|
+
def find_actions(target, search, tagged = nil, all: false, done: false, project: nil, search_note: true, target_line: nil)
|
|
174
175
|
todo = NA::Todo.new({ search: search,
|
|
175
176
|
search_note: search_note,
|
|
176
177
|
require_na: false,
|
|
@@ -187,7 +188,17 @@ module NA
|
|
|
187
188
|
|
|
188
189
|
return [todo.projects, todo.actions] if todo.actions.count == 1 || all
|
|
189
190
|
|
|
190
|
-
|
|
191
|
+
# If target_line is specified, find the action at that specific line
|
|
192
|
+
if target_line
|
|
193
|
+
matching_action = todo.actions.find { |a| a.line == target_line }
|
|
194
|
+
return [todo.projects, NA::Actions.new([matching_action])] if matching_action
|
|
195
|
+
|
|
196
|
+
NA.notify("#{NA.theme[:error]}No action found at line #{target_line}", exit_code: 1)
|
|
197
|
+
return [todo.projects, NA::Actions.new]
|
|
198
|
+
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
options = todo.actions.map { |action| "#{action.file} : #{action.action}" }
|
|
191
202
|
res = choose_from(options, prompt: 'Make a selection: ', multiple: true, sorted: true)
|
|
192
203
|
|
|
193
204
|
unless res&.length&.positive?
|
|
@@ -197,9 +208,14 @@ module NA
|
|
|
197
208
|
|
|
198
209
|
selected = NA::Actions.new
|
|
199
210
|
res.each do |result|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
211
|
+
# Extract file:line from result (e.g., "./todo.taskpaper:21 : action text")
|
|
212
|
+
match = result.match(/^(.+?):(\d+) : /)
|
|
213
|
+
next unless match
|
|
214
|
+
|
|
215
|
+
file_path = match[1]
|
|
216
|
+
line_num = match[2].to_i
|
|
217
|
+
action = todo.actions.select { |a| a.file_path == file_path && a.file_line == line_num }.first
|
|
218
|
+
selected.push(action) if action
|
|
203
219
|
end
|
|
204
220
|
[todo.projects, selected]
|
|
205
221
|
end
|
|
@@ -312,6 +328,9 @@ module NA
|
|
|
312
328
|
remove_tag: [],
|
|
313
329
|
replace: nil,
|
|
314
330
|
tagged: nil)
|
|
331
|
+
# Expand target to absolute path to avoid path resolution issues
|
|
332
|
+
target = File.expand_path(target) unless Pathname.new(target).absolute?
|
|
333
|
+
|
|
315
334
|
projects = find_projects(target)
|
|
316
335
|
affected_actions = []
|
|
317
336
|
|
|
@@ -336,11 +355,14 @@ module NA
|
|
|
336
355
|
contents = target.read_file.split("\n")
|
|
337
356
|
|
|
338
357
|
if add.is_a?(Action)
|
|
358
|
+
# NOTE: Edit is handled in the update command before calling update_action
|
|
359
|
+
# So we don't need to handle it here - the action is already edited
|
|
360
|
+
|
|
339
361
|
add_tag ||= []
|
|
340
362
|
add.process(priority: priority, finish: finish, add_tag: add_tag, remove_tag: remove_tag)
|
|
341
363
|
|
|
342
364
|
# Remove the original action and its notes
|
|
343
|
-
action_line = add.
|
|
365
|
+
action_line = add.file_line
|
|
344
366
|
note_lines = add.note.is_a?(Array) ? add.note.count : 0
|
|
345
367
|
contents.slice!(action_line, note_lines + 1)
|
|
346
368
|
|
|
@@ -389,27 +411,36 @@ module NA
|
|
|
389
411
|
changes << "moved to #{target_proj.project}" if move && target_proj
|
|
390
412
|
affected_actions << { action: add, desc: changes.join(', ') }
|
|
391
413
|
else
|
|
414
|
+
# Check if search is actually target_line
|
|
415
|
+
target_line = search.is_a?(Hash) && search[:target_line] ? search[:target_line] : nil
|
|
392
416
|
_, actions = find_actions(target, search, tagged, done: done, all: all, project: project,
|
|
393
|
-
search_note: search_note)
|
|
417
|
+
search_note: search_note, target_line: target_line)
|
|
394
418
|
|
|
395
419
|
return if actions.nil?
|
|
396
420
|
|
|
397
|
-
|
|
398
|
-
|
|
421
|
+
# Handle edit (single or multi-action)
|
|
422
|
+
if edit
|
|
423
|
+
editor_content = Editor.format_multi_action_input(actions)
|
|
424
|
+
edited_content = Editor.fork_editor(editor_content)
|
|
425
|
+
edited_actions = Editor.parse_multi_action_output(edited_content)
|
|
426
|
+
|
|
427
|
+
# Map edited content back to actions
|
|
428
|
+
actions.each do |action|
|
|
429
|
+
# Use file_path:file_line as the key
|
|
430
|
+
key = "#{action.file_path}:#{action.file_line}"
|
|
431
|
+
action.action, action.note = edited_actions[key] if edited_actions[key]
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
actions.sort_by(&:file_line).reverse.each do |action|
|
|
436
|
+
contents.slice!(action.file_line, action.note.count + 1)
|
|
399
437
|
if delete
|
|
400
438
|
# Track deletion before skipping re-insert
|
|
401
439
|
affected_actions << { action: action, desc: 'deleted' }
|
|
402
440
|
next
|
|
403
441
|
end
|
|
404
442
|
|
|
405
|
-
projects = shift_index_after(projects, action.
|
|
406
|
-
|
|
407
|
-
if edit
|
|
408
|
-
editor_content = "#{action.action}\n#{action.note.join("\n")}"
|
|
409
|
-
new_action, new_note = Editor.format_input(Editor.fork_editor(editor_content))
|
|
410
|
-
action.action = new_action
|
|
411
|
-
action.note = new_note
|
|
412
|
-
end
|
|
443
|
+
projects = shift_index_after(projects, action.file_line, action.note.count + 1)
|
|
413
444
|
|
|
414
445
|
# If replace is defined, use search to search and replace text in action
|
|
415
446
|
action.action.sub!(Regexp.new(Regexp.escape(search), Regexp::IGNORECASE), replace) if replace
|
data/lib/na/string.rb
CHANGED
|
@@ -149,13 +149,17 @@ class ::String
|
|
|
149
149
|
# @param color [String] The highlight color template
|
|
150
150
|
# @param last_color [String] Color to restore after highlight
|
|
151
151
|
def highlight_search(regexes, color: NA.theme[:search_highlight], last_color: NA.theme[:action])
|
|
152
|
+
# Skip if string already contains ANSI codes - applying regex to colored text
|
|
153
|
+
# will break escape sequences (e.g., searching for "3" will match "3" in "38;2;236;204;135m")
|
|
154
|
+
return self if include?("\e")
|
|
155
|
+
|
|
156
|
+
# Original simple approach for strings without ANSI codes
|
|
152
157
|
string = dup
|
|
153
158
|
color = NA::Color.template(color.dup)
|
|
154
159
|
regexes.each do |rx|
|
|
155
160
|
next if rx.nil?
|
|
156
161
|
|
|
157
162
|
rx = Regexp.new(rx, Regexp::IGNORECASE) if rx.is_a?(String)
|
|
158
|
-
|
|
159
163
|
string.gsub!(rx) do
|
|
160
164
|
m = Regexp.last_match
|
|
161
165
|
last = m.pre_match.last_color
|
|
@@ -190,9 +194,9 @@ class ::String
|
|
|
190
194
|
output = []
|
|
191
195
|
line = []
|
|
192
196
|
length = 0
|
|
193
|
-
gsub
|
|
197
|
+
text = gsub(/(@\S+)\((.*?)\)/) { "#{Regexp.last_match(1)}(#{Regexp.last_match(2).gsub(/ /, '†')})" }
|
|
194
198
|
|
|
195
|
-
split
|
|
199
|
+
text.split.each do |word|
|
|
196
200
|
uncolored = NA::Color.uncolor(word)
|
|
197
201
|
if (length + uncolored.length + 1) <= width
|
|
198
202
|
line << word
|
data/lib/na/theme.rb
CHANGED
|
@@ -37,51 +37,60 @@ module NA
|
|
|
37
37
|
# @example
|
|
38
38
|
# NA::Theme.load_theme(template: { action: '{r}' })
|
|
39
39
|
def load_theme(template: {})
|
|
40
|
-
NA::Benchmark
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
tags: '{m}',
|
|
49
|
-
value_parens: '{m}',
|
|
50
|
-
values: '{c}',
|
|
51
|
-
search_highlight: '{y}',
|
|
52
|
-
note: '{dw}',
|
|
53
|
-
dirname: '{xdw}',
|
|
54
|
-
filename: '{xb}{#eccc87}',
|
|
55
|
-
prompt: '{m}',
|
|
56
|
-
success: '{bg}',
|
|
57
|
-
error: '{b}{#b61d2a}',
|
|
58
|
-
warning: '{by}',
|
|
59
|
-
debug: '{dw}',
|
|
60
|
-
templates: {
|
|
61
|
-
output: '%filename%parents| %action',
|
|
62
|
-
default: '%parent%action',
|
|
63
|
-
single_file: '%parent%action',
|
|
64
|
-
multi_file: '%filename%parent%action',
|
|
65
|
-
no_file: '%parent%action'
|
|
66
|
-
}
|
|
67
|
-
}
|
|
40
|
+
if defined?(NA::Benchmark) && NA::Benchmark
|
|
41
|
+
NA::Benchmark.measure('Theme.load_theme') do
|
|
42
|
+
load_theme_internal(template: template)
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
load_theme_internal(template: template)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
68
48
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
49
|
+
def load_theme_internal(template: {})
|
|
50
|
+
# Default colorization, can be overridden with full or partial template variable
|
|
51
|
+
default_template = {
|
|
52
|
+
parent: '{c}',
|
|
53
|
+
bracket: '{dc}',
|
|
54
|
+
parent_divider: '{xw}/',
|
|
55
|
+
action: '{bg}',
|
|
56
|
+
project: '{xbk}',
|
|
57
|
+
tags: '{m}',
|
|
58
|
+
value_parens: '{m}',
|
|
59
|
+
values: '{c}',
|
|
60
|
+
search_highlight: '{y}',
|
|
61
|
+
note: '{dw}',
|
|
62
|
+
dirname: '{xdw}',
|
|
63
|
+
filename: '{xb}{#eccc87}',
|
|
64
|
+
line: '{dw}',
|
|
65
|
+
prompt: '{m}',
|
|
66
|
+
success: '{bg}',
|
|
67
|
+
error: '{b}{#b61d2a}',
|
|
68
|
+
warning: '{by}',
|
|
69
|
+
debug: '{dw}',
|
|
70
|
+
templates: {
|
|
71
|
+
output: '%filename%line%parents| %action',
|
|
72
|
+
default: '%parents %line %action',
|
|
73
|
+
single_file: '%parents %line %action',
|
|
74
|
+
multi_file: '%filename%line%parents %action',
|
|
75
|
+
no_file: '%parents %line %action'
|
|
76
|
+
}
|
|
77
|
+
}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
# Load custom theme
|
|
80
|
+
theme_file = NA.database_path(file: 'theme.yaml')
|
|
81
|
+
theme = if File.exist?(theme_file)
|
|
82
|
+
YAML.load(File.read(theme_file)) || {}
|
|
83
|
+
else
|
|
84
|
+
{}
|
|
85
|
+
end
|
|
86
|
+
theme = default_template.deep_merge(theme)
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
File.open(theme_file, 'w') do |f|
|
|
89
|
+
f.puts template_help.comment
|
|
90
|
+
f.puts YAML.dump(theme)
|
|
84
91
|
end
|
|
92
|
+
|
|
93
|
+
theme.merge(template)
|
|
85
94
|
end
|
|
86
95
|
end
|
|
87
96
|
end
|
data/lib/na/version.rb
CHANGED
data/lib/na.rb
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'na/benchmark'
|
|
3
|
+
require 'na/benchmark' if ENV['NA_BENCHMARK']
|
|
4
|
+
# Define a dummy Benchmark if not available for tests
|
|
5
|
+
unless defined?(NA::Benchmark)
|
|
6
|
+
module NA
|
|
7
|
+
module Benchmark
|
|
8
|
+
def self.measure(_label)
|
|
9
|
+
yield
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
4
14
|
require 'na/version'
|
|
5
15
|
require 'na/pager'
|
|
6
16
|
require 'time'
|
data/src/_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 <!--VER-->1.2.
|
|
12
|
+
The current version of `na` is <!--VER-->1.2.85<!--END VER-->.
|
|
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
|
|
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.86
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brett Terpstra
|
|
@@ -245,6 +245,7 @@ extra_rdoc_files:
|
|
|
245
245
|
- README.md
|
|
246
246
|
- na.rdoc
|
|
247
247
|
files:
|
|
248
|
+
- ".cursor/commands/priority35m36m335m32m.md"
|
|
248
249
|
- ".rubocop.yml"
|
|
249
250
|
- ".rubocop_todo.yml"
|
|
250
251
|
- ".travis.yml"
|