na 1.2.80 → 1.2.82

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,43 +24,47 @@ 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
32
  NA::Benchmark.measure('Theme.load_theme') do
27
33
  # Default colorization, can be overridden with full or partial template variable
28
34
  default_template = {
29
- parent: '{c}',
30
- bracket: '{dc}',
31
- parent_divider: '{xw}/',
32
- action: '{bg}',
33
- project: '{xbk}',
34
- tags: '{m}',
35
- value_parens: '{m}',
36
- values: '{c}',
37
- search_highlight: '{y}',
38
- note: '{dw}',
39
- dirname: '{xdw}',
40
- filename: '{xb}{#eccc87}',
41
- prompt: '{m}',
42
- success: '{bg}',
43
- error: '{b}{#b61d2a}',
44
- warning: '{by}',
45
- debug: '{dw}',
46
- templates: {
47
- output: '%filename%parents| %action',
48
- default: '%parent%action',
49
- single_file: '%parent%action',
50
- multi_file: '%filename%parent%action',
51
- no_file: '%parent%action'
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
+ }
52
59
  }
53
- }
54
60
 
55
- # Load custom theme
56
- theme_file = NA.database_path(file: 'theme.yaml')
57
- theme = if File.exist?(theme_file)
58
- YAML.load(IO.read(theme_file)) || {}
59
- else
60
- {}
61
- end
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
62
68
  theme = default_template.deep_merge(theme)
63
69
 
64
70
  File.open(theme_file, 'w') do |f|
data/lib/na/todo.rb CHANGED
@@ -4,28 +4,28 @@ 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
30
  NA::Benchmark.measure('Todo.parse') do
31
31
  defaults = {
@@ -33,6 +33,7 @@ module NA
33
33
  done: false,
34
34
  file_path: nil,
35
35
  negate: false,
36
+ hidden: false,
36
37
  project: nil,
37
38
  query: nil,
38
39
  regex: false,
@@ -43,72 +44,89 @@ module NA
43
44
  }
44
45
 
45
46
  settings = defaults.merge(options)
47
+ # Coerce settings[:search] to a string or nil if it's an integer
48
+ if settings[:search].is_a?(Integer)
49
+ settings[:search] = settings[:search] <= 0 ? nil : settings[:search].to_s
50
+ end
51
+ # Ensure tag is always an Array
52
+ if settings[:tag].nil?
53
+ settings[:tag] = []
54
+ elsif !settings[:tag].is_a?(Array)
55
+ settings[:tag] = [settings[:tag]]
56
+ end
46
57
 
47
- actions = NA::Actions.new
48
- required = []
49
- optional = []
50
- negated = []
51
- required_tag = []
52
- optional_tag = []
53
- negated_tag = []
54
- projects = []
55
-
56
- NA.notify("Tags: #{settings[:tag]}", debug:true)
57
- NA.notify("Search: #{settings[:search]}", debug:true)
58
-
59
- settings[:tag]&.each do |t|
60
- unless t[:tag].nil?
61
- if settings[:negate]
62
- optional_tag.push(t) if t[:negate]
63
- required_tag.push(t) if t[:required] && t[:negate]
64
- negated_tag.push(t) unless t[:negate]
65
- else
66
- optional_tag.push(t) unless t[:negate] || t[:required]
67
- required_tag.push(t) if t[:required] && !t[:negate]
68
- negated_tag.push(t) if t[:negate]
58
+ actions = NA::Actions.new
59
+ required = []
60
+ optional = []
61
+ negated = []
62
+ required_tag = []
63
+ optional_tag = []
64
+ negated_tag = []
65
+ projects = []
66
+
67
+ NA.notify("Tags: #{settings[:tag]}", debug: true)
68
+ NA.notify("Search: #{settings[:search]}", debug: true)
69
+
70
+ settings[:tag]&.each do |t|
71
+ # If t is a Hash, use its keys; if String, treat as a tag string
72
+ if t.is_a?(Hash)
73
+ unless t[:tag].nil?
74
+ if settings[:negate]
75
+ optional_tag.push(t) if t[:negate]
76
+ required_tag.push(t) if t[:required] && t[:negate]
77
+ negated_tag.push(t) unless t[:negate]
78
+ else
79
+ optional_tag.push(t) unless t[:negate] || t[:required]
80
+ required_tag.push(t) if t[:required] && !t[:negate]
81
+ negated_tag.push(t) if t[:negate]
82
+ end
83
+ end
84
+ elsif t.is_a?(String)
85
+ # Treat string as a simple tag
86
+ optional_tag.push({ tag: t })
69
87
  end
70
88
  end
71
- end
72
-
73
- unless settings[:search].nil? || settings[:search].empty?
74
- if settings[:regex] || settings[:search].is_a?(String)
75
- if settings[:negate]
76
- negated.push(settings[:search])
89
+ unless settings[:search].nil? || (settings[:search].respond_to?(:empty?) && settings[:search].empty?)
90
+ if settings[:regex] || settings[:search].is_a?(String)
91
+ if settings[:negate]
92
+ negated.push(settings[:search])
93
+ else
94
+ optional.push(settings[:search])
95
+ required.push(settings[:search])
96
+ end
77
97
  else
