na 1.1.10 → 1.1.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d9dd176974a6c4c20be90d5051c7608469ea7b105537a7859697e3be4050ea5
4
- data.tar.gz: b201fecc68568c3b80c6626eb425929d2f50faf6a1bd5fe5bae076021ea0d8b1
3
+ metadata.gz: da1e1113960a99932feda46ee18cd7a8b9e8d309f479c9e3a6ec323d97db686a
4
+ data.tar.gz: 871fcae47831f04efcff94b7034abb70d32cb47b2f504247b94705ad015c191f
5
5
  SHA512:
6
- metadata.gz: 169372ba631178c20af2af22e461ba0a6c9fcb7e7e5421d71464b1f180ad766bcd2888d30305354554e24ab86c366c12ffdbab9cc2e0f6c7c03f48f402a32679
7
- data.tar.gz: c6ea88026936d1f0fe6d9c822945bd7f9ecf351a67871d5fbe27673a2142ef03502fb1759d20d817b4ab8f435942cf6bbdfa24829e535c9684fe507549ac5bee
6
+ metadata.gz: c8e8c92c9d3a3ffa4a00fe8fcdd8ace94681e19f9ac53a3d8b988bcf4bd69d3b032765b4a870e3d9f660cd58b18783e97e8333cdb252164cb60f0062495210b5
7
+ data.tar.gz: de5596e85883b893afbe2ca43833fe60f82fd96ab12e1f2506d84d13a90644d5d403b748b6598c9fb4c4d4db3998bdd0e560a1c071f4f7c2946b5f75dd45c728
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ### 1.1.12
2
+
3
+ 2022-10-06 05:42
4
+
5
+ #### NEW
6
+
7
+ - `na add -d X` to allow adding new actions to todo files in subdirectories
8
+ - You can now perform <>= queries on tag values (`na tagged "priority>=3"`)
9
+ - You can now perform string matches on tag values (`na tagged "note*=markdown"`)
10
+ - You can use `--project X` to display only actions within a specific project. Specify subprojects with a path, e.g. `na/bugs`. Partial matches allowed, works with `next`, `find`, and `tagged`
11
+ - Find and tagged recognize * and ? as wildcards
12
+ - --regex flag for find command
13
+ - --invert command (like grep -v) for find
14
+ - -v/--invert for tagged command
15
+
16
+ #### IMPROVED
17
+
18
+ - Require value 1-9 for --depth option
19
+
20
+ ### 1.1.11
21
+
22
+ 2022-10-05 08:56
23
+
24
+ #### IMPROVED
25
+
26
+ - Respect na_tag setting when creating new todo file
27
+ - Code cleanup
28
+
1
29
  ### 1.1.10
2
30
 
3
31
  2022-10-05 08:19
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.1.10)
4
+ na (1.1.12)
5
5
  gli (~> 2.21.0)
6
6
  tty-reader (~> 0.9, >= 0.9.0)
7
7
  tty-screen (~> 0.8, >= 0.8.1)
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  _If you're one of the rare people like me who find this useful, feel free to
10
10
  [buy me some coffee][donate]._
11
11
 
12
- The current version of `na` is 1.1.10
12
+ The current version of `na` is 1.1.12
13
13
  .
14
14
 
