na 1.2.80 → 1.2.81

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.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.2.80)
4
+ na (1.2.81)
5
5
  chronic (~> 0.10, >= 0.10.2)
6
6
  git (~> 3.0.0)
7
7
  gli (~> 2.21.0)
@@ -14,14 +14,14 @@ PATH
14
14
  GEM
15
15
  remote: https://rubygems.org/
16
16
  specs:
17
- activesupport (8.0.2)
17
+ activesupport (8.1.0)
18
18
  base64
19
- benchmark (>= 0.3)
20
19
  bigdecimal
21
20
  concurrent-ruby (~> 1.0, >= 1.3.1)
22
21
  connection_pool (>= 2.2.5)
23
22
  drb
24
23
  i18n (>= 1.6, < 2)
24
+ json
25
25
  logger (>= 1.4.2)
26
26
  minitest (>= 5.1)
27
27
  securerandom (>= 0.3)
@@ -30,11 +30,12 @@ GEM
30
30
  addressable (2.8.7)
31
31
  public_suffix (>= 2.0.2, < 7.0)
32
32
  base64 (0.3.0)
33
- benchmark (0.4.1)
34
- bigdecimal (3.2.3)
33
+ bigdecimal (3.3.1)
34
+ bump (0.6.1)
35
35
  chronic (0.10.2)
36
36
  concurrent-ruby (1.3.5)
37
- connection_pool (2.5.3)
37
+ connection_pool (2.5.4)
38
+ diff-lcs (1.6.2)
38
39
  drb (2.2.3)
39
40
  git (3.0.2)
40
41
  activesupport (>= 5.0)
@@ -44,15 +45,29 @@ GEM
44
45
  gli (2.21.5)
45
46
  i18n (1.14.7)
46
47
  concurrent-ruby (~> 1.0)
48
+ json (2.15.2)
47
49
  logger (1.7.0)
48
50
  mdless (1.0.37)
49
- minitest (5.25.5)
50
- ostruct (0.6.1)
51
+ minitest (5.26.0)
52
+ ostruct (0.6.3)
51
53
  process_executer (1.3.0)
52
54
  public_suffix (6.0.2)
53
55
  rake (13.3.0)
54
- rchardet (1.9.0)
56
+ rchardet (1.10.0)
55
57
  rdoc (4.3.0)
58
+ rspec (3.13.0)
59
+ rspec-core (~> 3.13.0)
60
+ rspec-expectations (~> 3.13.0)
61
+ rspec-mocks (~> 3.13.0)
62
+ rspec-core (3.13.3)
63
+ rspec-support (~> 3.13.0)
64
+ rspec-expectations (3.13.5)
65
+ diff-lcs (>= 1.2.0, < 2.0)
66
+ rspec-support (~> 3.13.0)
67
+ rspec-mocks (3.13.4)
68
+ diff-lcs (>= 1.2.0, < 2.0)
69
+ rspec-support (~> 3.13.0)
70
+ rspec-support (3.13.6)
56
71
  securerandom (0.4.1)
57
72
  tty-cursor (0.7.1)
58
73
  tty-reader (0.9.0)
@@ -65,7 +80,7 @@ GEM
65
80
  tty-which (0.5.0)
66
81
  tzinfo (2.0.6)
67
82
  concurrent-ruby (~> 1.0)
68
- uri (1.0.3)
83
+ uri (1.0.4)
69
84
  wisper (2.0.1)
70
85
 
71
86
  PLATFORMS
@@ -82,10 +97,12 @@ PLATFORMS
82
97
  x86_64-linux-musl
83
98
 
84
99
  DEPENDENCIES
100
+ bump (~> 0.6.0)
85
101
  minitest (~> 5.14)
86
102
  na!
87
103
  rake
88
104
  rdoc (~> 4.3)
105
+ rspec (~> 3.0)
89
106
  tty-spinner (~> 0.9, >= 0.9.0)