78
- optional.push(settings[:search])
79
- required.push(settings[:search])
80
- end
81
- else
82
- settings[:search].each do |t|
83
- opt, req, neg = parse_search(t, settings[:negate])
84
- optional.concat(opt)
85
- required.concat(req)
86
- negated.concat(neg)
98
+ settings[:search].each do |t|
99
+ opt, req, neg = parse_search(t, settings[:negate])
100
+ optional.concat(opt)
101
+ required.concat(req)
102
+ negated.concat(neg)
103
+ end
87
104
  end
88
105
  end
89
- end
90
-
91
- # Pre-compile regexes for better performance
92
- optional = optional.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
93
- required = required.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
94
- negated = negated.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
95
106
 
96
- files = if !settings[:file_path].nil?
97
- [settings[:file_path]]
98
- elsif settings[:query].nil?
99
- NA.find_files(depth: settings[:depth])
100
- else
101
- NA.match_working_dir(settings[:query])
102
- end
103
-
104
- NA.notify("Files: #{files.join(', ')}", debug: true)
105
- # Cache project regex compilation outside the line loop for better performance
106
- project_regex = if settings[:project]
107
- rx = settings[:project].split(%r{[/:]}).join('.*?/')
108
- Regexp.new("#{rx}.*?", Regexp::IGNORECASE)
109
- end
107
+ # Pre-compile regexes for better performance
108
+ optional = optional.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
109
+ required = required.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
110
+ negated = negated.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
111
+
112
+ files = if !settings[:file_path].nil?
113
+ [settings[:file_path]]
114
+ elsif settings[:query].nil?
115
+ NA.find_files(depth: settings[:depth], include_hidden: settings[:hidden])
116
+ else
117
+ NA.match_working_dir(settings[:query])
118
+ end
119
+
120
+ NA.notify("Files: #{files.join(', ')}", debug: true)
121
+ # Cache project regex compilation outside the line loop for better performance
122
+ project_regex = if settings[:project]
123
+ rx = settings[:project].split(%r{[/:]}).join('.*?/')
124
+ Regexp.new("#{rx}.*?", Regexp::IGNORECASE)
125
+ end
110
126
 
111
127
  files.each do |file|
128
+ next if File.directory?(file)
129
+
112
130
  NA::Benchmark.measure("Parse file: #{File.basename(file)}") do
113
131
  NA.save_working_dir(File.expand_path(file))
114
132
  content = file.read_file
@@ -116,60 +134,58 @@ module NA
116
134
  parent = []
117
135
  in_yaml = false
118
136
  in_action = false
119
- content.split(/\n/).each.with_index do |line, idx|
120
- if in_yaml && line !~ /^(---|~~~)\s*$/
121
- NA.notify("YAML: #{line}", debug: true)
122
- elsif line =~ /^(---|~~~)\s*$/
123
- in_yaml = !in_yaml
124
- elsif line.project? && !in_yaml
125
- in_action = false
126
- proj = line.project
127
- indent = line.indent_level
128
-
129
- if indent.zero? # top level project
130
- parent = [proj]
131
- elsif indent <= indent_level # if indent level is same or less, split parent before indent level and append
132
- parent.slice!(indent, parent.count - indent)
133
- parent.push(proj)
134
- else # if indent level is greater, append project to parent
135
- parent.push(proj)
136
- end
137
-
138
- projects.push(NA::Project.new(parent.join(':'), indent, idx, idx))
139
-
140
- indent_level = indent
141
- elsif line.blank?
142
- in_action = false # Comment out to allow line breaks in of notes, which isn't TaskPaper-compatible
143
- elsif line.action?
144
- in_action = false
145
-
146
- # Early exits before creating Action object
147
- next if line.done? && !settings[:done]
148
-
149
- next if settings[:require_na] && !line.na?
150
-
151
- if project_regex
152
- next unless parent.join('/') =~ project_regex
153
- end
154
-
155
- # Only create Action if we passed basic filters
156
- action = line.action
157
- new_action = NA::Action.new(file, File.basename(file, ".#{NA.extension}"), parent.dup, action, idx)
158
-
159
- projects[-1].last_line = idx if projects.count.positive?
160
-
161
- # Tag matching
162
- has_tag = !optional_tag.empty? || !required_tag.empty? || !negated_tag.empty?
163
- next if has_tag && !new_action.tags_match?(any: optional_tag,
164
- all: required_tag,
165
- none: negated_tag)
166
-
167
- actions.push(new_action)
168
- in_action = true
169
- elsif in_action
170
- actions[-1].note.push(line.strip) if actions.count.positive?
171
- projects[-1].last_line = idx if projects.count.positive?
172
- end
137
+ content.split("\n").each.with_index do |line, idx|
138
+ if in_yaml && line !~ /^(---|~~~)\s*$/
139
+ NA.notify("YAML: #{line}", debug: true)
140
+ elsif line =~ /^(---|~~~)\s*$/
141
+ in_yaml = !in_yaml
142
+ elsif line.project? && !in_yaml
143
+ in_action = false
144
+ proj = line.project
145
+ indent = line.indent_level
146
+
147
+ if indent.zero? # top level project
148
+ parent = [proj]
149
+ elsif indent <= indent_level # if indent level is same or less, split parent before indent level and append
150
+ parent.slice!(indent, parent.count - indent)
151
+ parent.push(proj)
152
+ else # if indent level is greater, append project to parent
153
+ parent.push(proj)
154
+ end
155
+
156
+ projects.push(NA::Project.new(parent.join(':'), indent, idx, idx))
157
+
158
+ indent_level = indent
159
+ elsif line.blank?
160
+ in_action = false # Comment out to allow line breaks in of notes, which isn't TaskPaper-compatible
161
+ elsif line.action?
162
+ in_action = false
163
+
164
+ # Early exits before creating Action object
165
+ next if line.done? && !settings[:done]
166
+
167
+ next if settings[:require_na] && !line.na?
168
+
169
+ next if project_regex && parent.join('/') !~ project_regex
170
+
171
+ # Only create Action if we passed basic filters
172
+ action = line.action
173
+ new_action = NA::Action.new(file, File.basename(file, ".#{NA.extension}"), parent.dup, action, idx)
174
+
175
+ projects[-1].last_line = idx if projects.count.positive?
176
+
177
+ # Tag matching
178
+ has_tag = !optional_tag.empty? || !required_tag.empty? || !negated_tag.empty?
179
+ next if has_tag && !new_action.tags_match?(any: optional_tag,
180
+ all: required_tag,
181
+ none: negated_tag)
182
+
183
+ actions.push(new_action)
184
+ in_action = true
185
+ elsif in_action
186
+ actions[-1].note.push(line.strip) if actions.count.positive?
187
+ projects[-1].last_line = idx if projects.count.positive?
188
+ end
173
189
  end
