doing 2.0.3.pre → 2.0.8.pre

Sign up to get free protection for your applications and to get access to all the features.
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|