15
15
  `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.
data/bin/na CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
2
4
  $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
3
5
  require 'gli'
4
6
  require 'na'
5
7
 
8
+ # Main application
6
9
  class App
7
10
  extend GLI::App
8
11
 
@@ -17,7 +20,7 @@ class App
17
20
 
18
21
  desc 'File extension to consider a todo file'
19
22
  default_value 'taskpaper'
20
- arg_name 'FILE_EXTENSION'
23
+ arg_name 'EXT'
21
24
  flag :ext
22
25
 
23
26
  desc 'Tag to consider a next action'
@@ -45,7 +48,7 @@ class App
45
48
  desc 'Recurse to depth'
46
49
  arg_name 'DEPTH'
47
50
  default_value 1
48
- flag %i[d depth], type: :integer, must_match: /^\d+$/
51
+ flag %i[d depth], type: :integer, must_match: /^[1-9]$/
49
52
 
50
53
  desc 'Display verbose output'
51
54
  switch %i[debug]
@@ -56,13 +59,19 @@ class App
56
59
  c.example 'na next', desc: 'display the next actions from any todo files in the current directory'
57
60
  c.example 'na next -d 3', desc: 'display the next actions from the current directory, traversing 3 levels deep'
58
61
  c.example 'na next marked', desc: 'display next actions for a project you visited in the past'
62
+
59
63
  c.desc 'Recurse to depth'
60
64
  c.arg_name 'DEPTH'
61
- c.flag %i[d depth], type: :integer, must_match: /^\d+$/
65
+ c.flag %i[d depth], type: :integer, must_match: /^[1-9]$/
62
66
 
63
67
  c.desc 'Alternate tag to search for'
68
+ c.arg_name 'TAG'
64
69
  c.flag %i[t tag]
65
70
 
71
+ c.desc 'Show actions from a specific project'
72
+ c.arg_name 'PROJECT[/SUBPROJECT]'
73
+ c.flag %i[proj project]
74
+
66
75
  c.action do |global_options, options, args|
67
76
  if global_options[:add]
68
77
  cmd = ['add']
@@ -97,6 +106,7 @@ class App
97
106
  files, actions = NA.parse_actions(depth: depth,
98
107
  query: tokens,
99
108
  tag: tag,
109
+ project: options[:project],
100
110
  require_na: require_na)
101
111
 
102
112
  NA.output_actions(actions, depth, files: files)
@@ -119,11 +129,13 @@ class App
119
129
  c.switch %i[n note], negatable: false
120
130
 
121
131
  c.desc 'Add a priority level 1-5'
132
+ c.arg_name 'PRIO'
122
133
  c.flag %i[p priority], must_match: /[1-5]/, type: :integer, default_value: 0
123
134
 
124
135
  c.desc 'Add action to specific project'
136
+ c.arg_name 'PROJECT'
125
137
  c.default_value 'Inbox'
126
- c.flag %[to]
138
+ c.flag %i[to]
127
139
 
128
140
  c.desc 'Use a tag other than the default next action tag'
129
141
  c.arg_name 'TAG'
@@ -136,7 +148,11 @@ class App
136
148
  c.arg_name 'PATH'
137
149
  c.flag %i[f file]
138
150
 
139
- c.action do |global_options, options, args|
151
+ c.desc 'Search for files X directories deep'
152
+ c.arg_name 'DEPTH'
153
+ c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
154
+
155
+ c.action do |_global_options, options, args|
140
156
  reader = TTY::Reader.new
141
157
  action = if args.count.positive?
142
158
  args.join(' ').strip
@@ -168,7 +184,10 @@ class App
168
184
 
169
185
  note = if options[:note]
170
186
  if TTY::Which.exist?('gum')
171
- `gum write --placeholder "Enter a note, CTRL-d to save" --width $(tput cols) --char-limit 0`.strip.split("\n")
187
+ args = ['--placeholder "Enter a note, CTRL-d to save"']
188
+ args << '--char-limit 0'
189
+ args << '--width $(tput cols)'
190
+ `gum write #{args.join(' ')}`.strip.split("\n")
172
191
  else
173
192
  puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
174
193
  reader.read_multiline
@@ -189,8 +208,8 @@ class App
189
208
  end
190
209
  end
191
210
  else
192
- files = NA.find_files(depth: 1)
193
- if files.count == 0
211
+ files = NA.find_files(depth: options[:depth])
212
+ if files.count.zero?
194
213
  print NA::Color.template('{by}No todo file found, create one? {w}(y/{g}N{w}){x} ')
195
214
  res = reader.read_char
196
215
  if res =~ /y/i
@@ -215,19 +234,28 @@ class App
215
234
  long_desc 'Search tokens are separated by spaces. Actions matching any token in the pattern will be shown
216
235
  (partial matches allowed). Add a + before a token to make it required, e.g. `na find +feature +maybe`'
217
236
  arg_name 'PATTERN'
218
- command %i[find] do |c|
237
+ command %i[find grep] do |c|
219
238
  c.example 'na find feature +idea +swift', desc: 'Find all actions containing feature, idea, and swift'
220
239
  c.example 'na find -x feature idea', desc: 'Find all actions containing the exact text "feature idea"'
