na 1.2.84 → 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 +30 -35
- data/CHANGELOG.md +18 -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 +74 -14
- data/lib/na/array.rb +9 -0
- data/lib/na/benchmark.rb +8 -0
- data/lib/na/colors.rb +11 -4
- data/lib/na/editor.rb +89 -1
- data/lib/na/hash.rb +9 -0
- data/lib/na/help_monkey_patch.rb +10 -1
- data/lib/na/next_action.rb +93 -19
- data/lib/na/project.rb +8 -0
- data/lib/na/string.rb +51 -12
- data/lib/na/theme.rb +58 -41
- data/lib/na/todo.rb +8 -0
- data/lib/na/version.rb +5 -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
|
|
@@ -13,12 +13,7 @@ Lint/DuplicateBranch:
|
|
|
13
13
|
- 'lib/na/action.rb'
|
|
14
14
|
- 'lib/na/colors.rb'
|
|
15
15
|
|
|
16
|
-
# Offense count:
|
|
17
|
-
Lint/SelfAssignment:
|
|
18
|
-
Exclude:
|
|
19
|
-
- 'lib/na/string.rb'
|
|
20
|
-
|
|
21
|
-
# Offense count: 1
|
|
16
|
+
# Offense count: 2
|
|
22
17
|
# This cop supports safe autocorrection (--autocorrect).
|
|
23
18
|
# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
|
|
24
19
|
# NotImplementedExceptions: NotImplementedError
|
|
@@ -26,46 +21,53 @@ Lint/UnusedMethodArgument:
|
|
|
26
21
|
Exclude:
|
|
27
22
|
- 'lib/na/string.rb'
|
|
28
23
|
|
|
29
|
-
# Offense count:
|
|
24
|
+
# Offense count: 36
|
|
30
25
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
|
31
26
|
Metrics/AbcSize:
|
|
32
|
-
Max:
|
|
27
|
+
Max: 243
|
|
33
28
|
|
|
34
|
-
# Offense count:
|
|
29
|
+
# Offense count: 9
|
|
35
30
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
36
31
|
# AllowedMethods: refine
|
|
37
32
|
Metrics/BlockLength:
|
|
38
33
|
Max: 144
|
|
39
34
|
|
|
40
|
-
# Offense count:
|
|
35
|
+
# Offense count: 5
|
|
41
36
|
# Configuration parameters: CountComments, CountAsOne.
|
|
42
37
|
Metrics/ClassLength:
|
|
43
|
-
Max:
|
|
38
|
+
Max: 777
|
|
44
39
|
|
|
45
|
-
# Offense count:
|
|
40
|
+
# Offense count: 25
|
|
46
41
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
47
42
|
Metrics/CyclomaticComplexity:
|
|
48
|
-
Max:
|
|
43
|
+
Max: 70
|
|
49
44
|
|
|
50
|
-
# Offense count:
|
|
45
|
+
# Offense count: 40
|
|
51
46
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
52
47
|
Metrics/MethodLength:
|
|
53
48
|
Max: 146
|
|
54
49
|
|
|
55
|
-
# Offense count:
|
|
50
|
+
# Offense count: 3
|
|
56
51
|
# Configuration parameters: CountComments, CountAsOne.
|
|
57
52
|
Metrics/ModuleLength:
|
|
58
|
-
Max:
|
|
53
|
+
Max: 779
|
|
59
54
|
|
|
60
55
|
# Offense count: 4
|
|
61
56
|
# Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
|
|
62
57
|
Metrics/ParameterLists:
|
|
63
58
|
Max: 19
|
|
64
59
|
|
|
65
|
-
# Offense count:
|
|
60
|
+
# Offense count: 24
|
|
66
61
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
67
62
|
Metrics/PerceivedComplexity:
|
|
68
|
-
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'
|
|
69
71
|
|
|
70
72
|
# Offense count: 3
|
|
71
73
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
@@ -74,21 +76,6 @@ Security/YAMLLoad:
|
|
|
74
76
|
- 'lib/na/next_action.rb'
|
|
75
77
|
- 'lib/na/theme.rb'
|
|
76
78
|
|
|
77
|
-
# Offense count: 8
|
|
78
|
-
# Configuration parameters: AllowedConstants.
|
|
79
|
-
Style/Documentation:
|
|
80
|
-
Exclude:
|
|
81
|
-
- 'spec/**/*'
|
|
82
|
-
- 'test/**/*'
|
|
83
|
-
- 'lib/na/action.rb'
|
|
84
|
-
- 'lib/na/array.rb'
|
|
85
|
-
- 'lib/na/benchmark.rb'
|
|
86
|
-
- 'lib/na/editor.rb'
|
|
87
|
-
- 'lib/na/hash.rb'
|
|
88
|
-
- 'lib/na/project.rb'
|
|
89
|
-
- 'lib/na/theme.rb'
|
|
90
|
-
- 'lib/na/todo.rb'
|
|
91
|
-
|
|
92
79
|
# Offense count: 2
|
|
93
80
|
# This cop supports safe autocorrection (--autocorrect).
|
|
94
81
|
Style/IfUnlessModifier:
|
|
@@ -104,13 +91,21 @@ Style/ModuleFunction:
|
|
|
104
91
|
Exclude:
|
|
105
92
|
- 'lib/na/colors.rb'
|
|
106
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
|
+
|
|
107
102
|
# Offense count: 1
|
|
108
103
|
# This cop supports safe autocorrection (--autocorrect).
|
|
109
104
|
Style/YAMLFileRead:
|
|
110
105
|
Exclude:
|
|
111
106
|
- 'lib/na/theme.rb'
|
|
112
107
|
|
|
113
|
-
# Offense count:
|
|
108
|
+
# Offense count: 22
|
|
114
109
|
# This cop supports safe autocorrection (--autocorrect).
|
|
115
110
|
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
|
|
116
111
|
# URISchemes: http, https
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
### 1.2.86
|
|
2
|
+
|
|
3
|
+
2025-10-27 17:16
|
|
4
|
+
|
|
5
|
+
### 1.2.85
|
|
6
|
+
|
|
7
|
+
2025-10-26 08:43
|
|
8
|
+
|
|
9
|
+
#### IMPROVED
|
|
10
|
+
|
|
11
|
+
- YARD documentation
|
|
12
|
+
- Currently at 100% YARD documentation
|
|
13
|
+
|
|
14
|
+
#### FIXED
|
|
15
|
+
|
|
16
|
+
- Nil handler for trunc_middle
|
|
17
|
+
- Nil handler for hihglight_file
|
|
18
|
+
|
|
1
19
|
### 1.2.84
|
|
2
20
|
|
|
3
21
|
2025-10-25 15:50
|
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
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module NA
|
|
4
|
+
# Represents a single actionable item in a todo file, with tags, notes, and project context.
|
|
5
|
+
#
|
|
6
|
+
# @example Create a new action
|
|
7
|
+
# action = NA::Action.new('todo.txt', 'Inbox', [], '- Buy milk', 1)
|
|
4
8
|
class Action < Hash
|
|
5
9
|
attr_reader :file, :project, :tags, :line
|
|
6
10
|
attr_accessor :parent, :action, :note
|
|
7
11
|
|
|
12
|
+
# @example
|
|
13
|
+
# action = NA::Action.new('todo.txt', 'Inbox', [], '- Buy milk', 1)
|
|
8
14
|
def initialize(file, project, parent, action, idx, note = [])
|
|
9
15
|
super()
|
|
10
16
|
|
|
11
|
-
|
|
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
|
|
12
23
|
@project = project
|
|
13
24
|
@parent = parent
|
|
14
25
|
@action = action.gsub('{', '\\{')
|
|
@@ -17,6 +28,32 @@ module NA
|
|
|
17
28
|
@note = note
|
|
18
29
|
end
|
|
19
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
|
+
|
|
20
57
|
# Update the action string and note with priority, tags, and completion status
|
|
21
58
|
#
|
|
22
59
|
# @param priority [Integer] Priority value to set
|
|
@@ -25,6 +62,8 @@ module NA
|
|
|
25
62
|
# @param remove_tag [Array<String>] Tags to remove
|
|
26
63
|
# @param note [Array<String>] Notes to set
|
|
27
64
|
# @return [void]
|
|
65
|
+
# @example
|
|
66
|
+
# action.process(priority: 5, finish: true, add_tag: ['urgent'], remove_tag: ['waiting'], note: ['Call Bob'])
|
|
28
67
|
def process(priority: 0, finish: false, add_tag: [], remove_tag: [], note: [])
|
|
29
68
|
string = @action.dup
|
|
30
69
|
|
|
@@ -54,13 +93,15 @@ module NA
|
|
|
54
93
|
# String representation of the action
|
|
55
94
|
#
|
|
56
95
|
# @return [String]
|
|
96
|
+
# @example
|
|
97
|
+
# action.to_s #=> "{ project: 'Inbox', ... }"
|
|
57
98
|
def to_s
|
|
58
99
|
note = if @note.count.positive?
|
|
59
100
|
"\n#{@note.join("\n")}"
|
|
60
101
|
else
|
|
61
102
|
''
|
|
62
103
|
end
|
|
63
|
-
"(#{
|
|
104
|
+
"(#{file_path}:#{file_line}) #{@project}:#{@parent.join(">")} | #{@action}#{note}"
|
|
64
105
|
end
|
|
65
106
|
|
|
66
107
|
# Pretty string representation of the action with color formatting
|
|
@@ -72,7 +113,7 @@ module NA
|
|
|
72
113
|
else
|
|
73
114
|
''
|
|
74
115
|
end
|
|
75
|
-
"{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}"
|
|
76
117
|
end
|
|
77
118
|
|
|
78
119
|
# Inspect the action object
|
|
@@ -102,49 +143,67 @@ module NA
|
|
|
102
143
|
NA::Benchmark.measure('Action.pretty') do
|
|
103
144
|
# Use cached theme instead of loading every time
|
|
104
145
|
theme = NA.theme
|
|
105
|
-
|
|
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
|
|
106
154
|
|
|
107
155
|
# Pre-compute common template parts to avoid repeated processing
|
|
108
156
|
output_template = template[:templates][:output]
|
|
109
157
|
needs_filename = output_template.include?('%filename')
|
|
158
|
+
needs_line = output_template.include?('%line')
|
|
110
159
|
needs_parents = output_template.include?('%parents') || output_template.include?('%parent')
|
|
111
160
|
needs_project = output_template.include?('%project')
|
|
112
161
|
|
|
113
162
|
# Create the hierarchical parent string (optimized)
|
|
114
163
|
parents = if needs_parents && @parent.any?
|
|
115
164
|
parent_parts = @parent.map { |par| "#{template[:parent]}#{par}" }.join(template[:parent_divider])
|
|
116
|
-
|
|
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}")}"
|
|
117
167
|
else
|
|
118
168
|
''
|
|
119
169
|
end
|
|
120
170
|
|
|
121
|
-
# Create the project string (optimized)
|
|
171
|
+
# Create the project string (optimized) - Ensure color reset before project
|
|
122
172
|
project = if needs_project && !@project.empty?
|
|
123
|
-
NA::Color.template("{x}#{template[:project]}#{@project}{x}
|
|
173
|
+
NA::Color.template("{x}#{template[:project]}#{@project}{x}")
|
|
124
174
|
else
|
|
125
175
|
''
|
|
126
176
|
end
|
|
127
177
|
|
|
128
178
|
# Create the source filename string (optimized)
|
|
129
179
|
filename = if needs_filename
|
|
130
|
-
|
|
180
|
+
path_only = file_path # Extract just the path from PATH:LINE
|
|
181
|
+
path = path_only.sub(%r{^\./}, '').sub(/#{Dir.home}/, '~')
|
|
131
182
|
if File.dirname(path) == '.'
|
|
132
183
|
fname = NA.include_ext ? File.basename(path) : File.basename(path, ".#{extension}")
|
|
133
184
|
fname = "./#{fname}" if NA.show_cwd_indicator
|
|
134
|
-
NA::Color.template("#{template[:filename]}#{fname}
|
|
185
|
+
NA::Color.template("#{template[:filename]}#{fname}{x}")
|
|
135
186
|
else
|
|
136
187
|
colored = (NA.include_ext ? path : path.sub(/\.#{extension}$/, '')).highlight_filename
|
|
137
|
-
NA::Color.template("#{template[:filename]}#{colored}
|
|
188
|
+
NA::Color.template("#{template[:filename]}#{colored}{x}")
|
|
138
189
|
end
|
|
139
190
|
else
|
|
140
191
|
''
|
|
141
192
|
end
|
|
142
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
|
+
|
|
143
201
|
# colorize the action and highlight tags (optimized)
|
|
144
202
|
action_text = @action.dup
|
|
145
203
|
action_text.gsub!(/\{(.*?)\}/, '\\{\1\\}')
|
|
146
204
|
action_text = action_text.sub(/ @#{NA.na_tag}\b/, '')
|
|
147
|
-
action
|
|
205
|
+
# Reset colors before action to prevent bleeding from parents/project
|
|
206
|
+
action = NA::Color.template("{x}#{template[:action]}#{action_text}{x}")
|
|
148
207
|
action = action.highlight_tags(color: template[:tags],
|
|
149
208
|
parens: template[:value_parens],
|
|
150
209
|
value: template[:values],
|
|
@@ -159,8 +218,8 @@ module NA
|
|
|
159
218
|
width = @cached_width ||= TTY::Screen.columns
|
|
160
219
|
# Calculate indent more efficiently - avoid repeated template processing
|
|
161
220
|
base_template = output_template.gsub('%action', '').gsub('%note', '')
|
|
162
|
-
base_output = base_template.gsub('%filename', filename).gsub('%project', project).gsub(/%parents?/,
|
|
163
|
-
|
|
221
|
+
base_output = base_template.gsub('%filename', filename).gsub('%line', line_num).gsub('%project', project).gsub(/%parents?/,
|
|
222
|
+
parents)
|
|
164
223
|
indent = NA::Color.uncolor(NA::Color.template(base_output)).length
|
|
165
224
|
note = NA::Color.template(@note.wrap(width, indent, template[:note]))
|
|
166
225
|
else
|
|
@@ -175,7 +234,7 @@ module NA
|
|
|
175
234
|
if detect_width && !action.empty?
|
|
176
235
|
width = @cached_width ||= TTY::Screen.columns
|
|
177
236
|
base_template = output_template.gsub('%action', '').gsub('%note', '')
|
|
178
|
-
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)
|
|
179
238
|
indent = NA::Color.uncolor(NA::Color.template(base_output)).length
|
|
180
239
|
action = action.wrap(width, indent)
|
|
181
240
|
end
|
|
@@ -183,6 +242,7 @@ module NA
|
|
|
183
242
|
# Replace variables in template string and output colorized (optimized)
|
|
184
243
|
final_output = output_template.dup
|
|
185
244
|
final_output.gsub!('%filename', filename)
|
|
245
|
+
final_output.gsub!('%line', line_num)
|
|
186
246
|
final_output.gsub!('%project', project)
|
|
187
247
|
final_output.gsub!(/%parents?/, parents)
|
|
188
248
|
final_output.gsub!('%action', action.highlight_search(regexes))
|
data/lib/na/array.rb
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
##
|
|
4
|
+
# Extensions to Ruby's Array class for todo management and formatting.
|
|
5
|
+
#
|
|
6
|
+
# @example Remove bad elements from an array
|
|
7
|
+
# ['foo', '', nil, 0, false, 'bar'].remove_bad #=> ['foo', 'bar']
|
|
3
8
|
class ::Array
|
|
4
9
|
# Like Array#compact -- removes nil items, but also
|
|
5
10
|
# removes empty strings, zero or negative numbers and FalseClass items
|
|
6
11
|
#
|
|
7
12
|
# @return [Array] Array without "bad" elements
|
|
13
|
+
# @example
|
|
14
|
+
# ['foo', '', nil, 0, false, 'bar'].remove_bad #=> ['foo', 'bar']
|
|
8
15
|
def remove_bad
|
|
9
16
|
compact.map { |x| x.is_a?(String) ? x.strip : x }.select(&:good?)
|
|
10
17
|
end
|
|
@@ -15,6 +22,8 @@ class ::Array
|
|
|
15
22
|
# @param indent [Integer] Indentation spaces
|
|
16
23
|
# @param color [String] Color code to apply
|
|
17
24
|
# @return [Array, String] Wrapped and colorized lines
|
|
25
|
+
# @example
|
|
26
|
+
# ['foo', 'bar'].wrap(80, 2, '{g}') #=> "\n{g} • foo{x}\n{g} • bar{x}"
|
|
18
27
|
def wrap(width, indent, color)
|
|
19
28
|
return map { |l| "#{color} #{l.wrap(width, 2)}" } if width < 60
|
|
20
29
|
|