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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1bdcec170985481e8d49edf6e599f0d1fba33bd250fcae7d0e900e24d3f8d8e4
4
- data.tar.gz: 7e9d815b0dee8abed829216771c8d4177a49629f37b54f76ff2000fede7172d9
3
+ metadata.gz: 05b84c85234711e95bc6fae91b279577d0908ba7bc0b1fb254729d65c2823f99
4
+ data.tar.gz: acf70b87b92d32a81ae7802397e3f72fd72b28410c50556c680ad0f05c6ea21f
5
5
  SHA512:
6
- metadata.gz: c45055a0a92ad0cbcfcf670a9717b585a5fa0c2ddad5ffe84644a4a64fe3df564f7d0683fd9be47942c3d829f3148be1836039a57b82d108518a63b3d1de1e28
7
- data.tar.gz: 97d7a6616eae0ea5b967068a28154717aafd4c5f37aca62064bf86d2a82f719fe941aaacf98dd1158250e83d5d332f296eb99cb5d39d751852fc071e461d7ac7
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-25 14:39:51 UTC using RuboCop version 1.75.7.
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: 1
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: 35
24
+ # Offense count: 36
30
25
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
31
26
  Metrics/AbcSize:
32
- Max: 232
27
+ Max: 243
33
28
 
34
- # Offense count: 10
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: 4
35
+ # Offense count: 5
41
36
  # Configuration parameters: CountComments, CountAsOne.
42
37
  Metrics/ClassLength:
43
- Max: 762
38
+ Max: 777
44
39
 
45
- # Offense count: 22
40
+ # Offense count: 25
46
41
  # Configuration parameters: AllowedMethods, AllowedPatterns.
47
42
  Metrics/CyclomaticComplexity:
48
- Max: 66
43
+ Max: 70
49
44
 
50
- # Offense count: 38
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: 2
50
+ # Offense count: 3
56
51
  # Configuration parameters: CountComments, CountAsOne.
57
52
  Metrics/ModuleLength:
58
- Max: 764
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: 21
60
+ # Offense count: 24
66
61
  # Configuration parameters: AllowedMethods, AllowedPatterns.
67
62
  Metrics/PerceivedComplexity:
68
- Max: 76
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: 18
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
@@ -3,3 +3,5 @@
3
3
  source 'https://rubygems.org'
4
4
  gemspec
5
5
  gem 'rake'
6
+
7
+ gem "simplecov", "~> 0.22.0", group: :development
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.2.84)
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.84.
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.84
79
+ 1.2.86
80
80
 
81
81
  GLOBAL OPTIONS
82
82
  -a, --add - Add a next action (deprecated, for backwards compatibility)
@@ -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:project:parent > action
222
- display = "#{File.basename(action_obj.file)}:#{action_obj.project}:#{action_obj.parent.join('>')} | #{action_obj.action}"
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.file, line: action_obj.line, action: 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
- output, _ = Open3.capture2("echo \"#{input.gsub('"', '\"')}\" | #{selector}")
241
- selected = output.split("\n").map(&:strip).reject(&:empty?)
242
- selected_indices = candidate_actions.each_index.select { |i| selected.include?(candidate_actions[i]) }
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
- # Open editor for the selected action and update its content
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
- # Pass the selected action object as 'add', set search to nil
403
- # Pass the exact selected action object to update_action, bypassing all search/filter logic
404
- target = targets_for_selection[idx][:file]
405
- action_obj = targets_for_selection[idx][:action]
406
- # Direct action mode: update only the selected action in the known file
407
- NA.update_action(target, nil,
408
- add: action_obj,
409
- add_tag: add_tags,
410
- all: true,
411
- append: append,
412
- delete: options[:delete],
413
- done: options[:done],
414
- edit: options[:edit],
415
- finish: options[:finish],
416
- move: target_proj,
417
- note: note_val,
418
- overwrite: options[:overwrite],
419
- priority: priority,
420
- project: options[:project],
421
- remove_tag: remove_tags,
422
- replace: options[:replace],
423
- search_note: options[:search_notes],
424
- tagged: nil)
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, tokens,
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
- @file = file
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
- "(#{@file}:#{@line}) #{@project}:#{@parent.join('>')} | #{@action}#{note}"
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(@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}"
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
- template = theme.merge(template)
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
- NA::Color.template("{x}#{template[:bracket]}[#{template[:error]}#{parent_parts}{x}#{template[:bracket]}]{x} ")
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
- path = @file.sub(%r{^\./}, '').sub(/#{Dir.home}/, '~')
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} {x}")
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} {x}")
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 = NA::Color.template("#{template[:action]}#{action_text}{x}")
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
- parents)
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