90
107
 
91
108
  BUNDLED WITH
data/README.md CHANGED
@@ -9,9 +9,9 @@
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.80.
12
+ The current version of `na` is 1.2.81.
13
13
 
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.
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
 
16
16
  Used with Taskpaper files, it can add new action items quickly from the command line, automatically tagging them as next actions. It can also mark actions as completed, delete them, archive them, and move them between projects.
17
17
 
@@ -48,7 +48,7 @@ You can list next actions in files in the current directory by typing `na`. By d
48
48
 
49
49
  #### Adding todos
50
50
 
51
- You can also quickly add todo items from the command line with the `add` subcommand. The script will look for a file in the current directory with a `.taskpaper` extension (configurable).
51
+ You can also quickly add todo items from the command line with the `add` subcommand. The script will look for a file in the current directory with a `.taskpaper` extension (configurable).
52
52
 
53
53
  If found, it will try to locate an `Inbox:` project, or create one if it doesn't exist. Any arguments after `add` will be combined to create a new task in TaskPaper format. They will automatically be assigned as next actions (tagged `@na`) and will show up when `na` lists the tasks for the project.
54
54
 
@@ -76,7 +76,7 @@ SYNOPSIS
76
76
  na [global options] command [command options] [arguments...]
77
77
 
78
78
  VERSION
79
- 1.2.80
79
+ 1.2.81
80
80
 
81
81
  GLOBAL OPTIONS
82
82
  -a, --add - Add a next action (deprecated, for backwards compatibility)
@@ -116,6 +116,7 @@ COMMANDS
116
116
  prompt - Show or install prompt hooks for the current shell
117
117
  restore, unfinish - Find and remove @done tag from an action
118
118
  saved - Execute a saved search
119
+ scan - Scan a directory tree for todo files and cache them
119
120
  tag - Add tags to matching action(s)
120
121
  tagged - Find actions matching a tag
121
122
  todos - Show list of known todo files
@@ -133,7 +134,7 @@ If you run the `add` command with no arguments, you'll be asked for input on the
133
134
 
134
135
  ###### Adding notes
135
136
 
136
- Use the `--note` switch to add a note. If STDIN (piped) input is present when this switch is used, it will be included in the note. A prompt will be displayed for adding additional notes, which will be appended to any STDIN note passed. Press CTRL-d to end editing and save the note.
137
+ Use the `--note` switch to add a note. If STDIN (piped) input is present when this switch is used, it will be included in the note. A prompt will be displayed for adding additional notes, which will be appended to any STDIN note passed. Press CTRL-d to end editing and save the note.
137
138
 
138
139
  Notes are not displayed by the `next/tagged/find` commands unless `--notes` is specified.
139
140
 
@@ -335,6 +336,7 @@ COMMAND OPTIONS
335
336
  --[no-]done - Include @done actions
336
337
  --exact - Search query is exact text match (not tokens)
337
338
  --file=TODO_FILE - Display matches from specific todo file ([relative] path) (default: none)
339
+ --hidden - Include hidden directories while traversing
338
340
  --in, --todo=TODO - Display matches from a known todo file anywhere in history (short name) (may be used more than once, default: none)
339
341
  --nest - Output actions nested by file
340
342
  --no_file - No filename in output
@@ -426,6 +428,44 @@ EXAMPLES
426
428
  na saved