221
240
  c.example 'na find -d 3 swift obj-c', desc: 'Find all actions 3 directories deep containing either swift or obj-c'
222
241
 
242
+ c.desc 'Interpret search pattern as regular expression'
243
+ c.switch %i[e regex], negatable: false
244
+
223
245
  c.desc 'Match pattern exactly'
224
246
  c.switch %i[x exact], negatable: false
225
247
 
226
248
  c.desc 'Recurse to depth'
227
249
  c.arg_name 'DEPTH'
228
- c.default_value 1
229
250
  c.flag %i[d depth], type: :integer, must_match: /^\d+$/
230
251
 
252
+ c.desc 'Show actions from a specific project'
253
+ c.arg_name 'PROJECT[/SUBPROJECT]'
254
+ c.flag %i[proj project]
255
+
256
+ c.desc 'Show actions not matching search pattern'
257
+ c.switch %i[v invert], negatable: false
258
+
231
259
  c.action do |global_options, options, args|
232
260
  depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
233
261
  3
@@ -237,6 +265,8 @@ class App
237
265
  tokens = nil
238
266
  if options[:exact]
239
267
  tokens = args.join(' ')
268
+ elsif options[:regex]
269
+ tokens = Regexp.new(args.join(' '), Regexp::IGNORECASE)
240
270
  else
241
271
  tokens = []
242
272
  args.each do |arg|
@@ -251,15 +281,18 @@ class App
251
281
 
252
282
  files, actions = NA.parse_actions(depth: depth,
253
283
  search: tokens,
284
+ negate: options[:invert],
285
+ regex: options[:regex],
286
+ project: options[:project],
254
287
  require_na: false)
255
288
  NA.output_actions(actions, depth, files: files)
256
289
  end
257
290
  end
258
291
 
259
-
260
292
  desc 'Find actions matching a tag'
261
293
  long_desc 'Finds actions with tags matching the arguments. An action is shown if it
262
- contains any of the tags listed. Add a + before a tag to make it required. You can specify values using TAG=VALUE pairs.'
294
+ contains any of the tags listed. Add a + before a tag to make it required.
295
+ You can specify values using TAG=VALUE pairs.'
263
296
  arg_name 'TAG [VALUE]'
264
297
  command %i[tagged] do |c|
265
298
  c.example 'na tagged +maybe', desc: 'Show all actions tagged @maybe'
@@ -271,6 +304,13 @@ class App
271
304
  c.default_value 1
272
305
  c.flag %i[d depth], type: :integer, must_match: /^\d+$/
273
306
 
307
+ c.desc 'Show actions from a specific project'
308
+ c.arg_name 'PROJECT[/SUBPROJECT]'
309
+ c.flag %i[proj project]
310
+
311
+ c.desc 'Show actions not matching tags'
312
+ c.switch %i[v invert], negatable: false
313
+
274
314
  c.action do |global_options, options, args|
275
315
  depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
276
316
  3
@@ -279,9 +319,9 @@ class App
279
319
  end
280
320
 
281
321
  tags = []
282
- args.each do |arg|
322
+ args.join(',').split(/ *, */).each do |arg|
283
323
  # TODO: <> comparisons do nothing right now
