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/lib/na/theme.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  module NA
4
4
  module Theme
5
5
  class << self
6
+ # Returns a help string describing available color placeholders for themes.
7
+ # @return [String] Help text for theme placeholders
6
8
  def template_help
7
9
  <<~EOHELP
8
10
  Use {X} placeholders to apply colors. Available colors are:
@@ -22,50 +24,56 @@ module NA
22
24
  EOHELP
23
25
  end
24
26
 
27
+ # Loads the theme configuration, merging defaults with any custom theme file and the provided template.
28
+ # Writes the help text and theme YAML to the theme file.
29
+ # @param template [Hash] Additional theme settings to merge
30
+ # @return [Hash] The merged theme configuration
25
31
  def load_theme(template: {})
26
- # Default colorization, can be overridden with full or partial template variable
27
- default_template = {
28
- parent: '{c}',
29
- bracket: '{dc}',
30
- parent_divider: '{xw}/',
31
- action: '{bg}',
32
- project: '{xbk}',
33
- tags: '{m}',
34
- value_parens: '{m}',
35
- values: '{c}',
36
- search_highlight: '{y}',
37
- note: '{dw}',
38
- dirname: '{xdw}',
39
- filename: '{xb}{#eccc87}',
40
- prompt: '{m}',
41
- success: '{bg}',
42
- error: '{b}{#b61d2a}',
43
- warning: '{by}',
44
- debug: '{dw}',
45
- templates: {
46
- output: '%filename%parents| %action',
47
- default: '%parent%action',
48
- single_file: '%parent%action',
49
- multi_file: '%filename%parent%action',
50
- no_file: '%parent%action'
32
+ NA::Benchmark.measure('Theme.load_theme') do
33
+ # Default colorization, can be overridden with full or partial template variable
34
+ default_template = {
35
+ parent: '{c}',
36
+ bracket: '{dc}',
37
+ parent_divider: '{xw}/',
38
+ action: '{bg}',
39
+ project: '{xbk}',
40
+ tags: '{m}',
41
+ value_parens: '{m}',
42
+ values: '{c}',
43
+ search_highlight: '{y}',
44
+ note: '{dw}',
45
+ dirname: '{xdw}',
46
+ filename: '{xb}{#eccc87}',
47
+ prompt: '{m}',
48
+ success: '{bg}',
49
+ error: '{b}{#b61d2a}',
50
+ warning: '{by}',
51
+ debug: '{dw}',
52
+ templates: {
53
+ output: '%filename%parents| %action',
54
+ default: '%parent%action',
55
+ single_file: '%parent%action',
56
+ multi_file: '%filename%parent%action',
57
+ no_file: '%parent%action'
58
+ }
51
59
  }
52
- }
53
60
 
54
- # Load custom theme
55
- theme_file = NA.database_path(file: 'theme.yaml')
56
- theme = if File.exist?(theme_file)
57
- YAML.load(IO.read(theme_file)) || {}
58
- else
59
- {}
60
- end
61
- theme = default_template.deep_merge(theme)
61
+ # Load custom theme
62
+ theme_file = NA.database_path(file: 'theme.yaml')
63
+ theme = if File.exist?(theme_file)
64
+ YAML.load(File.read(theme_file)) || {}
65
+ else
66
+ {}
67
+ end
68
+ theme = default_template.deep_merge(theme)
62
69
 
63
- File.open(theme_file, 'w') do |f|
64
- f.puts template_help.comment
65
- f.puts YAML.dump(theme)
66
- end
70
+ File.open(theme_file, 'w') do |f|
71
+ f.puts template_help.comment
72
+ f.puts YAML.dump(theme)
73
+ end
67
74
 
68
- theme.merge(template)
75
+ theme.merge(template)
76
+ end
69
77
  end
70
78
  end
71
79
  end
data/lib/na/todo.rb CHANGED
@@ -4,172 +4,209 @@ module NA
4
4
  class Todo
5
5
  attr_accessor :actions, :projects, :files
6
6
 
7
+ # Initialize a Todo object and parse actions/projects/files
8
+ #
9
+ # @param options [Hash] Options for parsing todo files
10
+ # @return [void]
7
11
  def initialize(options = {})
8
12
  @files, @actions, @projects = parse(options)
9
13
  end
10
14
 
11
- ##
12
- ## Read a todo file and create a list of actions
13
- ##
14
- ## @param options The options
15
- ##
16
- ## @option depth [Number] The directory depth to
17
- ## search for files
18
- ## @option done [Boolean] include @done actions
19
- ## @option query [Hash] The todo file query
20
- ## @option tag [Array] Tags to search for
21
- ## @option search [String] A search string
22
- ## @option negate [Boolean] Invert results
23
- ## @option regex [Boolean] Interpret as regular
24
- ## expression
25
- ## @option project [String] The project
26
- ## @option require_na [Boolean] Require @na tag
27
- ## @option file_path [String] file path to parse
28
- ##
15
+ # Read a todo file and create a list of actions
16
+ #
17
+ # @param options [Hash] The options
18
+ # @option options [Number] :depth The directory depth to search for files
19
+ # @option options [Boolean] :done include @done actions
20
+ # @option options [Hash] :query The todo file query
21
+ # @option options [Array] :tag Tags to search for
22
+ # @option options [String] :search A search string
23
+ # @option options [Boolean] :negate Invert results
24
+ # @option options [Boolean] :regex Interpret as regular expression
25
+ # @option options [String] :project The project
26
+ # @option options [Boolean] :require_na Require @na tag
27
+ # @option options [String] :file_path file path to parse
28
+ # @return [Array] files, actions, projects
29
29
  def parse(options)
30
- defaults = {
31
- depth: 1,
32
- done: false,
33
- file_path: nil,
34
- negate: false,
35
- project: nil,
36
- query: nil,
37
- regex: false,
38
- require_na: true,
39
- search: nil,
40
- search_note: true,
41
- tag: nil
42
- }
43
-
44
- settings = defaults.merge(options)
45
-
46
- actions = NA::Actions.new
47
- required = []
48
- optional = []
49
- negated = []
50
- required_tag = []
51
- optional_tag = []
52
- negated_tag = []
53
- projects = []
54
-
55
- NA.notify("Tags: #{settings[:tag]}", debug:true)
56
- NA.notify("Search: #{settings[:search]}", debug:true)
57
-
58
- settings[:tag]&.each do |t|
59
- unless t[:tag].nil?
60
- if settings[:negate]
61
- optional_tag.push(t) if t[:negate]
62
- required_tag.push(t) if t[:required] && t[:negate]
63
- negated_tag.push(t) unless t[:negate]
64
- else
65
- optional_tag.push(t) unless t[:negate] || t[:required]
66
- required_tag.push(t) if t[:required] && !t[:negate]
67
- negated_tag.push(t) if t[:negate]
68
- end
69
- end
70
- end
71
-
72
- unless settings[:search].nil? || settings[:search].empty?
73
- if settings[:regex] || settings[:search].is_a?(String)
74
- if settings[:negate]
75
- negated.push(settings[:search])
76
- else
77
- optional.push(settings[:search])
78
- required.push(settings[:search])
79
- end
80
- else
81
- settings[:search].each do |t|
82
- opt, req, neg = parse_search(t, settings[:negate])
83
- optional.concat(opt)
84
- required.concat(req)
85
- negated.concat(neg)
86
- end
30
+ NA::Benchmark.measure('Todo.parse') do
31
+ defaults = {
32
+ depth: 1,
33
+ done: false,
34
+ file_path: nil,
35
+ negate: false,
36
+ hidden: false,
37
+ project: nil,
38
+ query: nil,
39
+ regex: false,
40
+ require_na: true,
41
+ search: nil,
42
+ search_note: true,
43
+ tag: nil
44
+ }
45
+
46
+ settings = defaults.merge(options)
47
+ # Ensure tag is always an Array
48
+ if settings[:tag].nil?
49
+ settings[:tag] = []
50
+ elsif !settings[:tag].is_a?(Array)
51
+ settings[:tag] = [settings[:tag]]
87
52
  end
88
- end
89
53
 
90
- files = if !settings[:file_path].nil?
91
- [settings[:file_path]]
92
- elsif settings[:query].nil?
93
- NA.find_files(depth: settings[:depth])
54
+ actions = NA::Actions.new
55
+ required = []
56
+ optional = []
57
+ negated = []
58
+ required_tag = []
59
+ optional_tag = []
60
+ negated_tag = []
61
+ projects = []
62
+
63
+ NA.notify("Tags: #{settings[:tag]}", debug: true)
64
+ NA.notify("Search: #{settings[:search]}", debug: true)
65
+
66
+ settings[:tag]&.each do |t|
67
+ # If t is a Hash, use its keys; if String, treat as a tag string
68
+ if t.is_a?(Hash)
69
+ unless t[:tag].nil?
70
+ if settings[:negate]
71
+ optional_tag.push(t) if t[:negate]
72
+ required_tag.push(t) if t[:required] && t[:negate]
73
+ negated_tag.push(t) unless t[:negate]
94
74
  else
95
- NA.match_working_dir(settings[:query])
75
+ optional_tag.push(t) unless t[:negate] || t[:required]
76
+ required_tag.push(t) if t[:required] && !t[:negate]
77
+ negated_tag.push(t) if t[:negate]
96
78
  end
97
-
98
- NA.notify("Files: #{files.join(', ')}", debug: true)
99
- files.each do |file|
100
- NA.save_working_dir(File.expand_path(file))
101
- content = file.read_file
102
- indent_level = 0
103
- parent = []
104
- in_yaml = false
105
- in_action = false
106
- content.split(/\n/).each.with_index do |line, idx|
107
- if in_yaml && line !~ /^(---|~~~)\s*$/
108
- NA.notify("YAML: #{line}", debug: true)
109
- elsif line =~ /^(---|~~~)\s*$/
110
- in_yaml = !in_yaml
111
- elsif line.project? && !in_yaml
112
- in_action = false
113
- proj = line.project
114
- indent = line.indent_level
115
-
116
- if indent.zero? # top level project
117
- parent = [proj]
118
- elsif indent <= indent_level # if indent level is same or less, split parent before indent level and append
119
- parent.slice!(indent, parent.count - indent)
120
- parent.push(proj)
121
- else # if indent level is greater, append project to parent
122
- parent.push(proj)
123
79
  end
80
+ elsif t.is_a?(String)
81
+ # Treat string as a simple tag
82
+ optional_tag.push({ tag: t })
83
+ end
84
+ end
124
85
 
125
- projects.push(NA::Project.new(parent.join(':'), indent, idx, idx))
86
+ unless settings[:search].nil? || settings[:search].empty?
87
+ if settings[:regex] || settings[:search].is_a?(String)
88
+ if settings[:negate]
89
+ negated.push(settings[:search])
90
+ else
91
+ optional.push(settings[:search])
92
+ required.push(settings[:search])
93
+ end
94
+ else
95
+ settings[:search].each do |t|
96
+ opt, req, neg = parse_search(t, settings[:negate])
97
+ optional.concat(opt)
98
+ required.concat(req)
99
+ negated.concat(neg)
100
+ end
101
+ end
102
+ end
126
103
 
127
- indent_level = indent
128
- elsif line.blank?
129
- in_action = false # Comment out to allow line breaks in of notes, which isn't TaskPaper-compatible
130
- elsif line.action?
104
+ # Pre-compile regexes for better performance
105
+ optional = optional.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
106
+ required = required.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
107
+ negated = negated.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
108
+
109
+ files = if !settings[:file_path].nil?
110
+ [settings[:file_path]]
111
+ elsif settings[:query].nil?
112
+ NA.find_files(depth: settings[:depth], include_hidden: settings[:hidden])
113
+ else
114
+ NA.match_working_dir(settings[:query])
115
+ end
116
+
117
+ NA.notify("Files: #{files.join(', ')}", debug: true)
118
+ # Cache project regex compilation outside the line loop for better performance
119
+ project_regex = if settings[:project]
120
+ rx = settings[:project].split(%r{[/:]}).join('.*?/')
121
+ Regexp.new("#{rx}.*?", Regexp::IGNORECASE)
122
+ end
123
+
124
+ files.each do |file|
125
+ next if File.directory?(file)
126
+
127
+ NA::Benchmark.measure("Parse file: #{File.basename(file)}") do
128
+ NA.save_working_dir(File.expand_path(file))
129
+ content = file.read_file
130
+ indent_level = 0
131
+ parent = []
132
+ in_yaml = false
131
133
  in_action = false
132
-
133
- action = line.action
134
- new_action = NA::Action.new(file, File.basename(file, ".#{NA.extension}"), parent.dup, action, idx)
135
-
136
- projects[-1].last_line = idx if projects.count.positive?
137
-
138
- next if line.done? && !settings[:done]
139
-
140
- next if settings[:require_na] && !line.na?
141
-
142
- if settings[:project]
143
- rx = settings[:project].split(%r{[/:]}).join('.*?/')
144
- next unless parent.join('/') =~ Regexp.new("#{rx}.*?", Regexp::IGNORECASE)
134
+ content.split("\n").each.with_index do |line, idx|
135
+ if in_yaml && line !~ /^(---|~~~)\s*$/
136
+ NA.notify("YAML: #{line}", debug: true)
137
+ elsif line =~ /^(---|~~~)\s*$/
138
+ in_yaml = !in_yaml
139
+ elsif line.project? && !in_yaml
140
+ in_action = false
141
+ proj = line.project
142
+ indent = line.indent_level
143
+
144
+ if indent.zero? # top level project
145
+ parent = [proj]
146
+ elsif indent <= indent_level # if indent level is same or less, split parent before indent level and append
147
+ parent.slice!(indent, parent.count - indent)
148
+ parent.push(proj)
149
+ else # if indent level is greater, append project to parent
150
+ parent.push(proj)
151
+ end
152
+
153
+ projects.push(NA::Project.new(parent.join(':'), indent, idx, idx))
154
+
155
+ indent_level = indent
156
+ elsif line.blank?
157
+ in_action = false # Comment out to allow line breaks in of notes, which isn't TaskPaper-compatible
158
+ elsif line.action?
159
+ in_action = false
160
+
161
+ # Early exits before creating Action object
162
+ next if line.done? && !settings[:done]
163
+
164
+ next if settings[:require_na] && !line.na?
165
+
166
+ next if project_regex && parent.join('/') !~ project_regex
167
+
168
+ # Only create Action if we passed basic filters
169
+ action = line.action
170
+ new_action = NA::Action.new(file, File.basename(file, ".#{NA.extension}"), parent.dup, action, idx)
171
+
172
+ projects[-1].last_line = idx if projects.count.positive?
173
+
174
+ # Tag matching
175
+ has_tag = !optional_tag.empty? || !required_tag.empty? || !negated_tag.empty?
176
+ next if has_tag && !new_action.tags_match?(any: optional_tag,
177
+ all: required_tag,
178
+ none: negated_tag)
179
+
180
+ actions.push(new_action)
181
+ in_action = true
182
+ elsif in_action
183
+ actions[-1].note.push(line.strip) if actions.count.positive?
184
+ projects[-1].last_line = idx if projects.count.positive?
185
+ end
145
186
  end
187
+ projects = projects.dup
188
+ end
189
+ end
146
190
 
147
- has_tag = !optional_tag.empty? || !required_tag.empty? || !negated_tag.empty?
148
- next if has_tag && !new_action.tags_match?(any: optional_tag,
149
- all: required_tag,
150
- none: negated_tag)
151
-
152
- actions.push(new_action)
153
- in_action = true
154
- elsif in_action
155
- actions[-1].note.push(line.strip) if actions.count.positive?
156
- projects[-1].last_line = idx if projects.count.positive?
191
+ NA::Benchmark.measure('Filter actions by search') do
192
+ actions.delete_if do |new_action|
193
+ has_search = !optional.empty? || !required.empty? || !negated.empty?
194
+ has_search && !new_action.search_match?(any: optional,
195
+ all: required,
196
+ none: negated,
197
+ include_note: settings[:search_note])
157
198
  end
158
199
  end
159
- projects = projects.dup
160
- end
161
200
 
162
- actions.delete_if do |new_action|
163
- has_search = !optional.empty? || !required.empty? || !negated.empty?
164
- has_search && !new_action.search_match?(any: optional,
165
- all: required,
166
- none: negated,
167
- include_note: settings[:search_note])
201
+ [files, actions, projects]
168
202
  end
169
-
170
- [files, actions, projects]
171
203
  end
172
204
 
205
+ # Parse a search tag and categorize as optional, required, or negated
206
+ #
207
+ # @param tag [Hash] Search tag with :token, :negate, :required
208
+ # @param negate [Boolean] Invert results
209
+ # @return [Array<Array>] Arrays of optional, required, and negated regexes
173
210
  def parse_search(tag, negate)
174
211
  required = []
175
212
  optional = []
data/lib/na/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Na
2
- VERSION = '1.2.79'
4
+ VERSION = '1.2.81'
3
5
  end
data/lib/na.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'na/benchmark'
3
4
  require 'na/version'
4
5
  require 'na/pager'
5
6
  require 'time'
6
7
  require 'fileutils'
7
8
  require 'shellwords'
8
- require 'chronic'
9
+ # Lazy load heavy gems - only load when needed
10
+ # require 'chronic' # Loaded in action.rb and string.rb when needed
9
11
  require 'tty-screen'
10
12
  require 'tty-reader'
11
13
  require 'tty-which'
@@ -13,6 +15,7 @@ require 'na/hash'
13
15
  require 'na/colors'
14
16
  require 'na/string'
15
17
  require 'na/array'
18
+ require 'yaml'
16
19
  require 'na/theme'
17
20
  require 'na/todo'
18
21
  require 'na/actions'
data/na.gemspec CHANGED
@@ -24,12 +24,14 @@ spec = Gem::Specification.new do |s|
24
24
  s.add_development_dependency('minitest', '~> 5.14')
25
25
  s.add_development_dependency('rdoc', '~> 4.3')
26
26
  s.add_runtime_dependency('chronic', '~> 0.10', '>= 0.10.2')
27
+ s.add_runtime_dependency('git', '~> 3.0.0')
27
28
  s.add_runtime_dependency('gli','~> 2.21.0')
28
29
  s.add_runtime_dependency('mdless', '~> 1.0', '>= 1.0.32')
30
+ s.add_runtime_dependency('ostruct', '~> 0.6', '>= 0.6.1')
29
31
  s.add_runtime_dependency('tty-reader', '~> 0.9', '>= 0.9.0')
30
32
  s.add_runtime_dependency('tty-screen', '~> 0.8', '>= 0.8.1')
31
33
  s.add_runtime_dependency('tty-which', '~> 0.5', '>= 0.5.0')
32
- s.add_runtime_dependency('git', '~> 3.0.0')
33
- s.add_runtime_dependency('ostruct', '~> 0.6', '>= 0.6.1')
34
34
  s.add_development_dependency('tty-spinner', '~> 0.9', '>= 0.9.0')
35
+ s.add_development_dependency 'rspec', '~> 3.0'
36
+ s.add_development_dependency 'bump', '~> 0.6.0'
35
37
  end
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
2
4
  require 'tty-progressbar'
3
5
  require 'shellwords'
4
6
 
@@ -21,7 +23,6 @@ class ::String
21
23
  end
22
24
 
23
25
  class FishCompletions
24
-
25
26
  attr_accessor :commands, :global_options
26
27
 
27
28
  def generate_helpers
@@ -68,7 +69,7 @@ class FishCompletions
68
69
  sections = {}
69
70
  scanned.each do |sect|
70
71
  title = sect[0].downcase.strip.gsub(/ +/, '_').to_sym
71
- content = sect[1].split(/\n/).map(&:strip).delete_if(&:empty?)
72
+ content = sect[1].split("\n").map(&:strip).delete_if(&:empty?)
72
73
  sections[title] = content
73
74
  end
74
75
  sections
@@ -77,6 +78,7 @@ class FishCompletions
77
78
  def parse_option(option)
78
79
  res = option.match(/(?:-(?<short>\w), )?(?:--(?:\[no-\])?(?<long>w+)(?:=(?<arg>\w+))?)\s+- (?<desc>.*?)$/)
79
80
  return nil unless res
81
+
80
82
  {
81
83
  short: res['short'],
82
84
  long: res['long'],
@@ -92,7 +94,7 @@ class FishCompletions
92
94
  def parse_command(command)
93
95
  res = command.match(/^(?<cmd>[^, \t]+)(?<alias>(?:, [^, \t]+)*)?\s+- (?<desc>.*?)$/)
94
96
  commands = [res['cmd']]
95
- commands.concat(res['alias'].split(/, /).delete_if(&:empty?)) if res['alias']
97
+ commands.concat(res['alias'].split(', ').delete_if(&:empty?)) if res['alias']
96
98
 
97
99
  {
98
100
  commands: commands,
@@ -106,7 +108,7 @@ class FishCompletions
106
108
 
107
109
  def generate_subcommand_completions
108
110
  out = []
109
- @commands.each_with_index do |cmd, i|
111
+ @commands.each_with_index do |cmd, _i|
110
112
  out << "complete -xc na -n '__fish_na_needs_command' -a '#{cmd[:commands].join(' ')}' -d #{Shellwords.escape(cmd[:description])}"
111
113
  end
112
114
 
@@ -114,35 +116,30 @@ class FishCompletions
114
116
  end
115
117
 
116
118
  def generate_subcommand_option_completions
117
-
118
119
  out = []
119
120
  need_export = []
120
121
 
121
- @commands.each_with_index do |cmd, i|
122
+ @commands.each_with_index do |cmd, _i|
122
123
  @bar.advance
123
124
  data = get_help_sections(cmd[:commands].first)
124
125
 
125
- if data[:synopsis].join(' ').strip.split(/ /).last =~ /(path|file)/i
126
- out << "complete -c na -F -n '__fish_na_using_command #{cmd[:commands].join(" ")}'"
127
- end
126
+ out << "complete -c na -F -n '__fish_na_using_command #{cmd[:commands].join(' ')}'" if data[:synopsis].join(' ').strip.split(/ /).last =~ /(path|file)/i
128
127
 
129
- if data[:command_options]
130
- parse_options(data[:command_options]).each do |option|
131
- next if option.nil?
128
+ next unless data[:command_options]
132
129
 
133
- arg = option[:arg] ? '-r' : ''
134
- short = option[:short] ? "-s #{option[:short]}" : ''
135
- long = option[:long] ? "-l #{option[:long]}" : ''
136
- out << "complete -c na #{long} #{short} -f #{arg} -n '__fish_na_using_command #{cmd[:commands].join(' ')}' -d #{Shellwords.escape(option[:description])}"
130
+ parse_options(data[:command_options]).each do |option|
131
+ next if option.nil?
137
132
 
138
- need_export.concat(cmd[:commands]) if option[:long] == 'output'
139
- end
133
+ arg = option[:arg] ? '-r' : ''
134
+ short = option[:short] ? "-s #{option[:short]}" : ''
135
+ long = option[:long] ? "-l #{option[:long]}" : ''
136
+ out << "complete -c na #{long} #{short} -f #{arg} -n '__fish_na_using_command #{cmd[:commands].join(' ')}' -d #{Shellwords.escape(option[:description])}"
137
+
138
+ need_export.concat(cmd[:commands]) if option[:long] == 'output'
140
139
  end
141
140
  end
142
141
 
143
- unless need_export.empty?
144
- out << "complete -f -c na -s o -l output -x -n '__fish_na_using_command #{need_export.join(' ')}' -a '(__fish_na_export_plugins)'"
145
- end
142
+ out << "complete -f -c na -s o -l output -x -n '__fish_na_using_command #{need_export.join(' ')}' -a '(__fish_na_export_plugins)'" unless need_export.empty?
146
143
 
147
144
  # clear
148
145
  out.join("\n")
data/src/_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 <!--VER-->1.2.78<!--END VER-->.
12
+ The current version of `na` is <!--VER-->1.2.80<!--END VER-->.
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
 
@@ -82,7 +82,7 @@ If you run the `add` command with no arguments, you'll be asked for input on the
82
82
 
83
83
  ###### Adding notes
84
84
 
85
- 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.
85
+ 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.
86
86
 
87
87
  Notes are not displayed by the `next/tagged/find` commands unless `--notes` is specified.
88
88
 
@@ -163,6 +163,16 @@ Run `na saved` without an argument to list your saved searches.
163
163
  @cli(bundle exec bin/na help saved)
164
164
  ```
165
165
 
166
+ ##### scan
167
+
168
+ Scan a directory tree for todo files and cache them in tdlist.txt. Avoids duplicates and can optionally prune non-existent entries.
169
+
170
+ 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.
171
+
172
+ ```
173
+ @cli(bundle exec bin/na help scan)
174
+ ```
175
+
166
176
  ##### tagged
167
177
 
168
178
  Example: `na tagged feature +maybe`.