174
190
  projects = projects.dup
175
191
  end
@@ -179,9 +195,9 @@ module NA
179
195
  actions.delete_if do |new_action|
180
196
  has_search = !optional.empty? || !required.empty? || !negated.empty?
181
197
  has_search && !new_action.search_match?(any: optional,
182
- all: required,
183
- none: negated,
184
- include_note: settings[:search_note])
198
+ all: required,
199
+ none: negated,
200
+ include_note: settings[:search_note])
185
201
  end
186
202
  end
187
203
 
@@ -189,6 +205,11 @@ module NA
189
205
  end
190
206
  end
191
207
 
208
+ # Parse a search tag and categorize as optional, required, or negated
209
+ #
210
+ # @param tag [Hash] Search tag with :token, :negate, :required
211
+ # @param negate [Boolean] Invert results
212
+ # @return [Array<Array>] Arrays of optional, required, and negated regexes
192
213
  def parse_search(tag, negate)
193
214
  required = []
194
215
  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.80'
4
+ VERSION = '1.2.82'
3
5
  end
data/lib/na.rb CHANGED
@@ -15,6 +15,7 @@ require 'na/hash'
15
15
  require 'na/colors'
16
16
  require 'na/string'
17
17
  require 'na/array'
18
+ require 'yaml'
18
19
  require 'na/theme'
19
20
  require 'na/todo'
20
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.79<!--END VER-->.
12
+ The current version of `na` is <!--VER-->1.2.81<!--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`.
@@ -174,7 +184,7 @@ You can also perform value comparisons on tags. A value in a TaskPaper tag is ad
174
184
  To perform a string comparison, you can use `*=` (contains), `^=` (starts with), `$=` (ends with), or `=` (matches). E.g. `na tagged "note*=video"`.
175
185
 
176
186
  ```
177
- @cli(bundle exec bin/na help show)
187
+ @cli(bundle exec bin/na help tagged)
178
188
  ```
179
189
 
180
190
  ##### todos
@@ -199,6 +209,8 @@ If more than one file is matched, a menu will be presented, multiple selections
199
209
 
200
210
  Any time an update action is carried out, a backup of the file before modification will be made in the same directory with a `.` prepended and `.bak` appended (e.g. `marked.taskpaper` is copied to `.marked.taskpaper.bak`). Only one undo step is available, but if something goes wrong (and this feature is still experimental, so be wary), you can just copy the ".bak" file back to the original.
201
211
 
212
+ > **Note:** When using the `update` command, if you have [fzf](https://github.com/junegunn/fzf) installed, menus for selecting files or actions will support multi-select (tab to mark multiple, return to confirm). If [gum](https://github.com/charmbracelet/gum) is installed, multi-select is also supported (use j/k/x to navigate and mark). If neither is available, a simple prompt is used. This makes it easy to apply updates to multiple actions at once.
213
+
202
214
  ###### Marking a task as complete
203
215
 
204
216
  You can mark an action complete using `--finish`, which will add a dated @done tag to the action. You can also mark it @done and immediately move it to the Archive project using `--archive`.