427
429
  ```
428
430
 
431
+ ##### scan
432
+
433
+ Scan a directory tree for todo files and cache them in tdlist.txt. Avoids duplicates and can optionally prune non-existent entries.
434
+
435
+ Scan reports how many files were added and, if --prune is used, how many were pruned. With --dry-run, it lists the full file paths that would be added and/or pruned.
436
+
437
+ ```
438
+ NAME
439
+ scan - Scan a directory tree for todo files and cache them
440
+
441
+ SYNOPSIS
442
+
443
+ na [global options] scan [command options] [PATH]
444
+
445
+ DESCRIPTION
446
+ Searches PATH (default: current directory) for files matching the current NA.extension and adds their absolute paths to the tdlist.txt cache. Avoids duplicates. Optionally prunes non-existent entries from the cache.
447
+
448
+ COMMAND OPTIONS
449
+ -d, --depth=DEPTH - Recurse to depth (1..N or i/inf for infinite) (default: 5)
450
+ --hidden - Include hidden directories and files while scanning
451
+ -n, --dry-run - Show what would be added/pruned, but do not write tdlist.txt
452
+ -p, --prune - Prune removed files from cache after scan
453
+
454
+ EXAMPLES
455
+
456
+ # Scan current directory up to default depth (5)
457
+ na scan
458
+
459
+ # Scan a specific path up to depth 3
460
+ na scan -d 3 ~/Projects
461
+
462
+ # Scan current directory recursively with no depth limit
463
+ na scan -d inf
464
+
465
+ # Prune non-existent entries from the cache (in addition to scanning)
466
+ na scan --prune
467
+ ```
468
+
429
469
  ##### tagged
430
470
 
431
471
  Example: `na tagged feature +maybe`.
@@ -453,6 +493,7 @@ COMMAND OPTIONS
453
493
  --[no-]done - Include @done actions
454
494
  --exact - Search query is exact text match (not tokens)
455
495
  --file=TODO_FILE - Display matches from specific todo file ([relative] path) (default: none)
496
+ --hidden - Include hidden directories while traversing
456
497
  --in, --todo=TODO - Display matches from a known todo file anywhere in history (short name) (may be used more than once, default: none)
457
498
  --nest - Output actions nested by file
458
499
  --no_file - No filename in output
data/Rakefile CHANGED
@@ -207,3 +207,9 @@ end
207
207
 
208
208
  desc "alias for build"
209
209
  task package: :build
210
+
211
+ desc 'Run tests with coverage'
212
+ task :coverage do
213
+ ENV['COVERAGE'] = 'true'
214
+ Rake::Task[:test].invoke
215
+ end
data/bin/commands/next.rb CHANGED
@@ -19,6 +19,9 @@ class App
19
19
  c.arg_name "DEPTH"
20
20
  c.flag %i[d depth], type: :integer, must_match: /^[1-9]$/
21
21
 
22
+ c.desc "Include hidden directories while traversing"
23
+ c.switch %i[hidden], negatable: false, default_value: false
24
+
22
25
  c.desc "Show next actions from all known todo files (in any directory)"
23
26
  c.switch %i[all], negatable: false, default_value: false
24
27
 
@@ -217,6 +220,7 @@ class App
217
220
  file_path = options[:file] ? File.expand_path(options[:file]) : nil
218
221
 
219
222
  todo = NA::Todo.new({ depth: depth,
223
+ hidden: options[:hidden],
220
224
  done: options[:done],
221
225
  file_path: file_path,
222
226
  project: options[:project],
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ class App
4
+ extend GLI::App
5
+ desc 'Scan a directory tree for todo files and cache them'
6
+ long_desc 'Searches PATH (default: current directory) for files matching the current NA.extension
7
+ and adds their absolute paths to the tdlist.txt cache. Avoids duplicates. Optionally prunes
8
+ non-existent entries from the cache.'
9
+ arg_name 'PATH', optional: true
10
+ command %i[scan] do |c|
11
+ c.example 'na scan', desc: 'Scan current directory up to default depth (5)'
12
+ c.example 'na scan -d 3 ~/Projects', desc: 'Scan a specific path up to depth 3'
13
+ c.example 'na scan -d inf', desc: 'Scan current directory recursively with no depth limit'
14
+ c.example 'na scan --prune', desc: 'Prune non-existent entries from the cache (in addition to scanning)'
15
+
16
+ c.desc 'Recurse to depth (1..N or i/inf for infinite)'
17
+ c.arg_name 'DEPTH'
18
+ c.default_value '5'
19
+ c.flag %i[d depth], must_match: /^(\d+|i\w*)$/i
20
+
21
+ c.desc 'Prune removed files from cache after scan'
22
+ c.switch %i[p prune], negatable: false, default_value: false
23
+
24
+ c.desc 'Include hidden directories and files while scanning'
25
+ c.switch %i[hidden], negatable: false, default_value: false
26
+
27
+ c.desc 'Show what would be added/pruned, but do not write tdlist.txt'
28
+ c.switch %i[n dry-run], negatable: false, default_value: false
29
+
30
+ c.action do |_global_options, options, args|
31
+ base = args.first || Dir.pwd
32
+ ext = NA.extension
33
+
34
+ # Parse depth: numeric or starts-with-i for infinite
35
+ depth_arg = (options[:depth] || '5').to_s
36
+ infinite = depth_arg =~ /^i/i ? true : false
37
+ depth = infinite ? nil : depth_arg.to_i
38
+ depth = 5 if depth.nil? && !infinite
39
+
40
+ # Prepare existing cache
41
+ db = NA.database_path
42
+ existing = if File.exist?(db)
43
+ File.read(db).split(/\n/).map(&:strip)
44
+ else
45
+ []
46
+ end
47
+
48
+ found = []
49
+ Dir.chdir(base) do
50
+ patterns = if infinite
51
+ ["*.#{ext}", "**/*.#{ext}"]
52
+ else
53
+ (1..[depth, 1].max).map { |d| (d > 1 ? ('*/' * (d - 1)) : '') + "*.#{ext}" }
54
+ end
55
+ pattern = patterns.length == 1 ? patterns.first : "{#{patterns.join(',')}}"
56
+ files = Dir.glob(pattern, File::FNM_DOTMATCH)
57
+ # Exclude hidden dirs/files (any segment starting with '.') unless --hidden
58
+ files.reject! { |f| f.split('/').any? { |seg| seg.start_with?('.') && seg !~ /^\.\.?$/ } } unless options[:hidden]
59
+ found = files.map { |f| File.expand_path(f) }
60
+ end
61
+
62
+ merged = (existing + found).map(&:strip).uniq.sort
63
+ merged.select! { |f| File.exist?(f) } if options[:prune]
64
+
65
+ added_files = (merged - existing)
66
+ pruned_files = options[:prune] ? (existing - merged) : []
67
+ added = added_files.count
68
+ pruned = pruned_files.count
69
+
70
+ if options[:dry_run]
71
+ msg = "#{NA.theme[:success]}Dry run: would add #{added} file#{added == 1 ? '' : 's'}"
72
+ msg << ", prune #{pruned} file#{pruned == 1 ? '' : 's'}" if options[:prune]
73
+ NA.notify(msg)
74
+ NA.notify("{bw}Would add:{x}\n#{added_files.join("\n")}") if added_files.any?
75
+ NA.notify("{bw}Would prune:{x}\n#{pruned_files.join("\n")}") if options[:prune] && pruned_files.any?
76
+ else
77
+ File.open(db, 'w') { |f| f.puts merged.join("\n") }
78
+ msg = "#{NA.theme[:success]}Scan complete: #{NA.theme[:filename]}#{added}{x}#{NA.theme[:success]} added"
79
+ msg << ", #{NA.theme[:filename]}#{pruned}{x}#{NA.theme[:success]} pruned" if options[:prune]
80
+ NA.notify(msg)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -192,7 +192,7 @@ class App
192
192
  # Require at least one actionable option to be provided
193
193
  actionable = [
194
194
  options[:note],
195
- (options[:priority].to_i if options[:priority]).to_i > 0,
195
+ (options[:priority].to_i if options[:priority]).to_i.positive?,
196
196
  !options[:move].to_s.empty?,
197
197
  !(options[:tag].nil? || options[:tag].empty?),
198
198
  !(options[:remove].nil? || options[:remove].empty?),
data/bin/na CHANGED
@@ -10,11 +10,11 @@ require 'fcntl'
10
10
  require 'tempfile'
11
11
 
12
12
  NA::Benchmark.init
13
- NA::Benchmark.measure('Gem loading') { nil } # Measures time up to this point
13
+ NA::Benchmark.measure('Gem loading') { nil } # Measures time up to this point
14
14
 
15
15
  # Search for XDG compliant config first. Default to ~/.na.rc for compatibility
16
16
  def self.find_config_file
17
- home = ENV['HOME']
17
+ home = Dir.home
18
18
  xdg_config_home = ENV['XDG_CONFIG_HOME'] || File.join(home, '.config')
19
19
 
20
20
  rc_paths = [
@@ -30,7 +30,6 @@ def self.find_config_file
30
30
  existing_path || File.join(xdg_config_home, 'na', 'na.rc')
31
31
  end
32
32
 
33
-
34
33
  # Main application
35
34
  class App
36
35
  extend GLI::App
@@ -84,7 +83,7 @@ class App
84
83
  switch %i[repo-top], default_value: false
85
84
 
86
85
  desc 'Provide a template for new/blank todo files, use initconfig to make permanent'
87
- flag %[template]
86
+ flag %(template)
88
87
 
89
88
  desc 'Use current working directory as [p]roject, [t]ag, or [n]one'
90
89
  arg_name 'TYPE'
@@ -121,7 +120,7 @@ class App
121
120
  NA.include_ext = global[:include_ext]
122
121
  NA.na_tag = global[:na_tag]
123
122
  NA.global_file = global[:file]
124
- NA.cwd = File.basename(ENV['PWD'])
123
+ NA.cwd = File.basename(ENV.fetch('PWD', nil))
125
124
  NA.cwd_is = if global[:cwd_as] =~ /^n/
126
125
  :none
127
126
  else
@@ -146,7 +145,8 @@ class App
146
145
  NA.global_file = taskpaper_file
147
146
  # Add this block to create the file if it doesn't exist
148
147
  unless File.exist?(taskpaper_file)
149
- res = NA.yn(NA::Color.template("#{NA.theme[:warning]}Repository file not found, create #{taskpaper_file}"), default: true)
148
+ res = NA.yn(NA::Color.template("#{NA.theme[:warning]}Repository file not found, create #{taskpaper_file}"),
149
+ default: true)
150
150
  if res
151
151
  NA.create_todo(taskpaper_file, repo_name, template: global[:template])
152
152
  else
@@ -198,7 +198,7 @@ class App
198
198
  end
199
199
 
200
200
  NA.stdin = $stdin.read.strip if $stdin.stat.size.positive? || $stdin.fcntl(Fcntl::F_GETFL, 0).zero?
201
- NA.stdin = nil unless NA.stdin && NA.stdin.length.positive?
201
+ NA.stdin = nil unless NA.stdin&.length&.positive?
202
202
 
203
203
  NA.globals = []
204
204
  NA.command_line = []
data/lib/na/action.rb CHANGED
@@ -12,12 +12,20 @@ module NA
12
12
  @file = file
13
13
  @project = project
14
14
  @parent = parent
15
- @action = action.gsub(/\{/, '\\{')
15
+ @action = action.gsub('{', '\\{')
16
16
  @tags = scan_tags
17
17
  @line = idx
18
18
  @note = note
19
19
  end
20
20
 
21
+ # Update the action string and note with priority, tags, and completion status
22
+ #
23
+ # @param priority [Integer] Priority value to set
24
+ # @param finish [Boolean] Mark as finished
25
+ # @param add_tag [Array<String>] Tags to add
26
+ # @param remove_tag [Array<String>] Tags to remove
27
+ # @param note [Array<String>] Notes to set
28
+ # @return [void]
21
29
  def process(priority: 0, finish: false, add_tag: [], remove_tag: [], note: [])
22
30
  string = @action.dup
23
31
 
@@ -44,6 +52,9 @@ module NA
44
52
  @note = note unless note.empty?
45
53
  end
46
54
 
55
+ # String representation of the action
56
+ #
57
+ # @return [String]
47
58
  def to_s
48
59
  note = if @note.count.positive?
49
60
  "\n#{@note.join("\n")}"
@@ -53,36 +64,41 @@ module NA
53
64
  "(#{@file}:#{@line}) #{@project}:#{@parent.join('>')} | #{@action}#{note}"
54
65
  end
55
66
 
67
+ # Pretty string representation of the action with color formatting
68
+ #
69
+ # @return [String]
56
70
  def to_s_pretty
57
71
  note = if @note.count.positive?
58
72
  "\n#{@note.join("\n")}"
59
73
  else
60
74
  ''
61
75
  end
62
- "#{NA.theme[:filename]}#{File.basename(@file)}:#{@line}#{NA.theme[:bracket]}[#{NA.theme[:project]}#{@project}:#{@parent.join(">")}#{NA.theme[:bracket]}]{x} | #{NA.theme[:action]}#{@action}#{NA.theme[:note]}#{note}"
76
+ "{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}"
63
77
  end
64
78
 
79
+ # Inspect the action object
80
+ #
81
+ # @return [String]
65
82
  def inspect
66
83
  <<~EOINSPECT
67
- @file: #{@file}
68
- @project: #{@project}
69
- @parent: #{@parent.join('>')}
70
- @action: #{@action}
71
- @tags: #{@tags}
72
- @note: #{@note}
84
+ @file: #{@file}
85
+ @project: #{@project}
86
+ @parent: #{@parent.join('>')}
87
+ @action: #{@action}
88
+ @tags: #{@tags}
89
+ @note: #{@note}
73
90
  EOINSPECT
74
91
  end
75
92
 
76
- ##
77
- ## Pretty print an action
78
- ##
79
- ## @param extension [String] The file extension
80
- ## @param template [Hash] The template to use for
81
- ## colorization
82
- ## @param regexes [Array] The regexes to
83
- ## highlight (searches)
84
- ## @param notes [Boolean] Include notes
85
- ##
93
+ #
94
+ # Pretty print an action with color and template formatting
95
+ #
96
+ # @param extension [String] File extension
97
+ # @param template [Hash] Color template
98
+ # @param regexes [Array] Regexes to highlight
99
+ # @param notes [Boolean] Include notes
100
+ # @param detect_width [Boolean] Detect terminal width
101
+ # @return [String]
86
102
  def pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false, detect_width: true)
87
103
  NA::Benchmark.measure('Action.pretty') do
88
104
  # Use cached theme instead of loading every time
@@ -98,24 +114,29 @@ module NA
98
114
  # Create the hierarchical parent string (optimized)
99
115
  parents = if needs_parents && @parent.any?
100
116
  parent_parts = @parent.map { |par| "#{template[:parent]}#{par}" }.join(template[:parent_divider])
101
- NA::Color.template("{x}#{template[:bracket]}[#{template[:error]}#{parent_parts}#{template[:bracket]}]{x} ")
117
+ NA::Color.template("{x}#{template[:bracket]}[#{template[:error]}#{parent_parts}{x}#{template[:bracket]}]{x} ")
102
118
  else
103
119
  ''
104
120
  end
105
121
 
106
122
  # Create the project string (optimized)
107
123
  project = if needs_project && !@project.empty?
108
- NA::Color.template("#{template[:project]}#{@project}{x} ")
124
+ NA::Color.template("{x}#{template[:project]}#{@project}{x} ")
109
125
  else
110
126
  ''
111
127
  end
112
128
 
113
129
  # Create the source filename string (optimized)
114
130
  filename = if needs_filename
115
- file = @file.sub(%r{^\./}, '').sub(/#{ENV['HOME']}/, '~')
116
- file = file.sub(/\.#{extension}$/, '') unless NA.include_ext
117
- file = file.highlight_filename
118
- NA::Color.template("#{template[:filename]}#{file} {x}")
131
+ path = @file.sub(%r{^\./}, '').sub(/#{Dir.home}/, '~')
132
+ if File.dirname(path) == '.'
133
+ fname = NA.include_ext ? File.basename(path) : File.basename(path, ".#{extension}")
134
+ fname = "./#{fname}" if NA.show_cwd_indicator
135
+ NA::Color.template("#{template[:filename]}#{fname} {x}")
136
+ else
137
+ colored = (NA.include_ext ? path : path.sub(/\.#{extension}$/, '')).highlight_filename
138
+ NA::Color.template("#{template[:filename]}#{colored} {x}")
139
+ end
119
140
  else
120
141
  ''
121
142
  end
@@ -138,44 +159,58 @@ module NA
138
159
  # Cache width calculation
139
160
  width = @cached_width ||= TTY::Screen.columns
140
161
  # Calculate indent more efficiently - avoid repeated template processing
141
- base_template = output_template.gsub(/%action/, '').gsub(/%note/, '')
142
- base_output = base_template.gsub(/%filename/, filename).gsub(/%project/, project).gsub(/%parents?/, parents)
162
+ base_template = output_template.gsub('%action', '').gsub('%note', '')
163
+ base_output = base_template.gsub('%filename', filename).gsub('%project', project).gsub(/%parents?/,
164
+ parents)
143
165
  indent = NA::Color.uncolor(NA::Color.template(base_output)).length
144
166
  note = NA::Color.template(@note.wrap(width, indent, template[:note]))
145
167
  else
146
- note = NA::Color.template("\n#{@note.map { |l| " #{template[:note]}• #{l}{x}" }.join("\n")}")
168
+ note = NA::Color.template("\n#{@note.map { |l| " {x}#{template[:note]}• #{l}{x}" }.join("\n")}")
147
169
  end
148
170
  else
149
- action += "#{template[:note]}*"
171
+ action += "{x}#{template[:note]}*"
150
172
  end
151
173
  end
152
174
 
153
175
  # Wrap action if needed (optimized)
154
176
  if detect_width && !action.empty?
155
177
  width = @cached_width ||= TTY::Screen.columns
156
- base_template = output_template.gsub(/%action/, '').gsub(/%note/, '')
157
- base_output = base_template.gsub(/%filename/, filename).gsub(/%project/, project).gsub(/%parents?/, parents)
178
+ base_template = output_template.gsub('%action', '').gsub('%note', '')
179
+ base_output = base_template.gsub('%filename', filename).gsub('%project', project).gsub(/%parents?/, parents)
158
180
  indent = NA::Color.uncolor(NA::Color.template(base_output)).length
159
181
  action = action.wrap(width, indent)
160
182
  end
161
183
 
162
184
  # Replace variables in template string and output colorized (optimized)
163
185
  final_output = output_template.dup
164
- final_output.gsub!(/%filename/, filename)
165
- final_output.gsub!(/%project/, project)
186
+ final_output.gsub!('%filename', filename)
187
+ final_output.gsub!('%project', project)
166
188
  final_output.gsub!(/%parents?/, parents)
167
- final_output.gsub!(/%action/, action.highlight_search(regexes))
168
- final_output.gsub!(/%note/, note)
169
- final_output.gsub!(/\\\{/, '{')
189
+ final_output.gsub!('%action', action.highlight_search(regexes))
190
+ final_output.gsub!('%note', note)
191
+ final_output.gsub!('\\{', '{')
170
192
 
171
193
  NA::Color.template(final_output)
172
194
  end
173
195
  end
174
196
 
197
+ # Check if action tags match any, all, and none criteria
198
+ #
199
+ # @param any [Array] Tags to match any
200
+ # @param all [Array] Tags to match all
201
+ # @param none [Array] Tags to match none
202
+ # @return [Boolean]
175
203
  def tags_match?(any: [], all: [], none: [])
176
204
  tag_matches_any(any) && tag_matches_all(all) && tag_matches_none(none)
177
205
  end
178
206
 
207
+ # Check if action or note matches any, all, and none search criteria
208
+ #
209
+ # @param any [Array] Regexes to match any
210
+ # @param all [Array] Regexes to match all
211
+ # @param none [Array] Regexes to match none
212
+ # @param include_note [Boolean] Include note in search
213
+ # @return [Boolean]
179
214
  def search_match?(any: [], all: [], none: [], include_note: true)
180
215
  search_matches_any(any, include_note: include_note) &&
181
216
  search_matches_all(all, include_note: include_note) &&
@@ -184,6 +219,11 @@ module NA
184
219
 
185
220
  private
186
221
 
222
+ # Check if action and note do not match any regexes
223
+ #
224
+ # @param regexes [Array] Regexes to check
225
+ # @param include_note [Boolean] Include note in search
226
+ # @return [Boolean]
187
227
  def search_matches_none(regexes, include_note: true)
188
228
  regexes.each do |rx|
189
229
  regex = rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE)
@@ -193,6 +233,11 @@ module NA
193
233
  true
194
234
  end
195
235
 
236
+ # Check if action or note matches any regexes
237
+ #
238
+ # @param regexes [Array] Regexes to check
239
+ # @param include_note [Boolean] Include note in search
240
+ # @return [Boolean]
196
241
  def search_matches_any(regexes, include_note: true)
197
242
  return true if regexes.empty?
198
243
 
@@ -204,6 +249,11 @@ module NA
204
249
  false
205
250
  end
206
251
 
252
+ # Check if action or note matches all regexes
253
+ #
254
+ # @param regexes [Array] Regexes to check
255
+ # @param include_note [Boolean] Include note in search
256
+ # @return [Boolean]
207
257
  def search_matches_all(regexes, include_note: true)
208
258
  regexes.each do |rx|
209
259
  regex = rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE)
@@ -213,6 +263,10 @@ module NA
213
263
  true
214
264
  end
215
265
 
266
+ # Check if none of the tags match
267
+ #
268
+ # @param tags [Array] Tags to check
269
+ # @return [Boolean]
216
270
  def tag_matches_none(tags)
217
271
  tags.each do |tag|
218
272
  return false if compare_tag(tag)
@@ -220,6 +274,10 @@ module NA
220
274
  true
221
275
  end
222
276
 
277
+ # Check if any of the tags match
278
+ #
279
+ # @param tags [Array] Tags to check
280
+ # @return [Boolean]
223
281
  def tag_matches_any(tags)
224
282
  return true if tags.empty?
225
283
 
@@ -229,6 +287,10 @@ module NA
229
287
  false
230
288
  end
231
289
 
290
+ # Check if all of the tags match
291
+ #
292
+ # @param tags [Array] Tags to check
293
+ # @return [Boolean]
232
294
  def tag_matches_all(tags)
233
295
  tags.each do |tag|
234
296
  return false unless compare_tag(tag)
@@ -236,6 +298,10 @@ module NA
236
298
  true
237
299
  end
238
300
 
301
+ # Compare a tag against the action's tags with optional value comparison
302
+ #
303
+ # @param tag [Hash] Tag criteria
304
+ # @return [Boolean]
239
305
  def compare_tag(tag)
240
306
  tag_regex = tag[:tag].is_a?(Regexp) ? tag[:tag] : Regexp.new(tag[:tag], Regexp::IGNORECASE)
241
307
  keys = @tags.keys.delete_if { |k| k !~ tag_regex }