na 1.1.26 → 1.2.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -0
- data/Gemfile.lock +3 -1
- data/README.md +161 -23
- data/bin/na +364 -43
- data/lib/na/action.rb +11 -13
- data/lib/na/colors.rb +2 -1
- data/lib/na/next_action.rb +347 -49
- data/lib/na/project.rb +26 -0
- data/lib/na/string.rb +9 -5
- data/lib/na/version.rb +1 -1
- data/lib/na.rb +1 -0
- data/na.gemspec +1 -0
- data/src/README.md +58 -4
- metadata +23 -2
data/bin/na
CHANGED
@@ -78,6 +78,9 @@ class App
|
|
78
78
|
c.arg_name 'PROJECT[/SUBPROJECT]'
|
79
79
|
c.flag %i[proj project]
|
80
80
|
|
81
|
+
c.desc 'Include @done actions'
|
82
|
+
c.switch %i[done]
|
83
|
+
|
81
84
|
c.action do |global_options, options, args|
|
82
85
|
if global_options[:add]
|
83
86
|
cmd = ['add']
|
@@ -95,13 +98,16 @@ class App
|
|
95
98
|
end
|
96
99
|
|
97
100
|
if args.count.positive?
|
101
|
+
all_req = false
|
102
|
+
|
98
103
|
tokens = []
|
99
104
|
args.each do |arg|
|
100
105
|
arg.split(/ *, */).each do |a|
|
101
|
-
m = a.match(/^(?<req
|
106
|
+
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
102
107
|
tokens.push({
|
103
108
|
token: m['tok'],
|
104
|
-
required: !m['req'].nil?
|
109
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
110
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
105
111
|
})
|
106
112
|
end
|
107
113
|
end
|
@@ -110,12 +116,13 @@ class App
|
|
110
116
|
NA.na_tag = options[:tag] unless options[:tag].nil?
|
111
117
|
require_na = true
|
112
118
|
|
113
|
-
tag = [{ tag: tag, value: nil }]
|
114
|
-
files, actions = NA.parse_actions(depth: depth,
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
+
tag = [{ tag: tag, value: nil }, { tag: 'done', value: nil, negate: true}]
|
120
|
+
files, actions, = NA.parse_actions(depth: depth,
|
121
|
+
done: options[:done],
|
122
|
+
query: tokens,
|
123
|
+
tag: tag,
|
124
|
+
project: options[:project],
|
125
|
+
require_na: require_na)
|
119
126
|
|
120
127
|
NA.output_actions(actions, depth, files: files)
|
121
128
|
end
|
@@ -146,7 +153,11 @@ class App
|
|
146
153
|
c.desc 'Add action to specific project'
|
147
154
|
c.arg_name 'PROJECT'
|
148
155
|
c.default_value 'Inbox'
|
149
|
-
c.flag %i[to]
|
156
|
+
c.flag %i[to project proj]
|
157
|
+
|
158
|
+
c.desc 'Add to a known todo file, partial matches allowed'
|
159
|
+
c.arg_name 'TODO_FILE'
|
160
|
+
c.flag %i[in todo]
|
150
161
|
|
151
162
|
c.desc 'Use a tag other than the default next action tag'
|
152
163
|
c.arg_name 'TAG'
|
@@ -183,7 +194,7 @@ class App
|
|
183
194
|
action = "#{action.gsub(/@priority\(\d+\)/, '')} @priority(#{options[:priority]})"
|
184
195
|
end
|
185
196
|
|
186
|
-
note_rx = /^(.+)\((.*?)\)$/
|
197
|
+
note_rx = /^(.+) \((.*?)\)$/
|
187
198
|
split_note = if action =~ note_rx
|
188
199
|
n = Regexp.last_match(2)
|
189
200
|
action.sub!(note_rx, '\1').strip!
|
@@ -229,6 +240,34 @@ class App
|
|
229
240
|
Process.exit 1
|
230
241
|
end
|
231
242
|
end
|
243
|
+
elsif options[:todo]
|
244
|
+
todo = []
|
245
|
+
options[:todo].split(/ *, */).each do |a|
|
246
|
+
m = a.match(/^(?<req>\+)?(?<tok>.*?)$/)
|
247
|
+
todo.push({
|
248
|
+
token: m['tok'],
|
249
|
+
required: !m['req'].nil?
|
250
|
+
})
|
251
|
+
end
|
252
|
+
dirs = NA.match_working_dir(todo)
|
253
|
+
if dirs.count.positive?
|
254
|
+
target = dirs[0]
|
255
|
+
else
|
256
|
+
todo = "#{options[:todo].sub(/#{NA.extension}$/, '')}.#{NA.extension}"
|
257
|
+
target = File.expand_path(todo)
|
258
|
+
unless File.exist?(target)
|
259
|
+
|
260
|
+
res = NA.yn(NA::Color.template("{by}Specified file not found, create #{todo}"), default: true)
|
261
|
+
if res
|
262
|
+
basename = File.basename(target, ".#{NA.extension}")
|
263
|
+
NA.create_todo(target, basename)
|
264
|
+
else
|
265
|
+
NA.notify('{r}Cancelled{x}', exit_code: 1)
|
266
|
+
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
end
|
232
271
|
else
|
233
272
|
files = NA.find_files(depth: options[:depth])
|
234
273
|
if files.count.zero?
|
@@ -243,12 +282,209 @@ class App
|
|
243
282
|
end
|
244
283
|
target = files.count > 1 ? NA.select_file(files) : files[0]
|
245
284
|
unless files.count.positive? && File.exist?(target)
|
246
|
-
|
247
|
-
|
285
|
+
NA.notify('{r}Cancelled{x}', exit_code: 1)
|
286
|
+
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
NA.add_action(target, options[:project], action, note)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
desc 'Update an existing action'
|
295
|
+
long_desc 'Provides an easy way to complete, prioritize, and tag existing actions.
|
296
|
+
|
297
|
+
If multiple todo files are found in the current directory, a menu will
|
298
|
+
allow you to pick which file to act on.'
|
299
|
+
arg_name 'ACTION'
|
300
|
+
command %i[update] do |c|
|
301
|
+
c.example 'na update --remove na "An existing task"', desc: 'Find "An existing task" action and remove the @na tag from it'
|
302
|
+
c.example 'na update --tag waiting "A bug I need to fix" -p 4 -n',
|
303
|
+
desc: 'Find "A bug..." action, add @waiting, add/update @priority(4), and prompt for an additional note'
|
304
|
+
c.example 'na update --archive My cool action', desc: 'Add @done to "My cool action" and immediately move to Archive'
|
305
|
+
|
306
|
+
c.desc 'Prompt for additional notes. Input will be appended to any existing note.'
|
307
|
+
c.switch %i[n note], negatable: false
|
308
|
+
|
309
|
+
c.desc 'Overwrite note instead of appending'
|
310
|
+
c.switch %i[o overwrite], negatable: false
|
311
|
+
|
312
|
+
c.desc 'Add/change a priority level 1-5'
|
313
|
+
c.arg_name 'PRIO'
|
314
|
+
c.flag %i[p priority], must_match: /[1-5]/, type: :integer, default_value: 0
|
315
|
+
|
316
|
+
c.desc 'Move action to specific project'
|
317
|
+
c.arg_name 'PROJECT'
|
318
|
+
c.flag %i[to project proj]
|
319
|
+
|
320
|
+
c.desc 'Use a known todo file, partial matches allowed'
|
321
|
+
c.arg_name 'TODO_FILE'
|
322
|
+
c.flag %i[in todo]
|
323
|
+
|
324
|
+
c.desc 'Add a tag to the action, @tag(values) allowed'
|
325
|
+
c.arg_name 'TAG'
|
326
|
+
c.flag %i[t tag], multiple: true
|
327
|
+
|
328
|
+
c.desc 'Remove a tag to the action'
|
329
|
+
c.arg_name 'TAG'
|
330
|
+
c.flag %i[r remove], multiple: true
|
331
|
+
|
332
|
+
c.desc 'Add a @done tag to action'
|
333
|
+
c.switch %i[f finish done], negatable: false
|
334
|
+
|
335
|
+
c.desc 'Add a @done tag to action and move to Archive'
|
336
|
+
c.switch %i[a archive], negatable: false
|
337
|
+
|
338
|
+
c.desc 'Delete an action'
|
339
|
+
c.switch %i[delete], negatable: false
|
340
|
+
|
341
|
+
c.desc 'Specify the file to search for the task'
|
342
|
+
c.arg_name 'PATH'
|
343
|
+
c.flag %i[file]
|
344
|
+
|
345
|
+
c.desc 'Search for files X directories deep'
|
346
|
+
c.arg_name 'DEPTH'
|
347
|
+
c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
|
348
|
+
|
349
|
+
c.desc 'Match actions containing tag. Allows value comparisons'
|
350
|
+
c.arg_name 'TAG'
|
351
|
+
c.flag %i[tagged], multiple: true
|
352
|
+
|
353
|
+
c.desc 'Act on all matches immediately (no menu)'
|
354
|
+
c.switch %i[all], negatable: false
|
355
|
+
|
356
|
+
c.desc 'Interpret search pattern as regular expression'
|
357
|
+
c.switch %i[e regex], negatable: false
|
358
|
+
|
359
|
+
c.desc 'Match pattern exactly'
|
360
|
+
c.switch %i[x exact], negatable: false
|
361
|
+
|
362
|
+
c.action do |_global_options, options, args|
|
363
|
+
reader = TTY::Reader.new
|
364
|
+
action = if args.count.positive?
|
365
|
+
args.join(' ').strip
|
366
|
+
elsif TTY::Which.exist?('gum') && options[:tagged].empty?
|
367
|
+
options = [
|
368
|
+
%(--placeholder "Enter a task to search for"),
|
369
|
+
'--char-limit=500',
|
370
|
+
"--width=#{TTY::Screen.columns}"
|
371
|
+
]
|
372
|
+
`gum input #{options.join(' ')}`.strip
|
373
|
+
elsif options[:tagged].empty?
|
374
|
+
puts NA::Color.template('{bm}Enter search string:{x}')
|
375
|
+
reader.read_line(NA::Color.template('{by}> {bw}')).strip
|
376
|
+
end
|
377
|
+
|
378
|
+
if action
|
379
|
+
tokens = nil
|
380
|
+
if options[:exact]
|
381
|
+
tokens = action
|
382
|
+
elsif options[:regex]
|
383
|
+
tokens = Regexp.new(action, Regexp::IGNORECASE)
|
384
|
+
else
|
385
|
+
tokens = []
|
386
|
+
all_req = action !~ /[+!\-]/ && !options[:or]
|
387
|
+
|
388
|
+
action.split(/ /).each do |arg|
|
389
|
+
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
390
|
+
tokens.push({
|
391
|
+
token: m['tok'],
|
392
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
393
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
394
|
+
})
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
if (action.nil? || action.empty?) && options[:tagged].empty?
|
400
|
+
puts 'Empty input, cancelled'
|
401
|
+
Process.exit 1
|
402
|
+
end
|
403
|
+
|
404
|
+
all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
|
405
|
+
tags = []
|
406
|
+
options[:tagged].join(',').split(/ *, */).each do |arg|
|
407
|
+
m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
|
408
|
+
|
409
|
+
tags.push({
|
410
|
+
tag: m['tag'].wildcard_to_rx,
|
411
|
+
comp: m['op'],
|
412
|
+
value: m['val'],
|
413
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
414
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
415
|
+
})
|
416
|
+
end
|
417
|
+
|
418
|
+
priority = options[:priority].to_i if options[:priority]&.to_i&.positive?
|
419
|
+
add_tags = options[:tag].map { |t| t.sub(/^@/, '').wildcard_to_rx }
|
420
|
+
remove_tags = options[:remove].map { |t| t.sub(/^@/, '').wildcard_to_rx }
|
421
|
+
|
422
|
+
line_note = if options[:note]
|
423
|
+
if TTY::Which.exist?('gum')
|
424
|
+
args = ['--placeholder "Enter a note, CTRL-d to save"']
|
425
|
+
args << '--char-limit 0'
|
426
|
+
args << '--width $(tput cols)'
|
427
|
+
`gum write #{args.join(' ')}`.strip.split("\n")
|
428
|
+
else
|
429
|
+
puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
|
430
|
+
reader.read_multiline
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
note = line_note.nil? || line_note.empty? ? [] : line_note
|
435
|
+
|
436
|
+
if options[:file]
|
437
|
+
file = File.expand_path(options[:file])
|
438
|
+
NA.notify('{r}File not found', exit_code: 1) unless File.exist?(file)
|
439
|
+
|
440
|
+
targets = [file]
|
441
|
+
elsif options[:todo]
|
442
|
+
todo = []
|
443
|
+
options[:todo].split(/ *, */).each do |a|
|
444
|
+
m = a.match(/^(?<req>\+)?(?<tok>.*?)$/)
|
445
|
+
todo.push({
|
446
|
+
token: m['tok'],
|
447
|
+
required: !m['req'].nil?
|
448
|
+
})
|
449
|
+
end
|
450
|
+
dirs = NA.match_working_dir(todo)
|
451
|
+
|
452
|
+
if dirs.count == 1
|
453
|
+
targets = [dirs[0]]
|
454
|
+
elsif dirs.count.positive?
|
455
|
+
targets = NA.select_file(dirs, multiple: true)
|
456
|
+
NA.notify('{r}Cancelled', exit_code: 1) unless targets && targets.count.positive?
|
457
|
+
else
|
458
|
+
NA.notify('{r}Todo not found', exit_code: 1) unless targets && targets.count.positive?
|
459
|
+
|
248
460
|
end
|
461
|
+
else
|
462
|
+
files = NA.find_files(depth: options[:depth])
|
463
|
+
NA.notify('{r}No todo file found', exit_code: 1) if files.count.zero?
|
464
|
+
|
465
|
+
targets = files.count > 1 ? NA.select_file(files, multiple: true) : [files[0]]
|
466
|
+
NA.notify('{r}Cancelled{x}', exit_code: 1) unless files.count.positive?
|
467
|
+
|
249
468
|
end
|
250
469
|
|
251
|
-
|
470
|
+
options[:finish] = true if options[:archive]
|
471
|
+
options[:project] = 'Archive' if options[:archive]
|
472
|
+
|
473
|
+
NA.notify('{r}No search terms provided', exit_code: 1) if tokens.nil? && options[:tagged].empty?
|
474
|
+
|
475
|
+
targets.each do |target|
|
476
|
+
NA.update_action(target, tokens,
|
477
|
+
priority: priority,
|
478
|
+
add_tag: add_tags,
|
479
|
+
remove_tag: remove_tags,
|
480
|
+
finish: options[:finish],
|
481
|
+
project: options[:project],
|
482
|
+
delete: options[:delete],
|
483
|
+
note: note,
|
484
|
+
overwrite: options[:overwrite],
|
485
|
+
tagged: tags,
|
486
|
+
all: options[:all])
|
487
|
+
end
|
252
488
|
end
|
253
489
|
end
|
254
490
|
|
@@ -281,6 +517,9 @@ class App
|
|
281
517
|
c.arg_name 'PROJECT[/SUBPROJECT]'
|
282
518
|
c.flag %i[proj project]
|
283
519
|
|
520
|
+
c.desc 'Include @done actions'
|
521
|
+
c.switch %i[done]
|
522
|
+
|
284
523
|
c.desc 'Show actions not matching search pattern'
|
285
524
|
c.switch %i[v invert], negatable: false
|
286
525
|
|
@@ -291,7 +530,7 @@ class App
|
|
291
530
|
c.action do |global_options, options, args|
|
292
531
|
if options[:save]
|
293
532
|
title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
|
294
|
-
NA.save_search(title, "find #{NA.command_line.map { |
|
533
|
+
NA.save_search(title, "find #{NA.command_line.map { |cmd| "\"#{cmd}\"" }.join(' ')}")
|
295
534
|
end
|
296
535
|
|
297
536
|
depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
|
@@ -324,24 +563,25 @@ class App
|
|
324
563
|
options[:in].split(/ *, */).each do |a|
|
325
564
|
m = a.match(/^(?<req>\+)?(?<tok>.*?)$/)
|
326
565
|
todo.push({
|
327
|
-
|
328
|
-
|
329
|
-
|
566
|
+
token: m['tok'],
|
567
|
+
required: !m['req'].nil?
|
568
|
+
})
|
330
569
|
end
|
331
570
|
end
|
332
571
|
|
333
|
-
files, actions = NA.parse_actions(depth: depth,
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
572
|
+
files, actions, = NA.parse_actions(depth: depth,
|
573
|
+
done: options[:done],
|
574
|
+
query: todo,
|
575
|
+
search: tokens,
|
576
|
+
negate: options[:invert],
|
577
|
+
regex: options[:regex],
|
578
|
+
project: options[:project],
|
579
|
+
require_na: false)
|
580
|
+
regexes = if tokens.is_a?(Array)
|
581
|
+
tokens.delete_if { |token| token[:negate] }.map { |token| token[:token] }
|
582
|
+
else
|
583
|
+
[tokens]
|
584
|
+
end
|
345
585
|
|
346
586
|
NA.output_actions(actions, depth, files: files, regexes: regexes)
|
347
587
|
end
|
@@ -378,6 +618,9 @@ class App
|
|
378
618
|
c.arg_name 'PROJECT[/SUBPROJECT]'
|
379
619
|
c.flag %i[proj project]
|
380
620
|
|
621
|
+
c.desc 'Include @done actions'
|
622
|
+
c.switch %i[done]
|
623
|
+
|
381
624
|
c.desc 'Show actions not matching tags'
|
382
625
|
c.switch %i[v invert], negatable: false
|
383
626
|
|
@@ -401,7 +644,6 @@ class App
|
|
401
644
|
|
402
645
|
all_req = args.join(' ') !~ /[+!\-]/ && !options[:or]
|
403
646
|
args.join(',').split(/ *, */).each do |arg|
|
404
|
-
# TODO: <> comparisons do nothing right now
|
405
647
|
m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
|
406
648
|
|
407
649
|
tags.push({
|
@@ -413,6 +655,10 @@ class App
|
|
413
655
|
})
|
414
656
|
end
|
415
657
|
|
658
|
+
search_for_done = false
|
659
|
+
tags.each { |tag| search_for_done = true if tag[:tag] =~ /done/ }
|
660
|
+
tags.push({ tag: 'done', value: nil, negate: true}) unless search_for_done
|
661
|
+
|
416
662
|
todo = nil
|
417
663
|
if options[:in]
|
418
664
|
todo = []
|
@@ -425,12 +671,13 @@ class App
|
|
425
671
|
end
|
426
672
|
end
|
427
673
|
|
428
|
-
files, actions = NA.parse_actions(depth: depth,
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
674
|
+
files, actions, = NA.parse_actions(depth: depth,
|
675
|
+
done: options[:done],
|
676
|
+
query: todo,
|
677
|
+
tag: tags,
|
678
|
+
negate: options[:invert],
|
679
|
+
project: options[:project],
|
680
|
+
require_na: false)
|
434
681
|
regexes = tags.delete_if { |token| token[:negate] }.map { |token| token[:token] }
|
435
682
|
NA.output_actions(actions, depth, files: files, regexes: regexes)
|
436
683
|
end
|
@@ -484,13 +731,15 @@ class App
|
|
484
731
|
c.arg_name 'EDITOR'
|
485
732
|
c.flag %i[a app]
|
486
733
|
|
487
|
-
c.action do |global_options, options,
|
734
|
+
c.action do |global_options, options, args|
|
488
735
|
depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
|
489
736
|
3
|
490
737
|
else
|
491
738
|
options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
|
492
739
|
end
|
493
740
|
files = NA.find_files(depth: depth)
|
741
|
+
files.delete_if { |f| f !~ /.*?(#{args.join('|')}).*?.#{NA.extension}/ } if args.count.positive?
|
742
|
+
|
494
743
|
file = if files.count > 1
|
495
744
|
NA.select_file(files)
|
496
745
|
else
|
@@ -513,13 +762,16 @@ class App
|
|
513
762
|
command %i[todos] do |c|
|
514
763
|
c.action do |_global_options, _options, args|
|
515
764
|
if args.count.positive?
|
516
|
-
|
765
|
+
all_req = args.join(' ') !~ /[+!\-]/
|
766
|
+
|
767
|
+
tokens = [{ token: '*', required: all_req, negate: false }]
|
517
768
|
args.each do |arg|
|
518
769
|
arg.split(/ *, */).each do |a|
|
519
|
-
m = a.match(/^(?<req
|
770
|
+
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
520
771
|
tokens.push({
|
521
772
|
token: m['tok'],
|
522
|
-
required: !m['req'].nil?
|
773
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
774
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
523
775
|
})
|
524
776
|
end
|
525
777
|
end
|
@@ -529,6 +781,39 @@ class App
|
|
529
781
|
end
|
530
782
|
end
|
531
783
|
|
784
|
+
desc 'Show list of projects for a file'
|
785
|
+
long_desc 'Arguments will be interpreted as a query for a known todo file,
|
786
|
+
fuzzy matched. Separate directories with /, :, or a space, e.g. `na projects code/marked`'
|
787
|
+
arg_name 'QUERY', optional: true
|
788
|
+
command %i[projects] do |c|
|
789
|
+
c.desc 'Search for files X directories deep'
|
790
|
+
c.arg_name 'DEPTH'
|
791
|
+
c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
|
792
|
+
|
793
|
+
c.desc 'Output projects as paths instead of hierarchy'
|
794
|
+
c.switch %i[p paths], negatable: false
|
795
|
+
|
796
|
+
c.action do |_global_options, options, args|
|
797
|
+
if args.count.positive?
|
798
|
+
all_req = args.join(' ') !~ /[+!\-]/
|
799
|
+
|
800
|
+
tokens = [{ token: '*', required: all_req, negate: false }]
|
801
|
+
args.each do |arg|
|
802
|
+
arg.split(/ *, */).each do |a|
|
803
|
+
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
804
|
+
tokens.push({
|
805
|
+
token: m['tok'],
|
806
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
807
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
808
|
+
})
|
809
|
+
end
|
810
|
+
end
|
811
|
+
end
|
812
|
+
|
813
|
+
NA.list_projects(query: tokens, depth: options[:depth], paths: options[:paths])
|
814
|
+
end
|
815
|
+
end
|
816
|
+
|
532
817
|
desc 'Show or install prompt hooks for the current shell'
|
533
818
|
long_desc 'Installing the prompt hook allows you to automatically
|
534
819
|
list next actions when you cd into a directory'
|
@@ -577,16 +862,52 @@ class App
|
|
577
862
|
end
|
578
863
|
end
|
579
864
|
|
865
|
+
desc 'Display the changelog'
|
866
|
+
command %i[changes changelog] do |c|
|
867
|
+
c.action do |_, _, _|
|
868
|
+
changelog = File.expand_path(File.join(File.dirname(__FILE__), '..', 'CHANGELOG.md'))
|
869
|
+
pagers = [
|
870
|
+
'mdless',
|
871
|
+
'mdcat',
|
872
|
+
'bat',
|
873
|
+
ENV['PAGER'],
|
874
|
+
'less -FXr',
|
875
|
+
ENV['GIT_PAGER'],
|
876
|
+
'more -r'
|
877
|
+
]
|
878
|
+
pager = pagers.find { |cmd| TTY::Which.exist?(cmd.split.first) }
|
879
|
+
system %(#{pager} "#{changelog}")
|
880
|
+
end
|
881
|
+
end
|
882
|
+
|
580
883
|
desc 'Execute a saved search'
|
581
884
|
long_desc 'Run without argument to list saved searches'
|
582
885
|
arg_name 'SEARCH_TITLE', optional: true
|
583
886
|
command %i[saved] do |c|
|
584
|
-
c.
|
887
|
+
c.example 'na saved overdue', description: 'perform the search named "overdue"'
|
888
|
+
c.example 'na saved over', description: 'perform the search named "overdue", assuming no other searches match "over"'
|
889
|
+
c.example 'na saved', description: 'list available searches'
|
890
|
+
|
891
|
+
c.desc 'Open the saved search file in $EDITOR'
|
892
|
+
c.switch %i[e edit]
|
893
|
+
|
894
|
+
c.desc 'Delete the specified search definition'
|
895
|
+
c.switch %i[d delete]
|
896
|
+
|
897
|
+
c.action do |_global_options, options, args|
|
898
|
+
if options[:edit]
|
899
|
+
NA.edit_searches
|
900
|
+
end
|
901
|
+
|
585
902
|
searches = NA.load_searches
|
586
903
|
if args.empty?
|
587
904
|
NA.notify("{bg}Saved searches stored in {bw}#{NA.database_path(file: 'saved_searches.yml')}")
|
588
905
|
NA.notify(searches.map { |k, v| "{y}#{k}: {w}#{v}" }.join("\n"), exit_code: 0)
|
589
906
|
else
|
907
|
+
if options[:delete]
|
908
|
+
NA.delete_search(args)
|
909
|
+
end
|
910
|
+
|
590
911
|
keys = searches.keys.delete_if { |k| k !~ /#{args[0]}/ }
|
591
912
|
NA.notify("{r}Search #{args[0]} not found", exit_code: 1) if keys.empty?
|
592
913
|
|
@@ -614,8 +935,8 @@ class App
|
|
614
935
|
on_error do |exception|
|
615
936
|
case exception
|
616
937
|
when GLI::UnknownCommand
|
617
|
-
cmd = ['
|
618
|
-
cmd.concat(ARGV.unshift($first_arg))
|
938
|
+
cmd = ['saved']
|
939
|
+
cmd.concat(ARGV.unshift($first_arg))
|
619
940
|
|
620
941
|
exit run(cmd)
|
621
942
|
when SystemExit
|
data/lib/na/action.rb
CHANGED
@@ -2,20 +2,22 @@
|
|
2
2
|
|
3
3
|
module NA
|
4
4
|
class Action < Hash
|
5
|
-
attr_reader :file, :project, :parent, :action, :tags
|
5
|
+
attr_reader :file, :project, :parent, :action, :tags, :line, :note
|
6
6
|
|
7
|
-
def initialize(file, project, parent, action)
|
7
|
+
def initialize(file, project, parent, action, idx, note = [])
|
8
8
|
super()
|
9
9
|
|
10
10
|
@file = file
|
11
11
|
@project = project
|
12
12
|
@parent = parent
|
13
|
-
@action = action
|
13
|
+
@action = action.gsub(/\{/, '\\{')
|
14
14
|
@tags = scan_tags
|
15
|
+
@line = idx
|
16
|
+
@note = note
|
15
17
|
end
|
16
18
|
|
17
19
|
def to_s
|
18
|
-
"(#{@file}) #{@project}:#{@parent.join('>')} | #{@action}"
|
20
|
+
"(#{@file}:#{@line}) #{@project}:#{@parent.join('>')} | #{@action}"
|
19
21
|
end
|
20
22
|
|
21
23
|
def inspect
|
@@ -41,14 +43,10 @@ module NA
|
|
41
43
|
}
|
42
44
|
template = default_template.merge(template)
|
43
45
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
parents = "{dc}[{x}#{parents}{dc}]{x} "
|
49
|
-
else
|
50
|
-
parents = ''
|
51
|
-
end
|
46
|
+
parents = @parent.map do |par|
|
47
|
+
NA::Color.template("#{template[:parent]}#{par}")
|
48
|
+
end.join(NA::Color.template(template[:parent_divider]))
|
49
|
+
parents = "{dc}[{x}#{parents}{dc}]{x} "
|
52
50
|
|
53
51
|
project = NA::Color.template("#{template[:project]}#{@project}{x} ")
|
54
52
|
|
@@ -67,7 +65,7 @@ module NA
|
|
67
65
|
NA::Color.template(template[:output].gsub(/%filename/, filename)
|
68
66
|
.gsub(/%project/, project)
|
69
67
|
.gsub(/%parents?/, parents)
|
70
|
-
.gsub(/%action/, action.highlight_search(regexes)))
|
68
|
+
.gsub(/%action/, action.highlight_search(regexes))).gsub(/\\\{/, '{')
|
71
69
|
end
|
72
70
|
|
73
71
|
def tags_match?(any: [], all: [], none: [])
|
data/lib/na/colors.rb
CHANGED
@@ -226,8 +226,9 @@ module NA
|
|
226
226
|
##
|
227
227
|
def template(input)
|
228
228
|
input = input.join(' ') if input.is_a? Array
|
229
|
+
|
229
230
|
fmt = input.gsub(/%/, '%%')
|
230
|
-
fmt = fmt.gsub(/(?<!\\
|
231
|
+
fmt = fmt.gsub(/(?<!\\)\{(\w+)\}/i) do
|
231
232
|
Regexp.last_match(1).split('').map { |c| "%<#{c}>s" }.join('')
|
232
233
|
end
|
233
234
|
|