doing 2.0.3.pre → 2.0.8.pre

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/bin/doing +316 -114
  6. data/doing.rdoc +244 -19
  7. data/example_plugin.rb +1 -1
  8. data/generate_completions.sh +1 -0
  9. data/lib/completion/_doing.zsh +179 -127
  10. data/lib/completion/doing.bash +60 -27
  11. data/lib/completion/doing.fish +74 -23
  12. data/lib/doing/cli_status.rb +4 -0
  13. data/lib/doing/configuration.rb +2 -0
  14. data/lib/doing/errors.rb +22 -15
  15. data/lib/doing/item.rb +12 -11
  16. data/lib/doing/log_adapter.rb +27 -25
  17. data/lib/doing/plugin_manager.rb +1 -1
  18. data/lib/doing/plugins/export/json_export.rb +2 -2
  19. data/lib/doing/plugins/export/template_export.rb +1 -1
  20. data/lib/doing/plugins/import/calendar_import.rb +7 -1
  21. data/lib/doing/plugins/import/doing_import.rb +6 -6
  22. data/lib/doing/plugins/import/timing_import.rb +7 -1
  23. data/lib/doing/string.rb +9 -7
  24. data/lib/doing/version.rb +1 -1
  25. data/lib/doing/wwid.rb +160 -92
  26. data/lib/examples/commands/autotag.rb +63 -0
  27. data/lib/examples/commands/wiki.rb +1 -0
  28. data/lib/examples/plugins/say_export.rb +1 -1
  29. data/lib/examples/plugins/{templates → wiki_export/templates}/wiki.css +0 -0
  30. data/lib/examples/plugins/{templates → wiki_export/templates}/wiki.haml +0 -0
  31. data/lib/examples/plugins/{templates → wiki_export/templates}/wiki_index.haml +0 -0
  32. data/lib/examples/plugins/{wiki_export.rb → wiki_export/wiki_export.rb} +0 -0
  33. data/scripts/generate_bash_completions.rb +3 -2
  34. data/scripts/generate_fish_completions.rb +4 -1
  35. data/scripts/generate_zsh_completions.rb +44 -39
  36. metadata +7 -7
  37. data/doing.fish +0 -278
data/bin/doing CHANGED
@@ -57,8 +57,8 @@ config = Doing.config
57
57
  settings = config.settings
58
58
  wwid.config = settings
59
59
 
60
- if config.settings.dig('plugins', 'command_path')
61
- commands_from File.expand_path(config.settings.dig('plugins', 'command_path'))
60
+ if settings.dig('plugins', 'command_path')
61
+ commands_from File.expand_path(settings.dig('plugins', 'command_path'))
62
62
  end
63
63
 
64
64
  program_desc 'A CLI for a What Was I Doing system'
@@ -137,7 +137,7 @@ command %i[now next] do |c|
137
137
  if options[:back]
138
138
  date = wwid.chronify(options[:back], guess: :begin)
139
139
 
140
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
140
+ raise InvalidTimeExpression.new('unable to parse date string', topic: 'Date parser:') if date.nil?
141
141
  else
142
142
  date = Time.now
143
143
  end
@@ -149,13 +149,13 @@ command %i[now next] do |c|
149
149
  end
150
150
 
151
151
  if options[:e] || (args.empty? && $stdin.stat.size.zero?)
152
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
152
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
153
153
 
154
154
  input = ''
155
155
  input += args.join(' ') unless args.empty?
156
156
  input = wwid.fork_editor(input).strip
157
157
 
158
- raise Doing::Errors::EmptyInput, 'No content' if input.empty?
158
+ raise EmptyInput, 'No content' if input.empty?
159
159
 
160
160
  title, note = wwid.format_input(input)
161
161
  note.push(options[:n]) if options[:n]
@@ -173,14 +173,14 @@ command %i[now next] do |c|
173
173
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
174
174
  wwid.write(wwid.doing_file)
175
175
  else
176
- raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
176
+ raise EmptyInput, 'You must provide content when creating a new entry'
177
177
  end
178
178
  end
179
179
  end
180
180
 
181
181
  desc 'Reset the start time of an entry'
182
182
  command %i[reset begin] do |c|
183
- c.desc 'Set the start date of an item to now'
183
+ c.desc 'Limit search to section'
184
184
  c.arg_name 'NAME'
185
185
  c.flag %i[s section], default_value: 'All'
186
186
 
@@ -195,12 +195,18 @@ command %i[reset begin] do |c|
195
195
  c.arg_name 'QUERY'
196
196
  c.flag [:search]
197
197
 
198
+ c.desc 'Force exact search string matching (case sensitive)'
199
+ c.switch %i[x exact], default_value: false, negatable: false
200
+
201
+ c.desc 'Reset items that *don\'t* match search/tag filters'
202
+ c.switch [:not], default_value: false, negatable: false
203
+
198
204
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
199
205
  c.arg_name 'BOOLEAN'
200
206
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
201
207
 
202
208
  c.desc 'Select from a menu of matching entries'
203
- c.switch %i[i interactive]
209
+ c.switch %i[i interactive], negatable: false, default_value: false
204
210
 
205
211
  c.action do |global_options, options, args|
206
212
  if options[:section]
@@ -209,6 +215,13 @@ command %i[reset begin] do |c|
209
215
 
210
216
  options[:tag_bool] = options[:bool].normalize_bool
211
217
 
