na 1.2.79 → 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/CHANGELOG.md CHANGED
@@ -1,3 +1,58 @@
1
+ ### 1.2.81
2
+
3
+ 2025-10-25 07:32
4
+
5
+ #### NEW
6
+
7
+ - `na scan` command to scan for untracked todo files (thanks @rhsev)
8
+
9
+ #### IMPROVED
10
+
11
+ - Rubocop cleanup
12
+ - YARD documentation
13
+ - Test coverage
14
+
15
+ #### FIXED
16
+
17
+ - Reset color/emphasis before closing bracket in parent display
18
+ - Subdirectory traversal with `na next -d X` fixed
19
+
20
+ ### 1.2.80
21
+
22
+ 2025-10-23 05:26
23
+
24
+ #### CHANGED
25
+
26
+ - Default behavior: git integration now opt-in with --repo-top flag
27
+ - Pager execution: use spawn for better performance than fork+exec
28
+ - Color template processing: cache compiled templates and colors hash
29
+ - Action processing: batch regex highlighting instead of per-action processing
30
+
31
+ #### NEW
32
+
33
+ - Add comprehensive benchmarking system with NA_BENCHMARK=1
34
+ - Add template caching in Color.template to avoid repeated regex processing
35
+ - Add colors hash caching to eliminate repeated hash creation
36
+ - Add smart pagination that skips pager for small outputs (<2000 chars, <50 lines)
37
+ - Add --repo-top flag to make git integration opt-in instead of default
38
+
39
+ #### IMPROVED
40
+
41
+ - Optimize performance
42
+ - Theme loading now uses cached NA.theme instead of loading on every Action.pretty call
43
+ - Action.pretty method with conditional processing and optimized string operations
44
+ - Actions.output with batch regex processing - compile all output first, then apply regexes once
45
+ - Pager performance using spawn instead of fork+exec and removing IO.select delays
46
+ - Lazy loading of heavy gems (git, chronic, mdless) only when needed
47
+ - String operations in Action.pretty with pre-computed template parts
48
+
49
+ #### FIXED
50
+
51
+ - Performance variability caused by git system calls running by default
52
+ - Pager overhead causing 300-479ms delays on small outputs
53
+ - Repeated regex compilation in color template processing
54
+ - Theme loading bottleneck in Action.pretty method
55
+
1
56
  ### 1.2.79
2
57
 
3
58
  2025-09-29 06:51
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
  gemspec
3
5
  gem 'rake'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.2.79)
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.1)
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.79.
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.79
79
+ 1.2.81
80
80
 
81
81
  GLOBAL OPTIONS
82
82
  -a, --add - Add a next action (deprecated, for backwards compatibility)
@@ -93,7 +93,7 @@ GLOBAL OPTIONS
93
93
  -p, --priority=PRIORITY - Set a priority 0-5 (deprecated, for backwards compatibility) (default: none)
94
94
  --[no-]pager - Enable pagination (default: enabled)
95
95
  -r, --[no-]recurse - Recurse 3 directories deep (deprecated, for backwards compatability)
96
- --[no-]repo - Use a taskpaper file named after the git repository (default: enabled)
96
+ --[no-]repo-top - Use a taskpaper file named after the git repository (enables git integration)
97
97
  -t, --na_tag=TAG - Tag to consider a next action (default: na)
98
98
  --template=arg - Provide a template for new/blank todo files, use initconfig to make permanent (default: none)
99
99
  --version - Display the program version
@@ -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
@@ -5,12 +5,16 @@ $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
5
5
  require 'gli'
6
6
  require 'na/help_monkey_patch'
7
7
  require 'na'
8
+ require 'na/benchmark'
8
9
  require 'fcntl'
9
10
  require 'tempfile'
10
11
 
12
+ NA::Benchmark.init
13
+ NA::Benchmark.measure('Gem loading') { nil } # Measures time up to this point
14
+
11
15
  # Search for XDG compliant config first. Default to ~/.na.rc for compatibility
12
16
  def self.find_config_file
13
- home = ENV['HOME']
17
+ home = Dir.home
14
18
  xdg_config_home = ENV['XDG_CONFIG_HOME'] || File.join(home, '.config')
15
19
 
16
20
  rc_paths = [
@@ -26,7 +30,6 @@ def self.find_config_file
26
30
  existing_path || File.join(xdg_config_home, 'na', 'na.rc')
27
31
  end
28
32
 
29
-
30
33
  # Main application
31
34
  class App
32
35
  extend GLI::App
@@ -75,12 +78,12 @@ class App
75
78
  arg_name 'PATH'
76
79
  flag %i[f file]
77
80
 
78
- desc 'Use a taskpaper file named after the git repository'
79
- arg_name 'REPO'
80
- switch %i[repo], negatable: true, default_value: true
81
+ desc 'Use a taskpaper file named after the git repository (enables git integration)'
82
+ arg_name 'REPO_TOP'
83
+ switch %i[repo-top], default_value: false
81
84
 
82
85
  desc 'Provide a template for new/blank todo files, use initconfig to make permanent'
83
- flag %[template]
86
+ flag %(template)
84
87
 
85
88
  desc 'Use current working directory as [p]roject, [t]ag, or [n]one'
86
89
  arg_name 'TYPE'
@@ -117,7 +120,7 @@ class App
117
120
  NA.include_ext = global[:include_ext]
118
121
  NA.na_tag = global[:na_tag]
119
122
  NA.global_file = global[:file]
120
- NA.cwd = File.basename(ENV['PWD'])
123
+ NA.cwd = File.basename(ENV.fetch('PWD', nil))
121
124
  NA.cwd_is = if global[:cwd_as] =~ /^n/
122
125
  :none
123
126
  else
@@ -125,8 +128,8 @@ class App
125
128
  end
126
129
 
127
130
  # start of git repo addition ==================================
128
- # defaut to git repo if in a git managed directory
129
- if global[:repo]
131
+ # Use git repo if --repo-top flag is specified
132
+ if global[:repo_top]
130
133
  begin
131
134
  require 'git'
132
135
 
@@ -142,7 +145,8 @@ class App
142
145
  NA.global_file = taskpaper_file
143
146
  # Add this block to create the file if it doesn't exist
144
147
  unless File.exist?(taskpaper_file)
145
- 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)
146
150
  if res
147
151
  NA.create_todo(taskpaper_file, repo_name, template: global[:template])
148
152
  else
@@ -194,7 +198,7 @@ class App
194
198
  end
195
199
 
196
200
  NA.stdin = $stdin.read.strip if $stdin.stat.size.positive? || $stdin.fcntl(Fcntl::F_GETFL, 0).zero?
197
- NA.stdin = nil unless NA.stdin && NA.stdin.length.positive?
201
+ NA.stdin = nil unless NA.stdin&.length&.positive?
198
202
 
199
203
  NA.globals = []
200
204
  NA.command_line = []
@@ -209,4 +213,6 @@ ARGV.each do |arg|
209
213
  end
210
214
  NA.command = NA.command_line[0]
211
215
 
212
- exit App.run(ARGV)
216
+ exit_code = App.run(ARGV)
217
+ NA::Benchmark.report
218
+ exit exit_code