na 1.1.26 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
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>\+)?(?<tok>.*?)$/)
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
- query: tokens,
116
- tag: tag,
117
- project: options[:project],
118
- require_na: require_na)
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
- puts NA::Color.template('{r}Cancelled{x}')
247
- Process.exit 1
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
- NA.add_action(target, options[:to], action, note)
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 { |c| "\"#{c}\"" }.join(' ')}")
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
- token: m['tok'],
328
- required: !m['req'].nil?
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
- query: todo,
335
- search: tokens,
336
- negate: options[:invert],
337
- regex: options[:regex],
338
- project: options[:project],
339
- require_na: false)
340
- if tokens.is_a?(Array)
341
- regexes = tokens.delete_if { |token| token[:negate] }.map { |token| token[:token] }
342
- else
343
- regexes = [tokens]
344
- end
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
- query: todo,
430
- tag: tags,
431
- negate: options[:invert],
432
- project: options[:project],
433
- require_na: false)
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, _args|
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
- tokens = []
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>\+)?(?<tok>.*?)$/)
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.action do |_global_options, _options, args|
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 = ['add']
618
- cmd.concat(ARGV.unshift($first_arg)) if ARGV.count.positive?
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
- if @parent != ['Inbox']
45
- parents = @parent.map do |par|
46
- NA::Color.template("#{template[:parent]}#{par}")
47
- end.join(NA::Color.template(template[:parent_divider]))
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(/(?<!\\u)\{(\w+)\}/i) do
231
+ fmt = fmt.gsub(/(?<!\\)\{(\w+)\}/i) do
231
232
  Regexp.last_match(1).split('').map { |c| "%<#{c}>s" }.join('')
232
233
  end
233
234