218
+ if options[:search]
219
+ search = options[:search]
220
+ search.sub!(/^'?/, "'") if options[:exact]
221
+ options[:search] = search
222
+ end
223
+
224
+
212
225
  items = wwid.filter_items([], opt: options)
213
226
 
214
227
  if options[:interactive]
@@ -266,12 +279,18 @@ command :note do |c|
266
279
  c.arg_name 'QUERY'
267
280
  c.flag [:search]
268
281
 
282
+ c.desc 'Force exact search string matching (case sensitive)'
283
+ c.switch %i[x exact], default_value: false, negatable: false
284
+
285
+ c.desc 'Add note to item that *doesn\'t* match search/tag filters'
286
+ c.switch [:not], default_value: false, negatable: false
287
+
269
288
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
270
289
  c.arg_name 'BOOLEAN'
271
290
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
272
291
 
273
292
  c.desc 'Select item for new note from a menu of matching entries'
274
- c.switch %i[i interactive]
293
+ c.switch %i[i interactive], negatable: false, default_value: false
275
294
 
276
295
  c.action do |_global_options, options, args|
277
296
  if options[:section]
@@ -280,6 +299,13 @@ command :note do |c|
280
299
 
281
300
  options[:tag_bool] = options[:bool].normalize_bool
282
301
 
302
+ if options[:search]
303
+ search = options[:search]
304
+ search.sub!(/^'?/, "'") if options[:exact]
305
+ options[:search] = search
306
+ end
307
+
308
+
283
309
  last_entry = wwid.last_entry(options)
284
310
 
285
311
  unless last_entry
@@ -291,7 +317,7 @@ command :note do |c|
291
317
  new_note = Doing::Note.new
292
318
 
293
319
  if options[:e] || (args.empty? && $stdin.stat.size.zero? && !options[:r])
294
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
320
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
295
321
 
296
322
  input = !args.empty? ? args.join(' ') : ''
297
323
 
@@ -312,7 +338,7 @@ command :note do |c|
312
338
  elsif $stdin.stat.size.positive?
313
339
  new_note.add($stdin.read)
314
340
  else
315
- raise Doing::Errors::EmptyInput, 'You must provide content when adding a note' unless options[:remove]
341
+ raise EmptyInput, 'You must provide content when adding a note' unless options[:remove]
316
342
  end
317
343
 
318
344
  if last_note.equal?(new_note)
@@ -338,7 +364,7 @@ command :meanwhile do |c|
338
364
  c.switch %i[e editor], negatable: false, default_value: false
339
365
 
340
366
  c.desc 'Archive previous @meanwhile entry'
341
- c.switch %i[a archive], default_value: false
367
+ c.switch %i[a archive], negatable: false, default_value: false
342
368
 
343
369
  c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]'
344
370
  c.arg_name 'DATE_STRING'
@@ -352,7 +378,7 @@ command :meanwhile do |c|
352
378
  if options[:back]
353
379
  date = wwid.chronify(options[:back], guess: :begin)
354
380
 
355
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
381
+ raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
356
382
  else
357
383
  date = Time.now
358
384
  end
@@ -365,7 +391,7 @@ command :meanwhile do |c|
365
391
  input = ''
366
392
 
367
393
  if options[:e]
368
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
394
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
369
395
 
370
396
  input += args.join(' ') unless args.empty?
371
397
  input = wwid.fork_editor(input).strip
@@ -403,7 +429,7 @@ long_desc %(
403
429
  arg_name 'TYPE', must_match: Doing::Plugins.template_regex
404
430
  command :template do |c|
405
431
  c.desc 'List all available templates'
406
- c.switch %i[l list]
432
+ c.switch %i[l list], negatable: false
407
433
 
408
434
  c.desc 'List in single column for completion'
409
435
  c.switch %i[c]
@@ -424,7 +450,7 @@ command :template do |c|
424
450
  type = args[0]
425
451
  end
426
452
 
427
- raise Doing::Errors::InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type
453
+ raise InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type
428
454
 
429
455
  $stdout.puts Doing::Plugins.template_for_trigger(type)
430
456
 
@@ -473,6 +499,12 @@ command :select do |c|
473
499
  c.arg_name 'QUERY'
474
500
  c.flag %i[q query search]
475
501
 
502
+ c.desc 'Force exact search string matching (case sensitive)'
503
+ c.switch %i[x exact], default_value: false, negatable: false
504
+
505
+ c.desc 'Select items that *don\'t* match search/tag filters'
506
+ c.switch [:not], default_value: false, negatable: false
507
+
476
508
  c.desc 'Use --no-menu to skip the interactive menu. Use with --query to filter items and act on results automatically. Test with `--output doing` to preview matches.'
477
509
  c.switch %i[menu], negatable: true, default_value: true
478
510
 
@@ -503,12 +535,12 @@ command :select do |c|
503
535
  c.flag %i[o output]
504
536
 
505
537
  c.desc "Copy selection as a new entry with current time and no @done tag. Only works with single selections. Can be combined with --editor."
506
- c.switch %i[again resume]
538
+ c.switch %i[again resume], negatable: false, default_value: false
507
539
 
508
540
  c.action do |_global_options, options, args|
509
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
541
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
510
542
 
511
- raise Doing::Errors::InvalidArgument, '--no-menu requires --query' if !options[:menu] && !options[:query]
543
+ raise InvalidArgument, '--no-menu requires --query' if !options[:menu] && !options[:query]
512
544
 
513
545
  wwid.interactive(options)
514
546
  end
@@ -531,17 +563,17 @@ command :later do |c|
531
563
  c.action do |_global_options, options, args|
532
564
  if options[:back]
533
565
  date = wwid.chronify(options[:back], guess: :begin)
534
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
566
+ raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
535
567
  else
536
568
  date = Time.now
537
569
  end
538
570
 
539
571
  if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
540
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
572
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
541
573
 
542
574
  input = args.empty? ? '' : args.join(' ')
543
575
  input = wwid.fork_editor(input).strip
544
- raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty?
576
+ raise EmptyInput, 'No content' unless input && !input.empty?
545
577
 
546
578
  title, note = wwid.format_input(input)
547
579
  note.push(options[:n]) if options[:n]
@@ -558,7 +590,7 @@ command :later do |c|
558
590
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
559
591
  wwid.write(wwid.doing_file)
560
592
  else
561
- raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
593
+ raise EmptyInput, 'You must provide content when creating a new entry'
562
594
  end
563
595
  end
564
596
  end
@@ -614,19 +646,19 @@ command %i[done did] do |c|
614
646
 
615
647
  if options[:took]
616
648
  took = wwid.chronify_qty(options[:took])
617
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
649
+ raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
618
650
  end
619
651
 
620
652
  if options[:back]
621
653
  date = wwid.chronify(options[:back], guess: :begin)
622
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
654
+ raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
623
655
  else
624
656
  date = options[:took] ? Time.now - took : Time.now
625
657
  end
626
658
 
627
659
  if options[:at]
628
660
  finish_date = wwid.chronify(options[:at], guess: :begin)
629
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
661
+ raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
630
662
 
631
663
  date = options[:took] ? finish_date - took : finish_date
632
664
  elsif options[:took]
@@ -651,7 +683,7 @@ command %i[done did] do |c|
651
683
  note.add(options[:note]) if options[:note]
652
684
 
653
685
  if options[:editor]
654
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
686
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
655
687
  is_new = false
656
688
 
657
689
  if args.empty?
@@ -659,7 +691,7 @@ command %i[done did] do |c|
659
691
 
660
692
  unless last_entry
661
693
  Doing.logger.debug('Skipped:', options[:unfinished] ? 'No unfinished entry' : 'Last entry already @done')
662
- raise Doing::Errors::NoResults, 'No results'
694
+ raise NoResults, 'No results'
663
695
  end
664
696
 
665
697
  old_entry = last_entry.dup
@@ -671,7 +703,7 @@ command %i[done did] do |c|
671
703
  end
672
704
 
673
705
  input = wwid.fork_editor(input).strip
674
- raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty?
706
+ raise EmptyInput, 'No content' unless input && !input.empty?
675
707
 
676
708
  title, note = wwid.format_input(input)
677
709
  new_entry = Doing::Item.new(date, title, section, note)
@@ -746,7 +778,7 @@ command %i[done did] do |c|
746
778
  wwid.write(wwid.doing_file)
747
779
  Doing.logger.info('Entry Added:', new_entry.title)
748
780
  else
749
- raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
781
+ raise EmptyInput, 'You must provide content when creating a new entry'
750
782
  end
751
783
  end
752
784
  end
@@ -774,7 +806,7 @@ command :cancel do |c|
774
806
  c.switch %i[u unfinished], negatable: false, default_value: false
775
807
 
776
808
  c.desc 'Select item(s) to cancel from a menu of matching entries'
777
- c.switch %i[i interactive]
809
+ c.switch %i[i interactive], negatable: false, default_value: false
778
810
 
779
811
  c.action do |_global_options, options, args|
780
812
  if options[:section]
@@ -789,9 +821,9 @@ command :cancel do |c|
789
821
  tags = options[:tag].to_tags
790
822
  end
791
823
 
792
- raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
824
+ raise InvalidArgument, 'Only one argument allowed' if args.length > 1
793
825
 
794
- raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
826
+ raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
795
827
 
796
828
  if options[:interactive]
797
829
  count = 0
@@ -844,6 +876,12 @@ command :finish do |c|
844
876
  c.arg_name 'QUERY'
845
877
  c.flag [:search]
846
878
 
879
+ c.desc 'Force exact search string matching (case sensitive)'
880
+ c.switch %i[x exact], default_value: false, negatable: false
881
+
882
+ c.desc 'Finish items that *don\'t* match search/tag filters'
883
+ c.switch [:not], default_value: false, negatable: false
884
+
847
885
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
848
886
  c.arg_name 'BOOLEAN'
849
887
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -867,28 +905,28 @@ command :finish do |c|
867
905
  c.flag %i[s section]
868
906
 
869
907
  c.desc 'Select item(s) to finish from a menu of matching entries'
870
- c.switch %i[i interactive]
908
+ c.switch %i[i interactive], negatable: false, default_value: false
871
909
 
872
910
  c.action do |_global_options, options, args|
873
911
  unless options[:auto]
874
912
  if options[:took]
875
913
  took = wwid.chronify_qty(options[:took])
876
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
914
+ raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
877
915
  end
878
916
 
879
- raise Doing::Errors::InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
917
+ raise InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
880
918
 
881
- raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
919
+ raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
882
920
 
883
921
  if options[:at]
884
922
  finish_date = wwid.chronify(options[:at], guess: :begin)
885
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
923
+ raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
886
924
 
887
925
  date = options[:took] ? finish_date - took : finish_date
888
926
  elsif options[:back]
889
927
  date = wwid.chronify(options[:back])
890
928
 
891
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
929
+ raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
892
930
  elsif options[:took]
893
931
  date = wwid.chronify_qty(options[:took])
894
932
  else
@@ -902,9 +940,9 @@ command :finish do |c|
902
940
  tags = options[:tag].to_tags
903
941
  end
904
942
 
905
- raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
943
+ raise InvalidArgument, 'Only one argument allowed' if args.length > 1
906
944
 
907
- raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
945
+ raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
908
946
 
909
947
  if options[:interactive]
910
948
  count = 0
@@ -912,12 +950,20 @@ command :finish do |c|
912
950
  count = args[0] ? args[0].to_i : 1
913
951
  end
914
952
 
953
+ search = nil
954
+
955
+ if options[:search]
956
+ search = options[:search]
957
+ search.sub!(/^'?/, "'") if options[:exact]
958
+ end
959
+
915
960
  opts = {
916
961
  archive: options[:archive],
917
962
  back: date,
918
963
  count: count,
919
964
  date: options[:date],
920
- search: options[:search],
965
+ search: search,
966
+ not: options[:not],
921
967
  section: options[:section],
922
968
  sequential: options[:auto],
923
969
  tag: tags,
@@ -951,6 +997,12 @@ command %i[again resume] do |c|
951
997
  c.arg_name 'QUERY'
952
998
  c.flag [:search]
953
999
 
1000
+ c.desc 'Force exact search string matching (case sensitive)'
1001
+ c.switch %i[x exact], default_value: false, negatable: false
1002
+
1003
+ c.desc 'Resume items that *don\'t* match search/tag filters'
1004
+ c.switch [:not], default_value: false, negatable: false
1005
+
954
1006
  c.desc 'Boolean used to combine multiple tags'
955
1007
  c.arg_name 'BOOLEAN'
956
1008
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -963,11 +1015,19 @@ command %i[again resume] do |c|
963
1015
  c.flag %i[n note]
964
1016
 
965
1017
  c.desc 'Select item to resume from a menu of matching entries'
966
- c.switch %i[i interactive]
1018
+ c.switch %i[i interactive], negatable: false, default_value: false
967
1019
 
968
1020
  c.action do |_global_options, options, _args|
969
1021
  tags = options[:tag].nil? ? [] : options[:tag].to_tags
1022
+
1023
+ if options[:search]
1024
+ search = options[:search]
1025
+ search.sub!(/^'?/, "'") if options[:exact]
1026
+ options[:search] = search
1027
+ end
1028
+
970
1029
  opts = options
1030
+
971
1031
  opts[:tag] = tags
972
1032
  opts[:tag_bool] = options[:bool].normalize_bool
973
1033
  opts[:interactive] = options[:interactive]
@@ -1037,17 +1097,23 @@ command :tag do |c|
1037
1097
  c.arg_name 'QUERY'
1038
1098
  c.flag [:search]
1039
1099
 
1100
+ c.desc 'Force exact search string matching (case sensitive)'
1101
+ c.switch %i[x exact], default_value: false, negatable: false
1102
+
1103
+ c.desc 'Tag items that *don\'t* match search/tag filters'
1104
+ c.switch [:not], default_value: false, negatable: false
1105
+
1040
1106
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1041
1107
  c.arg_name 'BOOLEAN'
1042
1108
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1043
1109
 
1044
1110
  c.desc 'Select item(s) to tag from a menu of matching entries'
1045
- c.switch %i[i interactive]
1111
+ c.switch %i[i interactive], negatable: false, default_value: false
1046
1112
 
1047
1113
  c.action do |_global_options, options, args|
1048
- raise Doing::Errors::MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:a]
1114
+ raise MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:autotag]
1049
1115
 
1050
- raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1116
+ raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1051
1117
 
1052
1118
  section = 'All'
1053
1119
 
@@ -1081,6 +1147,11 @@ command :tag do |c|
1081
1147
  count = options[:count].to_i
1082
1148
  end
1083
1149
 
1150
+ if options[:search]
1151
+ search = options[:search]
1152
+ search.sub!(/^'?/, "'") if options[:exact]
1153
+ options[:search] = search
1154
+ end
1084
1155
 
1085
1156
  if count.zero? && !options[:force]
1086
1157
  if options[:search]
@@ -1104,7 +1175,7 @@ command :tag do |c|
1104
1175
 
1105
1176
  res = wwid.yn(question, default_response: false)
1106
1177
 
1107
- exit_now! 'Cancelled' unless res
1178
+ raise UserCancelled unless res
1108
1179
  end
1109
1180
 
1110
1181
  options[:count] = count
@@ -1117,19 +1188,6 @@ command :tag do |c|
1117
1188
  end
1118
1189
  end
1119
1190
 
1120
- # desc 'Autotag last X entries'
1121
- # arg_name 'COUNT'
1122
- # command :autotag do |c|
1123
- # c.action do |global_options, options, args|
1124
- # options = {
1125
- # autotag: true,
1126
- # count: args[0].to_i
1127
- # }
1128
- # cmd = commands[:tag]
1129
- # cmd.action.(global_options, options, [])
1130
- # end
1131
- # end
1132
-
1133
1191
  desc 'Mark last entry as flagged'
1134
1192
  command [:mark, :flag] do |c|
1135
1193
  c.example 'doing flag', desc: 'Add @flagged to the last entry created'
@@ -1165,17 +1223,23 @@ command [:mark, :flag] do |c|
1165
1223
  c.arg_name 'QUERY'
1166
1224
  c.flag [:search]
1167
1225
 
1226
+ c.desc 'Force exact search string matching (case sensitive)'
1227
+ c.switch %i[x exact], default_value: false, negatable: false
1228
+
1229
+ c.desc 'Flag items that *don\'t* match search/tag/date filters'
1230
+ c.switch [:not], default_value: false, negatable: false
1231
+
1168
1232
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1169
1233
  c.arg_name 'BOOLEAN'
1170
1234
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1171
1235
 
1172
1236
  c.desc 'Select item(s) to flag from a menu of matching entries'
1173
- c.switch %i[i interactive]
1237
+ c.switch %i[i interactive], negatable: false, default_value: false
1174
1238
 
1175
1239
  c.action do |_global_options, options, _args|
1176
1240
  mark = settings['marker_tag'] || 'flagged'
1177
1241
 
1178
- raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1242
+ raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1179
1243
 
1180
1244
  section = 'All'
1181
1245
 
@@ -1196,6 +1260,12 @@ command [:mark, :flag] do |c|
1196
1260
  count = options[:count].to_i
1197
1261
  end
1198
1262
 
1263
+ if options[:search]
1264
+ search = options[:search]
1265
+ search.sub!(/^'?/, "'") if options[:exact]
1266
+ options[:search] = search
1267
+ end
1268
+
1199
1269
  if count.zero? && !options[:force]
1200
1270
  if options[:search]
1201
1271
  section_q = ' matching your search terms'
@@ -1270,6 +1340,12 @@ command :show do |c|
1270
1340
  c.arg_name 'QUERY'
1271
1341
  c.flag [:search]
1272
1342
 
1343
+ c.desc 'Force exact search string matching (case sensitive)'
1344
+ c.switch %i[x exact], default_value: false, negatable: false
1345
+
1346
+ c.desc 'Show items that *don\'t* match search/tag/date filters'
1347
+ c.switch [:not], default_value: false, negatable: false
1348
+
1273
1349
  c.desc 'Sort order (asc/desc)'
1274
1350
  c.arg_name 'ORDER'
1275
1351
  c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
@@ -1302,13 +1378,13 @@ command :show do |c|
1302
1378
  c.switch [:only_timed], default_value: false, negatable: false
1303
1379
 
1304
1380
  c.desc 'Select from a menu of matching entries to perform additional operations'
1305
- c.switch %i[i interactive]
1381
+ c.switch %i[i interactive], negatable: false, default_value: false
1306
1382
 
1307
1383
  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1308
1384
  c.arg_name 'FORMAT'
1309
1385
  c.flag %i[o output]
1310
- c.action do |_global_options, options, args|
1311
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1386
+ c.action do |global_options, options, args|
1387
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1312
1388
 
1313
1389
  tag_filter = false
1314
1390
  tags = []
@@ -1323,8 +1399,15 @@ command :show do |c|
1323
1399
  when /^@/
1324
1400
  section = 'All'
1325
1401
  else
1326
- section = wwid.guess_section(args[0])
1327
- raise Doing::Errors::InvalidSection, "No such section: #{args[0]}" unless section
1402
+ begin
1403
+ section = wwid.guess_section(args[0])
1404
+ rescue WrongCommand => exception
1405
+ cmd = commands[:view]
1406
+ action = cmd.send(:get_action, nil)
1407
+ return action.call(global_options, options, args)
1408
+ end
1409
+
1410
+ raise InvalidSection, "No such section: #{args[0]}" unless section
1328
1411
 
1329
1412
  args.shift
1330
1413
  end
@@ -1359,7 +1442,7 @@ command :show do |c|
1359
1442
  start = wwid.chronify(date_string, guess: :begin)
1360
1443
  finish = false
1361
1444
  end
1362
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1445
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
1363
1446
  dates = [start, finish]
1364
1447
  end
1365
1448
 
@@ -1367,6 +1450,12 @@ command :show do |c|
1367
1450
 
1368
1451
  tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1369
1452
 
1453
+ if options[:search]
1454
+ search = options[:search]
1455
+ search.sub!(/^'?/, "'") if options[:exact]
1456
+ options[:search] = search
1457
+ end
1458
+
1370
1459
  opt = options.dup
1371
1460
 
1372
1461
  opt[:sort_tags] = options[:tag_sort] =~ /^n/i
@@ -1429,20 +1518,31 @@ command %i[grep search] do |c|
1429
1518
  c.desc 'Only show items with recorded time intervals'
1430
1519
  c.switch [:only_timed], default_value: false, negatable: false
1431
1520
 
1521
+ c.desc 'Force exact string matching (case sensitive)'
1522
+ c.switch %i[x exact], default_value: false, negatable: false
1523
+
1524
+ c.desc 'Force case sensitive matching'
1525
+ c.switch %i[case]
1526
+
1527
+ c.desc 'Show items that *don\'t* match search string'
1528
+ c.switch [:not], default_value: false, negatable: false
1529
+
1432
1530
  c.desc 'Display an interactive menu of results to perform further operations'
1433
1531
  c.switch %i[i interactive], default_value: false, negatable: false
1434
1532
 
1435
1533
  c.action do |_global_options, options, args|
1436
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1534
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1437
1535
 
1438
1536
  tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1439
1537
 
1440
1538
  section = wwid.guess_section(options[:section]) if options[:section]
1539
+ search = args.join(' ')
1540
+ search.sub!(/^'?/, "'") if options[:exact]
1441
1541
 
1442
1542
  options[:times] = true if options[:totals]
1443
1543
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1444
1544
  options[:highlight] = true
1445
- options[:search] = args.join(' ')
1545
+ options[:search] = search
1446
1546
  options[:section] = section
1447
1547
  options[:tags_color] = tags_color
1448
1548
 
@@ -1476,7 +1576,7 @@ command :recent do |c|
1476
1576
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1477
1577
 
1478
1578
  c.desc 'Select from a menu of matching entries to perform additional operations'
1479
- c.switch %i[i interactive]
1579
+ c.switch %i[i interactive], negatable: false, default_value: false
1480
1580
 
1481
1581
  c.action do |global_options, options, args|
1482
1582
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
@@ -1548,7 +1648,7 @@ command :today do |c|
1548
1648
  c.flag [:after]
1549
1649
 
1550
1650
  c.action do |_global_options, options, _args|
1551
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1651
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1552
1652
 
1553
1653
  options[:t] = true if options[:totals]
1554
1654
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
@@ -1595,9 +1695,9 @@ command :on do |c|
1595
1695
  c.flag %i[o output]
1596
1696
 
1597
1697
  c.action do |_global_options, options, args|
1598
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1698
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1599
1699
 
1600
- raise Doing::Errors::MissingArgument, 'Missing date argument' if args.empty?
1700
+ raise MissingArgument, 'Missing date argument' if args.empty?
1601
1701
 
1602
1702
  date_string = args.join(' ')
1603
1703
 
@@ -1610,7 +1710,7 @@ command :on do |c|
1610
1710
  finish = false
1611
1711
  end
1612
1712
 
1613
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1713
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
1614
1714
 
1615
1715
  message = "Date interpreted as #{start}"
1616
1716
  message += " to #{finish}" if finish
@@ -1653,9 +1753,9 @@ command :since do |c|
1653
1753
  c.flag %i[o output]
1654
1754
 
1655
1755
  c.action do |_global_options, options, args|
1656
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1756
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1657
1757
 
1658
- raise Doing::Errors::MissingArgument, 'Missing date argument' if args.empty?
1758
+ raise MissingArgument, 'Missing date argument' if args.empty?
1659
1759
 
1660
1760
  date_string = args.join(' ')
1661
1761
 
@@ -1665,7 +1765,7 @@ command :since do |c|
1665
1765
  start = wwid.chronify(date_string, guess: :begin)
1666
1766
  finish = Time.now
1667
1767
 
1668
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1768
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
1669
1769
 
1670
1770
  Doing.logger.debug("Date interpreted as #{start} through the current time")
1671
1771
 
@@ -1698,7 +1798,6 @@ command :yesterday do |c|
1698
1798
  c.switch [:totals], default_value: false, negatable: false
1699
1799
 
1700
1800
  c.desc 'Sort tags by (name|time)'
1701
- default = 'time'
1702
1801
  default = settings['tag_sort'] || 'name'
1703
1802
  c.arg_name 'KEY'
1704
1803
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
@@ -1716,7 +1815,7 @@ command :yesterday do |c|
1716
1815
  c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
1717
1816
 
1718
1817
  c.action do |_global_options, options, _args|
1719
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1818
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1720
1819
 
1721
1820
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1722
1821
 
@@ -1761,8 +1860,14 @@ command :last do |c|
1761
1860
  c.arg_name 'QUERY'
1762
1861
  c.flag [:search]
1763
1862
 
1863
+ c.desc 'Force exact search string matching (case sensitive)'
1864
+ c.switch %i[x exact], default_value: false, negatable: false
1865
+
1866
+ c.desc 'Show items that *don\'t* match search string or tag filter'
1867
+ c.switch [:not], default_value: false, negatable: false
1868
+
1764
1869
  c.action do |global_options, options, _args|
1765
- raise Doing::Errors::InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
1870
+ raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
1766
1871
 
1767
1872
  if options[:tag].nil?
1768
1873
  tags = []
@@ -1779,11 +1884,18 @@ command :last do |c|
1779
1884
 
1780
1885
  end
1781
1886
 
1887
+ search = nil
1888
+
1889
+ if options[:search]
1890
+ search = options[:search]
1891
+ search.sub!(/^'?/, "'") if options[:exact]
1892
+ end
1893
+
1782
1894
  if options[:editor]
1783
- wwid.edit_last(section: options[:s], options: { search: options[:search], tag: tags, tag_bool: options[:bool] })
1895
+ wwid.edit_last(section: options[:s], options: { search: search, tag: tags, tag_bool: options[:bool], not: options[:not] })
1784
1896
  else
1785
1897
  Doing::Pager::page wwid.last(times: true, section: options[:s],
1786
- options: { search: options[:search], tag: tags, tag_bool: options[:bool] }).strip
1898
+ options: { search: search, negate: options[:not], tag: tags, tag_bool: options[:bool] }).strip
1787
1899
  end
1788
1900
  end
1789
1901
  end
@@ -1791,7 +1903,7 @@ end
1791
1903
  desc 'List sections'
1792
1904
  command :sections do |c|
1793
1905
  c.desc 'List in single column'
1794
- c.switch %i[c column], default_value: false
1906
+ c.switch %i[c column], negatable: false, default_value: false
1795
1907
 
1796
1908
  c.action do |_global_options, options, _args|
1797
1909
  joiner = options[:c] ? "\n" : "\t"
@@ -1814,7 +1926,7 @@ command :add_section do |c|
1814
1926
  c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file'
1815
1927
 
1816
1928
  c.action do |_global_options, _options, args|
1817
- raise Doing::Errors::InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
1929
+ raise InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
1818
1930
 
1819
1931
  wwid.add_section(args.join(' ').cap_first)
1820
1932
  wwid.write(wwid.doing_file)
@@ -1853,16 +1965,54 @@ command :plugins do |c|
1853
1965
 
1854
1966
  c.desc 'List plugins of type (import, export)'
1855
1967
  c.arg_name 'TYPE'
1856
- c.flag %i[t type], must_match: /^[iea].*$/i, default_value: 'all'
1968
+ c.flag %i[t type], must_match: /^(?:[iea].*)$/i, default_value: 'all'
1857
1969
 
1858
1970
  c.desc 'List in single column for completion'
1859
- c.switch %i[c column], default_value: false
1971
+ c.switch %i[c column], negatable: false, default_value: false
1860
1972
 
1861
1973
  c.action do |_global_options, options, _args|
1862
1974
  Doing::Plugins.list_plugins(options)
1863
1975
  end
1864
1976
  end
1865
1977
 
1978
+ desc 'Generate shell completion scripts'
1979
+ command :completion do |c|
1980
+ c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
1981
+ c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
1982
+ c.example 'doing completion --type fish --file ~/.config/fish/completions/doing.fish', desc: 'Output fish completions to file'
1983
+ c.example 'doing completion --type bash --file ~/.bash_it/completion/enabled/doing.bash', desc: 'Output bash completions to file'
1984
+
1985
+ c.desc 'Shell to generate for (bash, zsh, fish)'
1986
+ c.arg_name 'SHELL'
1987
+ c.flag %i[t type], must_match: /^[bzf](?:[ai]?sh)?$/i, default_value: 'zsh'
1988
+
1989
+ c.desc 'File to write output to'
1990
+ c.arg_name 'PATH'
1991
+ c.flag %i[f file], default_value: 'stdout'
1992
+
1993
+ c.action do |_global_options, options, _args|
1994
+ script_dir = File.join(File.dirname(__FILE__), '..', 'scripts')
1995
+
1996
+ case options[:type]
1997
+ when /^b/
1998
+ result = `ruby #{File.join(script_dir, 'generate_bash_completions.rb')}`
1999
+ when /^z/
2000
+ result = `ruby #{File.join(script_dir, 'generate_zsh_completions.rb')}`
2001
+ when /^f/
2002
+ result = `ruby #{File.join(script_dir, 'generate_fish_completions.rb')}`
2003
+ end
2004
+
2005
+ if options[:file] =~ /^stdout$/i
2006
+ $stdout.puts result
2007
+ else
2008
+ File.open(File.expand_path(options[:file]), 'w') do |f|
2009
+ f.puts result
2010
+ end
2011
+ Doing.logger.warn('File written:', "#{options[:type]} completions written to #{options[:file]}")
2012
+ end
2013
+ end
2014
+ end
2015
+
1866
2016
  desc 'Display a user-created view'
1867
2017
  long_desc 'Command line options override view configuration'
1868
2018
  arg_name 'VIEW_NAME'
@@ -1903,6 +2053,12 @@ command :view do |c|
1903
2053
  c.arg_name 'QUERY'
1904
2054
  c.flag [:search]
1905
2055
 
2056
+ c.desc 'Force exact search string matching (case sensitive)'
2057
+ c.switch %i[x exact], default_value: false, negatable: false
2058
+
2059
+ c.desc 'Show items that *don\'t* match search string'
2060
+ c.switch [:not], default_value: false, negatable: false
2061
+
1906
2062
  c.desc 'Sort tags by (name|time)'
1907
2063
  c.arg_name 'KEY'
1908
2064
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i
@@ -1923,17 +2079,24 @@ command :view do |c|
1923
2079
  c.switch [:only_timed], default_value: false, negatable: false
1924
2080
 
1925
2081
  c.desc 'Select from a menu of matching entries to perform additional operations'
1926
- c.switch %i[i interactive]
2082
+ c.switch %i[i interactive], negatable: false, default_value: false
1927
2083
 
1928
- c.action do |_global_options, options, args|
1929
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
2084
+ c.action do |global_options, options, args|
2085
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1930
2086
 
1931
- raise Doing::Errors::InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
2087
+ raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
1932
2088
 
1933
2089
  title = if args.empty?
1934
2090
  wwid.choose_view
1935
2091
  else
1936
- wwid.guess_view(args[0])
2092
+ begin
2093
+ wwid.guess_view(args[0])
2094
+ rescue WrongCommand => exception
2095
+ cmd = commands[:show]
2096
+ options[:sort] = 'asc'
2097
+ action = cmd.send(:get_action, nil)
2098
+ return action.call(global_options, options, args)
2099
+ end
1937
2100
  end
1938
2101
 
1939
2102
  if options[:section]
@@ -2023,11 +2186,19 @@ command :view do |c|
2023
2186
  start = wwid.chronify(date_string, guess: :begin)
2024
2187
  finish = false
2025
2188
  end
2026
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
2189
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
2027
2190
  dates = [start, finish]
2028
2191
  end
2029
2192
 
2193
+ search = nil
2194
+
2195
+ if options[:search]
2196
+ search = options[:search]
2197
+ search.sub!(/^'?/, "'") if options[:exact]
2198
+ end
2199
+
2030
2200
  opts = options
2201
+ opts[:search] = search
2031
2202
  opts[:output] = output_format
2032
2203
  opts[:count] = count
2033
2204
  opts[:format] = date_format
@@ -2046,9 +2217,9 @@ command :view do |c|
2046
2217
 
2047
2218
  Doing::Pager.page wwid.list_section(opts)
2048
2219
  elsif title.instance_of?(FalseClass)
2049
- exit_now! 'Cancelled'
2220
+ raise UserCancelled, 'Cancelled' unless res
2050
2221
  else
2051
- raise Doing::Errors::InvalidView, "View #{title} not found in config"
2222
+ raise InvalidView, "View #{title} not found in config"
2052
2223
  end
2053
2224
  end
2054
2225
  end
@@ -2100,6 +2271,12 @@ command %i[archive move] do |c|
2100
2271
  c.arg_name 'QUERY'
2101
2272
  c.flag [:search]
2102
2273
 
2274
+ c.desc 'Force exact search string matching (case sensitive)'
2275
+ c.switch %i[x exact], default_value: false, negatable: false
2276
+
2277
+ c.desc 'Show items that *don\'t* match search string'
2278
+ c.switch [:not], default_value: false, negatable: false
2279
+
2103
2280
  c.desc 'Archive entries older than date
2104
2281
  (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
2105
2282
  c.arg_name 'DATE_STRING'
@@ -2119,11 +2296,19 @@ command %i[archive move] do |c|
2119
2296
  tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
2120
2297
  end
2121
2298
 
2122
- raise Doing::Errors::InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
2299
+ raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
2123
2300
 
2124
2301
  tags.concat(options[:tag].to_tags) if options[:tag]
2125
2302
 
2303
+ search = nil
2304
+
2305
+ if options[:search]
2306
+ search = options[:search]
2307
+ search.sub!(/^'?/, "'") if options[:exact]
2308
+ end
2309
+
2126
2310
  opts = options
2311
+ opts[:search] = search
2127
2312
  opts[:bool] = options[:bool].normalize_bool
2128
2313
  opts[:destination] = options[:to]
2129
2314
  opts[:tags] = tags
@@ -2158,6 +2343,12 @@ command :rotate do |c|
2158
2343
  c.arg_name 'QUERY'
2159
2344
  c.flag [:search]
2160
2345
 
2346
+ c.desc 'Force exact search string matching (case sensitive)'
2347
+ c.switch %i[x exact], default_value: false, negatable: false
2348
+
2349
+ c.desc 'Rotate items that *don\'t* match search string or tag filter'
2350
+ c.switch [:not], default_value: false, negatable: false
2351
+
2161
2352
  c.desc 'Rotate entries older than date
2162
2353
  (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
2163
2354
  c.arg_name 'DATE_STRING'
@@ -2170,6 +2361,14 @@ command :rotate do |c|
2170
2361
 
2171
2362
  options[:bool] = options[:bool].normalize_bool
2172
2363
 
2364
+ search = nil
2365
+
2366
+ if options[:search]
2367
+ search = options[:search]
2368
+ search.sub!(/^'?/, "'") if options[:exact]
2369
+ options[:search] = search
2370
+ end
2371
+
2173
2372
  wwid.rotate(options)
2174
2373
  end
2175
2374
  end
@@ -2208,7 +2407,7 @@ command :open do |c|
2208
2407
  system %(open "#{File.expand_path(wwid.doing_file)}")
2209
2408
  end
2210
2409
  else
2211
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
2410
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
2212
2411
 
2213
2412
  system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
2214
2413
  end
@@ -2236,7 +2435,7 @@ command :config do |c|
2236
2435
  c.flag %i[e editor], default_value: nil
2237
2436
 
2238
2437
  c.desc 'Show a config key value based on arguments. Separate key paths with colons or dots, e.g. "export_templates.html". Empty arguments outputs the entire config.'
2239
- c.switch %i[d dump]
2438
+ c.switch %i[d dump], negatable: false
2240
2439
 
2241
2440
  c.desc 'Format for --dump (json|yaml|raw)'
2242
2441
  c.arg_name 'FORMAT'
@@ -2261,7 +2460,6 @@ command :config do |c|
2261
2460
  c.action do |_global_options, options, args|
2262
2461
  if options[:update]
2263
2462
  config.configure({rewrite: true, ignore_local: true})
2264
- # Doing.logger.warn("Config file rewritten: #{config.config_file}")
2265
2463
  return
2266
2464
  end
2267
2465
 
@@ -2276,7 +2474,6 @@ command :config do |c|
2276
2474
  when /^r/
2277
2475
  cfg
2278
2476
  else
2279
- # cfg = { last_key => cfg } unless last_key.nil?
2280
2477
  YAML.dump(cfg)
2281
2478
  end
2282
2479
  else
@@ -2291,7 +2488,7 @@ command :config do |c|
2291
2488
  choices.concat(config.additional_configs)
2292
2489
  res = wwid.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to edit > ')
2293
2490
 
2294
- raise Doing::Errors::UserCancelled, 'Cancelled' unless res
2491
+ raise UserCancelled, 'Cancelled' unless res
2295
2492
 
2296
2493
  config_file = res.strip || config.config_file
2297
2494
  else
@@ -2308,7 +2505,7 @@ command :config do |c|
2308
2505
  `open -a "#{editor}" "#{config_file}"`
2309
2506
  end
2310
2507
  else
2311
- raise Doing::Errors::InvalidArgument, 'No viable editor found in config or environment.'
2508
+ raise InvalidArgument, 'No viable editor found in config or environment.'
2312
2509
  end
2313
2510
  elsif options[:a] || options[:b]
2314
2511
  if options[:a]
@@ -2319,7 +2516,7 @@ command :config do |c|
2319
2516
  else
2320
2517
  editor = options[:e] || Doing::Util.find_default_editor('config')
2321
2518
 
2322
- raise Doing::Errors::MissingEditor, 'No viable editor defined in config or environment' unless editor
2519
+ raise MissingEditor, 'No viable editor defined in config or environment' unless editor
2323
2520
 
2324
2521
  if Doing::Util.exec_available(editor)
2325
2522
  system %(#{editor} "#{config_file}")
@@ -2329,7 +2526,7 @@ command :config do |c|
2329
2526
  end
2330
2527
  else
2331
2528
  editor = options[:e] || Doing::Util.default_editor
2332
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
2529
+ raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
2333
2530
 
2334
2531
  system %(#{editor} "#{config_file}")
2335
2532
  end
@@ -2360,6 +2557,12 @@ command :import do |c|
2360
2557
  c.arg_name 'QUERY'
2361
2558
  c.flag [:search]
2362
2559
 
2560
+ c.desc 'Force exact search string matching (case sensitive)'
2561
+ c.switch %i[x exact], default_value: false, negatable: false
2562
+
2563
+ c.desc 'Import items that *don\'t* match search/tag/date filters'
2564
+ c.switch [:not], default_value: false, negatable: false
2565
+
2363
2566
  c.desc 'Only import items with recorded time intervals'
2364
2567
  c.switch [:only_timed], default_value: false, negatable: false
2365
2568
 
@@ -2413,7 +2616,7 @@ command :import do |c|
2413
2616
  start = wwid.chronify(date_string, guess: :begin)
2414
2617
  finish = false
2415
2618
  end
2416
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
2619
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
2417
2620
  dates = [start, finish]
2418
2621
  end
2419
2622
 
@@ -2423,7 +2626,7 @@ command :import do |c|
2423
2626
  wwid.import(args, options)
2424
2627
  wwid.write(wwid.doing_file)
2425
2628
  else
2426
- raise Doing::Errors::InvalidPluginType, "Invalid import type: #{options[:type]}"
2629
+ raise InvalidPluginType, "Invalid import type: #{options[:type]}"
2427
2630
  end
2428
2631
  end
2429
2632
  end
@@ -2448,14 +2651,13 @@ pre do |global, _command, _options, _args|
2448
2651
  end
2449
2652
 
2450
2653
  on_error do |exception|
2451
- # if exception.kind_of?(SystemExit)
2452
- # false
2453
- # else
2454
- # p exception.inspect
2455
- # Doing.logger.output_results
2456
- # true
2457
- # end
2458
- false
2654
+ if exception.kind_of?(SystemExit)
2655
+ false
2656
+ else
2657
+ # Doing.logger.error('Fatal:', exception)
2658
+ Doing.logger.output_results
2659
+ true
2660
+ end
2459
2661
  end
2460
2662
 
2461
2663
  post do |global, _command, _options, _args|