284
- m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>]+)(?:(?<op>[=<>]+)(?<val>\S+))?$/)
324
+ m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>*$\^]+)(?:(?<op>[=<>*$\^]+)(?<val>\S+))?$/)
285
325
  tags.push({
286
326
  tag: m['tag'],
287
327
  comp: m['op'],
@@ -293,6 +333,8 @@ class App
293
333
 
294
334
  files, actions = NA.parse_actions(depth: depth,
295
335
  tag: tags,
336
+ negate: options[:invert],
337
+ project: options[:project],
296
338
  require_na: false)
297
339
  NA.output_actions(actions, depth, files: files)
298
340
  end
@@ -304,7 +346,7 @@ class App
304
346
  c.example 'na init', desc: 'Generate a new todo file, prompting for project name'
305
347
  c.example 'na init warpspeed', desc: 'Generate a new todo for a project called warpspeed'
306
348
 
307
- c.action do |global_options, _, args|
349
+ c.action do |_global_options, _options, args|
308
350
  reader = TTY::Reader.new
309
351
  if args.count.positive?
310
352
  project = args.join(' ')
@@ -346,7 +388,7 @@ class App
346
388
  c.arg_name 'EDITOR'
347
389
  c.flag %i[a app]
348
390
 
349
- c.action do |global_options, options, args|
391
+ c.action do |global_options, options, _args|
350
392
  depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
351
393
  3
352
394
  else
@@ -371,15 +413,16 @@ class App
371
413
  long_desc 'Installing the prompt hook allows you to automatically
372
414
  list next actions when you cd into a directory'
373
415
  command %i[prompt] do |c|
374
- c.desc 'Output the prompt hook for the current shell to STDOUT. Pass an argument to specify a shell (zsh, bash, fish)'
416
+ c.desc 'Output the prompt hook for the current shell to STDOUT. Pass an argument to
417
+ specify a shell (zsh, bash, fish)'
375
418
  c.arg_name '[SHELL]'
376
419
  c.command %i[show] do |s|
377
- s.action do |global_options, options, args|
378
- if args.count.positive?
379
- shell = args[0]
380
- else
381
- shell = File.basename(ENV['SHELL'])
382
- end
420
+ s.action do |_global_options, _options, args|
421
+ shell = if args.count.positive?
422
+ args[0]
423
+ else
424
+ File.basename(ENV['SHELL'])
425
+ end
383
426
 
384
427
  case shell
385
428
  when /^f/i
@@ -395,12 +438,12 @@ class App
395
438
  c.desc 'Install the hook for the current shell to the appropriate startup file.'
396
439
  c.arg_name '[SHELL]'
397
440
  c.command %i[install] do |s|
398
- s.action do |global_options, options, args|
399
- if args.count.positive?
400
- shell = args[0]
401
- else
402
- shell = File.basename(ENV['SHELL'])
403
- end
441
+ s.action do |_global_options, _options, args|
442
+ shell = if args.count.positive?
443
+ args[0]
444
+ else
445
+ File.basename(ENV['SHELL'])
446
+ end
404
447
 
405
448
  case shell
406
449
  when /^f/i
@@ -414,7 +457,7 @@ class App
414
457
  end
415
458
  end
416
459
 
417
- pre do |global, command, options, args|
460
+ pre do |global, _command, _options, _args|
418
461
  NA.verbose = global[:debug]
419
462
  NA.extension = global[:ext]
420
463
  NA.na_tag = global[:na_tag]
@@ -432,9 +475,7 @@ class App
432
475
  case exception
433
476
  when GLI::UnknownCommand
434
477
  cmd = ['add']
435
- if ARGV.count.positive?
436
- cmd.concat(ARGV.unshift($first_arg))
437
- end
478
+ cmd.concat(ARGV.unshift($first_arg)) if ARGV.count.positive?
438
479
 
439
480
  exit run(cmd)
440
481
  when SystemExit
data/lib/na/action.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module NA
4
4
  class Action < Hash
5
- attr_reader :file, :project, :parent, :action
5
+ attr_reader :file, :project, :parent, :action, :tags
6
6
 
7
7
  def initialize(file, project, parent, action)
8
8
  super()
@@ -11,6 +11,7 @@ module NA
11
11
  @project = project
12
12
  @parent = parent
13
13
  @action = action
14
+ @tags = scan_tags
14
15
  end
15
16
 
16
17
  def to_s
@@ -68,5 +69,106 @@ module NA
68
69
  .gsub(/%parents?/, parents)
69
70
  .gsub(/%action/, action))
70
71
  end
72
+
73
+ def tags_match?(any: [], all: [], none: [])
74
+ tag_matches_any(any) && tag_matches_all(all) && tag_matches_none(none)
75
+ end
76
+
77
+ def search_match?(any: [], all: [], none: [])
78
+ search_matches_any(any) && search_matches_all(all) && search_matches_none(none)
79
+ end
80
+
81
+ private
82
+
83
+ def search_matches_none(regexes)
84
+ regexes.each do |rx|
85
+ return false if @action.match(Regexp.new(rx, Regexp::IGNORECASE))
86
+ end
87
+ true
88
+ end
89
+
90
+ def search_matches_any(regexes)
91
+ return true if regexes.empty?
92
+
93
+ regexes.each do |rx|
94
+ return true if @action.match(Regexp.new(rx, Regexp::IGNORECASE))
95
+ end
96
+ false
97
+ end
98
+
99
+ def search_matches_all(regexes)
100
+ regexes.each do |rx|
101
+ return false unless @action.match(Regexp.new(rx, Regexp::IGNORECASE))
102
+ end
103
+ true
104
+ end
105
+
106
+ def tag_matches_none(tags)
107
+ tags.each do |tag|
108
+ return false if compare_tag(tag)
109
+ end
110
+ true
111
+ end
112
+
113
+ def tag_matches_any(tags)
114
+ return true if tags.empty?
115
+
116
+ tags.each do |tag|
117
+ return true if compare_tag(tag)
118
+ end
119
+ false
120
+ end
121
+
122
+ def tag_matches_all(tags)
123
+ tags.each do |tag|
124
+ return false unless compare_tag(tag)
125
+ end
126
+ true
127
+ end
128
+
129
+ def compare_tag(tag)
130
+ return false unless @tags.key?(tag[:tag])
131
+
132
+ return true if tag[:comp].nil?
133
+
134
+ tag_val = @tags[tag[:tag]]
135
+ val = tag[:value]
136
+
137
+ return false if tag_val.nil?
138
+
139
+ return case tag[:comp]
140
+ when /^>$/
141
+ tag_val.to_f > val.to_f
142
+ when /^<$/
143
+ tag_val.to_f < val.to_f
144
+ when /^<=$/
145
+ tag_val.to_f <= val.to_f
146
+ when /^>=$/
147
+ tag_val.to_f >= val.to_f
148
+ when /^==?$/
149
+ tag_val =~ /^#{val.wildcard_to_rx}$/
150
+ when /^\$=$/
151
+ tag_val =~ /#{val.wildcard_to_rx}$/i
152
+ when /^\*=$/
153
+ tag_val =~ /#{val.wildcard_to_rx}/i
154
+ when /^\^=$/
155
+ tag_val =~ /^#{val.wildcard_to_rx}/
156
+ else
157
+ false
158
+ end
159
+ end
160
+
161
+ def scan_tags
162
+ tags = {}
163
+ rx = /(?<= |^)@(?<tag>\S+?)(?:\((?<value>.*?)\))?(?= |$)/
164
+ all_tags = []
165
+ @action.scan(rx) { all_tags << Regexp.last_match }
166
+ all_tags.each do |m|
167
+ tag = m.named_captures.symbolize_keys
168
+ tags[tag[:tag]] = tag[:value]
169
+ end
170
+
171
+ tags
172
+ end
71
173
  end
72
174
  end
data/lib/na/hash.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ::Hash
4
+ def symbolize_keys
5
+ each_with_object({}) { |(k, v), hsh| hsh[k.to_sym] = v.is_a?(Hash) ? v.symbolize_keys : v }
6
+ end
7
+ end
@@ -5,10 +5,17 @@ module NA
5
5
  class << self
6
6
  attr_accessor :verbose, :extension, :na_tag
7
7
 
8
+ def notify(msg, exit_code: false)
9
+ $stderr.puts NA::Color.template("{x}#{msg}{x}")
10
+ if exit_code && exit_code.is_a?(Number)
11
+ Process.exit exit_code
12
+ end
13
+ end
14
+
8
15
  def create_todo(target, basename)
9
16
  File.open(target, 'w') do |f|
10
17
  content = <<~ENDCONTENT
11
- Inbox: @inbox
18
+ Inbox:
12
19
  #{basename}:
13
20
  \tFeature Requests:
14
21
  \tIdeas:
@@ -18,11 +25,11 @@ module NA
18
25
  \tTop Priority @search(@priority = 5 and not @done)
19
26
  \tHigh Priority @search(@priority > 3 and not @done)
20
27
  \tMaybe @search(@maybe)
21
- \tNext @search(@na and not @done and not project = \"Archive\")
28
+ \tNext @search(@#{NA.na_tag} and not @done and not project = \"Archive\")
22
29
  ENDCONTENT
23
30
  f.puts(content)
24
31
  end
25
- $stderr.puts NA::Color.template("{y}Created {bw}#{target}{x}")
32
+ notify("{y}Created {bw}#{target}")
26
33
  end
27
34
 
28
35
  def find_files(depth: 1)
@@ -41,8 +48,7 @@ module NA
41
48
  elsif TTY::Which.exist?('fzf')
42
49
  res = choose_from(files, prompt: 'Use which file?')
43
50
  unless res
44
- $stderr.puts 'No file selected, cancelled'
45
- Process.exit 1
51
+ notify('{r}No file selected, cancelled', exit_code: 1)
46
52
  end
47
53
 
48
54
  res.strip
@@ -71,7 +77,7 @@ module NA
71
77
 
72
78
  File.open(file, 'w') { |f| f.puts content }
73
79
 
74
- $stderr.puts NA::Color.template("{by}Task added to {bw}#{file}{x}")
80
+ notify("{by}Task added to {bw}#{file}")
75
81
  end
76
82
 
77
83
  def output_actions(actions, depth, files: nil)
@@ -91,40 +97,56 @@ module NA
91
97
  '%parent%action'
92
98
  end
93
99
  if files && @verbose
94
- $stderr.puts files.map { |f| NA::Color.template("{dw}#{f}{x}") }
100
+ files.map { |f| notify("{dw}#{f}") }
95
101
  end
96
102
 
97
103
  puts actions.map { |action| action.pretty(template: { output: template }) }
98
104
  end
99
105
 
100
- def parse_actions(depth: 1, query: nil, tag: nil, search: nil, require_na: true)
106
+ def parse_actions(depth: 1, query: nil, tag: nil, search: nil, negate: false, regex: false, project: nil, require_na: true)
101
107
  actions = []
102
108
  required = []
103
109
  optional = []
104
110
  negated = []
111
+ required_tag = []
112
+ optional_tag = []
113
+ negated_tag = []
105
114
 
106
115
  tag&.each do |t|
107
116
  unless t[:tag].nil?
108
- new_rx = " @#{t[:tag]}\\b"
109
- new_rx = "#{new_rx}\\(#{t[:value]}\\)" if t[:value]
110
-
111
- optional.push(new_rx) unless t[:negate]
112
- required.push(new_rx) if t[:required] && !t[:negate]
113
- negated.push(new_rx) if t[:negate]
117
+ if negate
118
+ optional_tag.push(t) if t[:negate]
119
+ required_tag.push(t) if t[:required] && t[:negate]
120
+ negated_tag.push(t) unless t[:negate]
121
+ else
122
+ optional_tag.push(t) unless t[:negate]
123
+ required_tag.push(t) if t[:required] && !t[:negate]
124
+ negated_tag.push(t) if t[:negate]
125
+ end
114
126
  end
115
127
  end
116
128
 
117
129
  unless search.nil?
118
- if search.is_a?(String)
119
- optional.push(search)
120
- required.push(search)
130
+ if regex || search.is_a?(String)
131
+ if negate
132
+ negated.push(search)
133
+ else
134
+ optional.push(search)
135
+ required.push(search)
136
+ end
121
137
  else
122
138
  search.each do |t|
123
- new_rx = t[:token].to_s
124
-
125
- optional.push(new_rx) unless t[:negate]
126
- required.push(new_rx) if t[:required] && !t[:negate]
127
- negated.push(new_rx) if t[:negate]
139
+ new_rx = t[:token].to_s.wildcard_to_rx
140
+
141
+ if negate
142
+ optional.push(new_rx) if t[:negate]
143
+ required.push(new_rx) if t[:required] && t[:negate]
144
+ negated.push(new_rx) unless t[:negate]
145
+ else
146
+ optional.push(new_rx) unless t[:negate]
147
+ required.push(new_rx) if t[:required] && !t[:negate]
148
+ negated.push(new_rx) if t[:negate]
149
+ end
128
150
  end
129
151
  end
130
152
  end
@@ -160,13 +182,24 @@ module NA
160
182
  elsif line =~ /^[ \t]*- / && line !~ / @done/
161
183
  next if require_na && line !~ /@#{NA.na_tag}\b/
162
184
 
163
- unless optional.empty? && required.empty? && negated.empty?
164
- next unless line.matches(any: optional, all: required, none: negated)
185
+ action = line.sub(/^[ \t]*- /, '').sub(/ @#{NA.na_tag}\b/, '')
186
+ new_action = NA::Action.new(file, File.basename(file, ".#{NA.extension}"), parent.dup, action)
187
+
188
+ has_search = !optional.empty? || !required.empty? || !negated.empty?
189
+ next if has_search && !new_action.search_match?(any: optional,
190
+ all: required,
191
+ none: negated)
165
192
 
193
+ if project
194
+ rx = project.split(%r{[/:]}).join('.*?/.*?')
195
+ next unless parent.join('/') =~ Regexp.new(rx, Regexp::IGNORECASE)
166
196
  end
167
197
 
168
- action = line.sub(/^[ \t]*- /, '').sub(/ @#{NA.na_tag}\b/, '')
169
- new_action = NA::Action.new(file, File.basename(file, ".#{NA.extension}"), parent.dup, action)
198
+ has_tag = !optional_tag.empty? || !required_tag.empty? || !negated_tag.empty?
199
+ next if has_tag && !new_action.tags_match?(any: optional_tag,
200
+ all: required_tag,
201
+ none: negated_tag)
202
+
170
203
  actions.push(new_action)
171
204
  end
172
205
  end
@@ -188,11 +221,7 @@ module NA
188
221
  def choose_from(options, prompt: 'Make a selection: ', multiple: false, sorted: true, fzf_args: [])
189
222
  return nil unless $stdout.isatty
190
223
 
191
- # fzf_args << '-1' # User is expecting a menu, and even if only one it seves as confirmation
192
- default_args = []
193
- default_args << %(--prompt="#{prompt}")
194
- default_args << "--height=#{options.count + 2}"
195
- default_args << '--info=inline'
224
+ default_args = [%(--prompt="#{prompt}"), "--height=#{options.count + 2}", '--info=inline']
196
225
  default_args << '--multi' if multiple
197
226
  header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
198
227
  default_args << %(--header="#{header}")
@@ -205,33 +234,50 @@ module NA
205
234
  res
206
235
  end
207
236
 
237
+ ##
238
+ ## Get path to database of known todo files
239
+ ##
240
+ ## @return [String] File path
241
+ ##
208
242
  def database_path
209
243
  db_dir = File.expand_path('~/.local/share/na')
244
+ # Create directory if needed
210
245
  FileUtils.mkdir_p(db_dir) unless File.directory?(db_dir)
211
246
  db_file = 'tdlist.txt'
212
247
  File.join(db_dir, db_file)
213
248
  end
214
249
 
215
- def match_working_dir(search)
250
+ ##
251
+ ## Find a matching path using semi-fuzzy matching.
252
+ ## Search tokens can include ! and + to negate or make
253
+ ## required.
254
+ ##
255
+ ## @param search [Array] search tokens to match
256
+ ## @param distance [Integer] allowed distance
257
+ ## between characters
258
+ ##
259
+ def match_working_dir(search, distance: 1)
216
260
  optional = []
217
261
  required = []
218
262
 
219
263
  search&.each do |t|
220
- new_rx = t[:token].to_s.split('').join('.{0,1}')
264
+ # Make "search" into "s.{0,1}e.{0,1}a.{0,1}r.{0,1}c.{0,1}h"
265
+ new_rx = t[:token].to_s.split('').join(".{0,#{distance}}")
221
266
 
222
267
  optional.push(new_rx)
223
268
  required.push(new_rx) if t[:required]
224
269
  end
225
270
 
271
+ match_dir(optional, required)
272
+ end
273
+
274
+ def match_dir(optional, required)
226
275
  file = database_path
227
- if File.exist?(file)
228
- dirs = IO.read(file).split("\n")
229
- dirs.delete_if { |d| !d.matches(any: optional, all: required) }
230
- dirs.sort.uniq
231
- else
232
- $stderr.puts NA::Color.template('{r}No na database found{x}')
233
- Process.exit 1
234
- end
276
+ notify('{r}No na database found', exit_code: 1) unless File.exist?(file)
277
+
278
+ dirs = IO.read(file).split("\n")
279
+ dirs.delete_if { |d| !d.matches(any: optional, all: required) }
280
+ dirs.sort.uniq
235
281
  end
236
282
 
237
283
  def save_working_dir(todo_file)
@@ -258,6 +304,26 @@ module NA
258
304
  os_open(file, app: app) if file && File.exist?(file)
259
305
  end
260
306
 
307
+ def darwin_open(file, app: nil)
308
+ if app
309
+ `open -a "#{app}" #{Shellwords.escape(file)}`
310
+ else
311
+ `open #{Shellwords.escape(file)}`
312
+ end
313
+ end
314
+
315
+ def win_open(file)
316
+ `start #{Shellwords.escape(file)}`
317
+ end
318
+
319
+ def linux_open(file)
320
+ if TTY::Which.exist?('xdg-open')
321
+ `xdg-open #{Shellwords.escape(file)}`
322
+ else
323
+ notify('{r}Unable to determine executable for `xdg-open`.')
324
+ end
325
+ end
326
+
261
327
  ##
262
328
  ## Platform-agnostic open command
263
329
  ##
@@ -267,19 +333,11 @@ module NA
267
333
  os = RbConfig::CONFIG['target_os']
268
334
  case os
269
335
  when /darwin.*/i
270
- if app
271
- `open -a "#{app}" #{Shellwords.escape(file)}`
272
- else
273
- `open #{Shellwords.escape(file)}`
274
- end
336
+ darwin_open(file, app: app)
275
337
  when /mingw|mswin/i
276
- `start #{Shellwords.escape(file)}`
338
+ win_open(file)
277
339
  else
278
- if 'xdg-open'.available?
279
- `xdg-open #{Shellwords.escape(file)}`
280
- else
281
- $stderr.puts NA::Color.template('{r}Unable to determine executable for `open`.{x}')
282
- end
340
+ linux_open(file)
283
341
  end
284
342
  end
285
343
  end
data/lib/na/string.rb CHANGED
@@ -49,6 +49,10 @@ class ::String
49
49
  true
50
50
  end
51
51
 
52
+ def wildcard_to_rx
53
+ gsub(/\./, '\\.').gsub(/\*/, '.*?').gsub(/\?/, '.')
54
+ end
55
+
52
56
  def cap_first!
53
57
  replace cap_first
54
58
  end
data/lib/na/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Na
2
- VERSION = '1.1.10'
2
+ VERSION = '1.1.12'
3
3
  end
data/lib/na.rb CHANGED
@@ -1,14 +1,14 @@
1
- require 'na/version.rb'
1
+ # frozen_string_literal: true
2
2
 
3
- # Add requires for other files you add to your project here, so
4
- # you just need to require this one file in your bin file
3
+ require 'na/version'
5
4
  require 'fileutils'
6
5
  require 'shellwords'
7
6
  require 'tty-screen'
8
7
  require 'tty-reader'
9
8
  require 'tty-which'
10
- require 'na/colors.rb'
11
- require 'na/string.rb'
12
- require 'na/action.rb'
13
- require 'na/next_action.rb'
14
- require 'na/prompt.rb'
9
+ require 'na/hash'
10
+ require 'na/colors'
11
+ require 'na/string'
12
+ require 'na/action'
13
+ require 'na/next_action'
14
+ require 'na/prompt'
data/src/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  _If you're one of the rare people like me who find this useful, feel free to
10
10
  [buy me some coffee][donate]._
11
11
 
12
- The current version of `na` is <!--VER-->1.1.9<!--END VER-->.
12
+ The current version of `na` is <!--VER-->1.1.11<!--END VER-->.
13
13
 
14
14
  `na` ("next action") is a command line tool designed to make it easy to see what your next actions are for any project, right from the command line. It works with TaskPaper-formatted files (but any plain text format will do), looking for `@na` tags (or whatever you specify) in todo files in your current folder.
15
15
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: na
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.10
4
+ version: 1.1.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-05 00:00:00.000000000 Z
11
+ date: 2022-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -169,6 +169,7 @@ files:
169
169
  - lib/na.rb
170
170
  - lib/na/action.rb
171
171
  - lib/na/colors.rb
172
+ - lib/na/hash.rb
172
173
  - lib/na/next_action.rb
173
174
  - lib/na/prompt.rb
174
175
  - lib/na/string.rb