doing 2.0.5.pre → 2.0.9.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.
data/bin/doing CHANGED
@@ -57,12 +57,15 @@ 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'
65
- program_long_desc %(Doing uses a TaskPaper-like formatting to keep a plain text record of what you've been doing, complete with tag-based time tracking. The command line tool allows you to add entries, annotate with tags and notes, and view your entries with myriad options, with a focus on a "natural" language syntax.)
65
+ program_long_desc %(Doing uses a TaskPaper-like formatting to keep a plain text
66
+ record of what you've been doing, complete with tag-based time tracking. The
67
+ command line tool allows you to add entries, annotate with tags and notes, and
68
+ view your entries with myriad options, with a focus on a "natural" language syntax.)
66
69
 
67
70
  default_command :recent
68
71
  # sort_help :manually
@@ -137,7 +140,7 @@ command %i[now next] do |c|
137
140
  if options[:back]
138
141
  date = wwid.chronify(options[:back], guess: :begin)
139
142
 
140
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
143
+ raise InvalidTimeExpression.new('unable to parse date string', topic: 'Date parser:') if date.nil?
141
144
  else
142
145
  date = Time.now
143
146
  end
@@ -149,13 +152,13 @@ command %i[now next] do |c|
149
152
  end
150
153
 
151
154
  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?
155
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
153
156
 
154
157
  input = ''
155
158
  input += args.join(' ') unless args.empty?
156
159
  input = wwid.fork_editor(input).strip
157
160
 
158
- raise Doing::Errors::EmptyInput, 'No content' if input.empty?
161
+ raise EmptyInput, 'No content' if input.empty?
159
162
 
160
163
  title, note = wwid.format_input(input)
161
164
  note.push(options[:n]) if options[:n]
@@ -173,14 +176,14 @@ command %i[now next] do |c|
173
176
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
174
177
  wwid.write(wwid.doing_file)
175
178
  else
176
- raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
179
+ raise EmptyInput, 'You must provide content when creating a new entry'
177
180
  end
178
181
  end
179
182
  end
180
183
 
181
184
  desc 'Reset the start time of an entry'
182
185
  command %i[reset begin] do |c|
183
- c.desc 'Set the start date of an item to now'
186
+ c.desc 'Limit search to section'
184
187
  c.arg_name 'NAME'
185
188
  c.flag %i[s section], default_value: 'All'
186
189
 
@@ -195,6 +198,16 @@ command %i[reset begin] do |c|
195
198
  c.arg_name 'QUERY'
196
199
  c.flag [:search]
197
200
 
201
+ c.desc 'Force exact search string matching (case sensitive)'
202
+ c.switch %i[x exact], default_value: false, negatable: false
203
+
204
+ c.desc 'Reset items that *don\'t* match search/tag filters'
205
+ c.switch [:not], default_value: false, negatable: false
206
+
207
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
208
+ c.arg_name 'TYPE'
209
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
210
+
198
211
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
199
212
  c.arg_name 'BOOLEAN'
200
213
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -207,7 +220,16 @@ command %i[reset begin] do |c|
207
220
  options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
208
221
  end
209
222
 
210
- options[:tag_bool] = options[:bool].normalize_bool
223
+ options[:bool] = options[:bool].normalize_bool
224
+
225
+ options[:case] = options[:case].normalize_case
226
+
227
+ if options[:search]
228
+ search = options[:search]
229
+ search.sub!(/^'?/, "'") if options[:exact]
230
+ options[:search] = search
231
+ end
232
+
211
233
 
212
234
  items = wwid.filter_items([], opt: options)
213
235
 
@@ -248,6 +270,11 @@ long_desc %(
248
270
  )
249
271
  arg_name 'NOTE_TEXT'
250
272
  command :note do |c|
273
+ c.example 'doing note', desc: 'Open the last entry in $EDITOR to append a note'
274
+ c.example 'doing note "Just a quick annotation"', desc: 'Add a quick note to the last entry'
275
+ c.example 'doing note --tag done "Keeping it real or something"', desc: 'Add a note to the last item tagged @done'
276
+ c.example 'doing note --search "late night" -e', desc: 'Open $EDITOR to add a note to the last item containing "late night" (fuzzy matched)'
277
+
251
278
  c.desc 'Section'
252
279
  c.arg_name 'NAME'
253
280
  c.flag %i[s section], default_value: 'All'
@@ -266,6 +293,16 @@ command :note do |c|
266
293
  c.arg_name 'QUERY'
267
294
  c.flag [:search]
268
295
 
296
+ c.desc 'Force exact search string matching (case sensitive)'
297
+ c.switch %i[x exact], default_value: false, negatable: false
298
+
299
+ c.desc 'Add note to item that *doesn\'t* match search/tag filters'
300
+ c.switch [:not], default_value: false, negatable: false
301
+
302
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
303
+ c.arg_name 'TYPE'
304
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
305
+
269
306
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
270
307
  c.arg_name 'BOOLEAN'
271
308
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -280,6 +317,15 @@ command :note do |c|
280
317
 
281
318
  options[:tag_bool] = options[:bool].normalize_bool
282
319
 
320
+ options[:case] = options[:case].normalize_case
321
+
322
+ if options[:search]
323
+ search = options[:search]
324
+ search.sub!(/^'?/, "'") if options[:exact]
325
+ options[:search] = search
326
+ end
327
+
328
+
283
329
  last_entry = wwid.last_entry(options)
284
330
 
285
331
  unless last_entry
@@ -291,7 +337,7 @@ command :note do |c|
291
337
  new_note = Doing::Note.new
292
338
 
293
339
  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?
340
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
295
341
 
296
342
  input = !args.empty? ? args.join(' ') : ''
297
343
 
@@ -312,7 +358,7 @@ command :note do |c|
312
358
  elsif $stdin.stat.size.positive?
313
359
  new_note.add($stdin.read)
314
360
  else
315
- raise Doing::Errors::EmptyInput, 'You must provide content when adding a note' unless options[:remove]
361
+ raise EmptyInput, 'You must provide content when adding a note' unless options[:remove]
316
362
  end
317
363
 
318
364
  if last_note.equal?(new_note)
@@ -330,6 +376,11 @@ end
330
376
  desc 'Finish any running @meanwhile tasks and optionally create a new one'
331
377
  arg_name 'ENTRY'
332
378
  command :meanwhile do |c|
379
+ c.example 'doing meanwhile "Long task that will have others after it before it\'s done"', desc: 'Add a new long-running entry, completing any current @meanwhile entry'
380
+ c.example 'doing meanwhile', desc: 'Finish any open @meanwhile entry'
381
+ c.example 'doing meanwhile --archive', desc: 'Finish any open @meanwhile entry and archive it'
382
+ c.example 'doing meanwhile --back 2h "Something I\'ve been working on for a while', desc: 'Add a @meanwhile entry with a start date 2 hours ago'
383
+
333
384
  c.desc 'Section'
334
385
  c.arg_name 'NAME'
335
386
  c.flag %i[s section]
@@ -338,7 +389,7 @@ command :meanwhile do |c|
338
389
  c.switch %i[e editor], negatable: false, default_value: false
339
390
 
340
391
  c.desc 'Archive previous @meanwhile entry'
341
- c.switch %i[a archive], default_value: false
392
+ c.switch %i[a archive], negatable: false, default_value: false
342
393
 
343
394
  c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]'
344
395
  c.arg_name 'DATE_STRING'
@@ -352,7 +403,7 @@ command :meanwhile do |c|
352
403
  if options[:back]
353
404
  date = wwid.chronify(options[:back], guess: :begin)
354
405
 
355
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
406
+ raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
356
407
  else
357
408
  date = Time.now
358
409
  end
@@ -365,7 +416,7 @@ command :meanwhile do |c|
365
416
  input = ''
366
417
 
367
418
  if options[:e]
368
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
419
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
369
420
 
370
421
  input += args.join(' ') unless args.empty?
371
422
  input = wwid.fork_editor(input).strip
@@ -396,14 +447,14 @@ end
396
447
  desc 'Output HTML, CSS, and Markdown (ERB) templates for customization'
397
448
  long_desc %(
398
449
  Templates are printed to STDOUT for piping to a file.
399
- Save them and use them in the configuration file under html_template.
400
-
401
- Example `doing template haml > ~/styles/my_doing.haml`
450
+ Save them and use them in the configuration file under export_templates.
402
451
  )
403
452
  arg_name 'TYPE', must_match: Doing::Plugins.template_regex
404
453
  command :template do |c|
454
+ c.example 'doing template haml > ~/styles/my_doing.haml', desc: 'Output the haml template and save it to a file'
455
+
405
456
  c.desc 'List all available templates'
406
- c.switch %i[l list]
457
+ c.switch %i[l list], negatable: false
407
458
 
408
459
  c.desc 'List in single column for completion'
409
460
  c.switch %i[c]
@@ -424,7 +475,7 @@ command :template do |c|
424
475
  type = args[0]
425
476
  end
426
477
 
427
- raise Doing::Errors::InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type
478
+ raise InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type
428
479
 
429
480
  $stdout.puts Doing::Plugins.template_for_trigger(type)
430
481
 
@@ -473,6 +524,16 @@ command :select do |c|
473
524
  c.arg_name 'QUERY'
474
525
  c.flag %i[q query search]
475
526
 
527
+ c.desc 'Force exact search string matching (case sensitive)'
528
+ c.switch %i[x exact], default_value: false, negatable: false
529
+
530
+ c.desc 'Select items that *don\'t* match search/tag filters'
531
+ c.switch [:not], default_value: false, negatable: false
532
+
533
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
534
+ c.arg_name 'TYPE'
535
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
536
+
476
537
  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
538
  c.switch %i[menu], negatable: true, default_value: true
478
539
 
@@ -503,12 +564,14 @@ command :select do |c|
503
564
  c.flag %i[o output]
504
565
 
505
566
  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]
567
+ c.switch %i[again resume], negatable: false, default_value: false
507
568
 
508
569
  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)
570
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
510
571
 
511
- raise Doing::Errors::InvalidArgument, '--no-menu requires --query' if !options[:menu] && !options[:query]
572
+ raise InvalidArgument, '--no-menu requires --query' if !options[:menu] && !options[:query]
573
+
574
+ options[:case] = options[:case].normalize_case
512
575
 
513
576
  wwid.interactive(options)
514
577
  end
@@ -517,6 +580,9 @@ end
517
580
  desc 'Add an item to the Later section'
518
581
  arg_name 'ENTRY'
519
582
  command :later do |c|
583
+ c.example 'doing later "Something I\'ll think about tomorrow"', desc: 'Add an entry to the Later section'
584
+ c.example 'doing later -e', desc: 'Open $EDITOR to create an entry and optional note'
585
+
520
586
  c.desc "Edit entry with #{Doing::Util.default_editor}"
521
587
  c.switch %i[e editor], negatable: false, default_value: false
522
588
 
@@ -531,17 +597,17 @@ command :later do |c|
531
597
  c.action do |_global_options, options, args|
532
598
  if options[:back]
533
599
  date = wwid.chronify(options[:back], guess: :begin)
534
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
600
+ raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
535
601
  else
536
602
  date = Time.now
537
603
  end
538
604
 
539
605
  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?
606
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
541
607
 
542
608
  input = args.empty? ? '' : args.join(' ')
543
609
  input = wwid.fork_editor(input).strip
544
- raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty?
610
+ raise EmptyInput, 'No content' unless input && !input.empty?
545
611
 
546
612
  title, note = wwid.format_input(input)
547
613
  note.push(options[:n]) if options[:n]
@@ -558,7 +624,7 @@ command :later do |c|
558
624
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
559
625
  wwid.write(wwid.doing_file)
560
626
  else
561
- raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
627
+ raise EmptyInput, 'You must provide content when creating a new entry'
562
628
  end
563
629
  end
564
630
  end
@@ -566,6 +632,11 @@ end
566
632
  desc 'Add a completed item with @done(date). No argument finishes last entry.'
567
633
  arg_name 'ENTRY'
568
634
  command %i[done did] do |c|
635
+ c.example 'doing done', desc: 'Tag the last entry @done'
636
+ c.example 'doing done I already finished this', desc: 'Add a new entry and immediately mark it @done'
637
+ c.example 'doing done --back 30m This took me half an hour', desc: 'Add an entry with a start date 30 minutes ago and a @done date of right now'
638
+ c.example 'doing done --at 3pm --took 1h Started and finished this afternoon', desc: 'Add an entry with a @done date of 3pm and a start date of 2pm (3pm - 1h)'
639
+
569
640
  c.desc 'Remove @done tag'
570
641
  c.switch %i[r remove], negatable: false, default_value: false
571
642
 
@@ -614,19 +685,19 @@ command %i[done did] do |c|
614
685
 
615
686
  if options[:took]
616
687
  took = wwid.chronify_qty(options[:took])
617
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
688
+ raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
618
689
  end
619
690
 
620
691
  if options[:back]
621
692
  date = wwid.chronify(options[:back], guess: :begin)
622
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
693
+ raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
623
694
  else
624
695
  date = options[:took] ? Time.now - took : Time.now
625
696
  end
626
697
 
627
698
  if options[:at]
628
699
  finish_date = wwid.chronify(options[:at], guess: :begin)
629
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
700
+ raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
630
701
 
631
702
  date = options[:took] ? finish_date - took : finish_date
632
703
  elsif options[:took]
@@ -651,7 +722,7 @@ command %i[done did] do |c|
651
722
  note.add(options[:note]) if options[:note]
652
723
 
653
724
  if options[:editor]
654
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
725
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
655
726
  is_new = false
656
727
 
657
728
  if args.empty?
@@ -659,7 +730,7 @@ command %i[done did] do |c|
659
730
 
660
731
  unless last_entry
661
732
  Doing.logger.debug('Skipped:', options[:unfinished] ? 'No unfinished entry' : 'Last entry already @done')
662
- raise Doing::Errors::NoResults, 'No results'
733
+ raise NoResults, 'No results'
663
734
  end
664
735
 
665
736
  old_entry = last_entry.dup
@@ -671,7 +742,7 @@ command %i[done did] do |c|
671
742
  end
672
743
 
673
744
  input = wwid.fork_editor(input).strip
674
- raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty?
745
+ raise EmptyInput, 'No content' unless input && !input.empty?
675
746
 
676
747
  title, note = wwid.format_input(input)
677
748
  new_entry = Doing::Item.new(date, title, section, note)
@@ -746,7 +817,7 @@ command %i[done did] do |c|
746
817
  wwid.write(wwid.doing_file)
747
818
  Doing.logger.info('Entry Added:', new_entry.title)
748
819
  else
749
- raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
820
+ raise EmptyInput, 'You must provide content when creating a new entry'
750
821
  end
751
822
  end
752
823
  end
@@ -755,6 +826,9 @@ desc 'End last X entries with no time tracked'
755
826
  long_desc 'Adds @done tag without datestamp so no elapsed time is recorded. Alias for `doing finish --no-date`.'
756
827
  arg_name 'COUNT'
757
828
  command :cancel do |c|
829
+ c.example 'doing cancel', desc: 'Cancel the last entry'
830
+ c.example 'doing cancel --tag project1 -u 5', desc: 'Cancel the last 5 unfinished entries containing @project1'
831
+
758
832
  c.desc 'Archive entries'
759
833
  c.switch %i[a archive], negatable: false, default_value: false
760
834
 
@@ -770,6 +844,20 @@ command :cancel do |c|
770
844
  c.arg_name 'BOOLEAN'
771
845
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
772
846
 
847
+ c.desc 'Cancel the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
848
+ c.arg_name 'QUERY'
849
+ c.flag [:search]
850
+
851
+ c.desc 'Force exact search string matching (case sensitive)'
852
+ c.switch %i[x exact], default_value: false, negatable: false
853
+
854
+ c.desc 'Finish items that *don\'t* match search/tag filters'
855
+ c.switch [:not], default_value: false, negatable: false
856
+
857
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
858
+ c.arg_name 'TYPE'
859
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
860
+
773
861
  c.desc 'Cancel last entry (or entries) not already marked @done'
774
862
  c.switch %i[u unfinished], negatable: false, default_value: false
775
863
 
@@ -789,9 +877,9 @@ command :cancel do |c|
789
877
  tags = options[:tag].to_tags
790
878
  end
791
879
 
792
- raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
880
+ raise InvalidArgument, 'Only one argument allowed' if args.length > 1
793
881
 
794
- raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
882
+ raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
795
883
 
796
884
  if options[:interactive]
797
885
  count = 0
@@ -799,7 +887,17 @@ command :cancel do |c|
799
887
  count = args[0] ? args[0].to_i : 1
800
888
  end
801
889
 
890
+ search = nil
891
+
892
+ if options[:search]
893
+ search = options[:search]
894
+ search.sub!(/^'?/, "'") if options[:exact]
895
+ end
896
+
802
897
  opts = {
898
+ search: search,
899
+ case: options[:case].normalize_case,
900
+ not: options[:not],
803
901
  archive: options[:a],
804
902
  count: count,
805
903
  date: false,
@@ -820,6 +918,10 @@ desc 'Mark last X entries as @done'
820
918
  long_desc 'Marks the last X entries with a @done tag and current date. Does not alter already completed entries.'
821
919
  arg_name 'COUNT'
822
920
  command :finish do |c|
921
+ c.example 'doing finish', desc: 'Mark the last entry @done'
922
+ c.example 'doing finish --auto --section Later 10', desc: 'Add @done to any unfinished entries in the last 10 in Later, setting the finish time based on the start time of the task after it'
923
+ c.example 'doing finish --search "a specific entry" --at "yesterday 3pm"', desc: 'Search for an entry containing string and set its @done time to yesterday at 3pm'
924
+
823
925
  c.desc 'Include date'
824
926
  c.switch [:date], negatable: true, default_value: true
825
927
 
@@ -844,6 +946,16 @@ command :finish do |c|
844
946
  c.arg_name 'QUERY'
845
947
  c.flag [:search]
846
948
 
949
+ c.desc 'Force exact search string matching (case sensitive)'
950
+ c.switch %i[x exact], default_value: false, negatable: false
951
+
952
+ c.desc 'Finish items that *don\'t* match search/tag filters'
953
+ c.switch [:not], default_value: false, negatable: false
954
+
955
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
956
+ c.arg_name 'TYPE'
957
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
958
+
847
959
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
848
960
  c.arg_name 'BOOLEAN'
849
961
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -873,22 +985,22 @@ command :finish do |c|
873
985
  unless options[:auto]
874
986
  if options[:took]
875
987
  took = wwid.chronify_qty(options[:took])
876
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
988
+ raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
877
989
  end
878
990
 
879
- raise Doing::Errors::InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
991
+ raise InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
880
992
 
881
- raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
993
+ raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
882
994
 
883
995
  if options[:at]
884
996
  finish_date = wwid.chronify(options[:at], guess: :begin)
885
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
997
+ raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
886
998
 
887
999
  date = options[:took] ? finish_date - took : finish_date
888
1000
  elsif options[:back]
889
1001
  date = wwid.chronify(options[:back])
890
1002
 
891
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
1003
+ raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
892
1004
  elsif options[:took]
893
1005
  date = wwid.chronify_qty(options[:took])
894
1006
  else
@@ -902,9 +1014,9 @@ command :finish do |c|
902
1014
  tags = options[:tag].to_tags
903
1015
  end
904
1016
 
905
- raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
1017
+ raise InvalidArgument, 'Only one argument allowed' if args.length > 1
906
1018
 
907
- raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
1019
+ raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
908
1020
 
909
1021
  if options[:interactive]
910
1022
  count = 0
@@ -912,12 +1024,21 @@ command :finish do |c|
912
1024
  count = args[0] ? args[0].to_i : 1
913
1025
  end
914
1026
 
1027
+ search = nil
1028
+
1029
+ if options[:search]
1030
+ search = options[:search]
1031
+ search.sub!(/^'?/, "'") if options[:exact]
1032
+ end
1033
+
915
1034
  opts = {
916
1035
  archive: options[:archive],
917
1036
  back: date,
918
1037
  count: count,
919
1038
  date: options[:date],
920
- search: options[:search],
1039
+ search: search,
1040
+ case: options[:case].normalize_case,
1041
+ not: options[:not],
921
1042
  section: options[:section],
922
1043
  sequential: options[:auto],
923
1044
  tag: tags,
@@ -951,6 +1072,16 @@ command %i[again resume] do |c|
951
1072
  c.arg_name 'QUERY'
952
1073
  c.flag [:search]
953
1074
 
1075
+ c.desc 'Force exact search string matching (case sensitive)'
1076
+ c.switch %i[x exact], default_value: false, negatable: false
1077
+
1078
+ c.desc 'Resume items that *don\'t* match search/tag filters'
1079
+ c.switch [:not], default_value: false, negatable: false
1080
+
1081
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
1082
+ c.arg_name 'TYPE'
1083
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1084
+
954
1085
  c.desc 'Boolean used to combine multiple tags'
955
1086
  c.arg_name 'BOOLEAN'
956
1087
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -967,7 +1098,17 @@ command %i[again resume] do |c|
967
1098
 
968
1099
  c.action do |_global_options, options, _args|
969
1100
  tags = options[:tag].nil? ? [] : options[:tag].to_tags
1101
+
1102
+ options[:case] = options[:case].normalize_case
1103
+
1104
+ if options[:search]
1105
+ search = options[:search]
1106
+ search.sub!(/^'?/, "'") if options[:exact]
1107
+ options[:search] = search
1108
+ end
1109
+
970
1110
  opts = options
1111
+
971
1112
  opts[:tag] = tags
972
1113
  opts[:tag_bool] = options[:bool].normalize_bool
973
1114
  opts[:interactive] = options[:interactive]
@@ -1037,6 +1178,16 @@ command :tag do |c|
1037
1178
  c.arg_name 'QUERY'
1038
1179
  c.flag [:search]
1039
1180
 
1181
+ c.desc 'Force exact search string matching (case sensitive)'
1182
+ c.switch %i[x exact], default_value: false, negatable: false
1183
+
1184
+ c.desc 'Tag items that *don\'t* match search/tag filters'
1185
+ c.switch [:not], default_value: false, negatable: false
1186
+
1187
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
1188
+ c.arg_name 'TYPE'
1189
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1190
+
1040
1191
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1041
1192
  c.arg_name 'BOOLEAN'
1042
1193
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -1045,9 +1196,9 @@ command :tag do |c|
1045
1196
  c.switch %i[i interactive], negatable: false, default_value: false
1046
1197
 
1047
1198
  c.action do |_global_options, options, args|
1048
- raise Doing::Errors::MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:a]
1199
+ raise MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:autotag]
1049
1200
 
1050
- raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1201
+ raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1051
1202
 
1052
1203
  section = 'All'
1053
1204
 
@@ -1081,6 +1232,13 @@ command :tag do |c|
1081
1232
  count = options[:count].to_i
1082
1233
  end
1083
1234
 
1235
+ options[:case] = options[:case].normalize_case
1236
+
1237
+ if options[:search]
1238
+ search = options[:search]
1239
+ search.sub!(/^'?/, "'") if options[:exact]
1240
+ options[:search] = search
1241
+ end
1084
1242
 
1085
1243
  if count.zero? && !options[:force]
1086
1244
  if options[:search]
@@ -1104,7 +1262,7 @@ command :tag do |c|
1104
1262
 
1105
1263
  res = wwid.yn(question, default_response: false)
1106
1264
 
1107
- exit_now! 'Cancelled' unless res
1265
+ raise UserCancelled unless res
1108
1266
  end
1109
1267
 
1110
1268
  options[:count] = count
@@ -1117,19 +1275,6 @@ command :tag do |c|
1117
1275
  end
1118
1276
  end
1119
1277
 
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
1278
  desc 'Mark last entry as flagged'
1134
1279
  command [:mark, :flag] do |c|
1135
1280
  c.example 'doing flag', desc: 'Add @flagged to the last entry created'
@@ -1165,6 +1310,16 @@ command [:mark, :flag] do |c|
1165
1310
  c.arg_name 'QUERY'
1166
1311
  c.flag [:search]
1167
1312
 
1313
+ c.desc 'Force exact search string matching (case sensitive)'
1314
+ c.switch %i[x exact], default_value: false, negatable: false
1315
+
1316
+ c.desc 'Flag items that *don\'t* match search/tag/date filters'
1317
+ c.switch [:not], default_value: false, negatable: false
1318
+
1319
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
1320
+ c.arg_name 'TYPE'
1321
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1322
+
1168
1323
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1169
1324
  c.arg_name 'BOOLEAN'
1170
1325
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -1175,7 +1330,7 @@ command [:mark, :flag] do |c|
1175
1330
  c.action do |_global_options, options, _args|
1176
1331
  mark = settings['marker_tag'] || 'flagged'
1177
1332
 
1178
- raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1333
+ raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1179
1334
 
1180
1335
  section = 'All'
1181
1336
 
@@ -1196,6 +1351,14 @@ command [:mark, :flag] do |c|
1196
1351
  count = options[:count].to_i
1197
1352
  end
1198
1353
 
1354
+ options[:case] = options[:case].normalize_case
1355
+
1356
+ if options[:search]
1357
+ search = options[:search]
1358
+ search.sub!(/^'?/, "'") if options[:exact]
1359
+ options[:search] = search
1360
+ end
1361
+
1199
1362
  if count.zero? && !options[:force]
1200
1363
  if options[:search]
1201
1364
  section_q = ' matching your search terms'
@@ -1270,6 +1433,16 @@ command :show do |c|
1270
1433
  c.arg_name 'QUERY'
1271
1434
  c.flag [:search]
1272
1435
 
1436
+ c.desc 'Force exact search string matching (case sensitive)'
1437
+ c.switch %i[x exact], default_value: false, negatable: false
1438
+
1439
+ c.desc 'Show items that *don\'t* match search/tag/date filters'
1440
+ c.switch [:not], default_value: false, negatable: false
1441
+
1442
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
1443
+ c.arg_name 'TYPE'
1444
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1445
+
1273
1446
  c.desc 'Sort order (asc/desc)'
1274
1447
  c.arg_name 'ORDER'
1275
1448
  c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
@@ -1307,8 +1480,8 @@ command :show do |c|
1307
1480
  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1308
1481
  c.arg_name 'FORMAT'
1309
1482
  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)
1483
+ c.action do |global_options, options, args|
1484
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1312
1485
 
1313
1486
  tag_filter = false
1314
1487
  tags = []
@@ -1323,8 +1496,15 @@ command :show do |c|
1323
1496
  when /^@/
1324
1497
  section = 'All'
1325
1498
  else
1326
- section = wwid.guess_section(args[0])
1327
- raise Doing::Errors::InvalidSection, "No such section: #{args[0]}" unless section
1499
+ begin
1500
+ section = wwid.guess_section(args[0])
1501
+ rescue WrongCommand => exception
1502
+ cmd = commands[:view]
1503
+ action = cmd.send(:get_action, nil)
1504
+ return action.call(global_options, options, args)
1505
+ end
1506
+
1507
+ raise InvalidSection, "No such section: #{args[0]}" unless section
1328
1508
 
1329
1509
  args.shift
1330
1510
  end
@@ -1359,7 +1539,7 @@ command :show do |c|
1359
1539
  start = wwid.chronify(date_string, guess: :begin)
1360
1540
  finish = false
1361
1541
  end
1362
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1542
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
1363
1543
  dates = [start, finish]
1364
1544
  end
1365
1545
 
@@ -1367,6 +1547,14 @@ command :show do |c|
1367
1547
 
1368
1548
  tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1369
1549
 
1550
+ options[:case] = options[:case].normalize_case
1551
+
1552
+ if options[:search]
1553
+ search = options[:search]
1554
+ search.sub!(/^'?/, "'") if options[:exact]
1555
+ options[:search] = search
1556
+ end
1557
+
1370
1558
  opt = options.dup
1371
1559
 
1372
1560
  opt[:sort_tags] = options[:tag_sort] =~ /^n/i
@@ -1429,20 +1617,35 @@ command %i[grep search] do |c|
1429
1617
  c.desc 'Only show items with recorded time intervals'
1430
1618
  c.switch [:only_timed], default_value: false, negatable: false
1431
1619
 
1620
+ c.desc 'Force exact string matching (case sensitive)'
1621
+ c.switch %i[x exact], default_value: false, negatable: false
1622
+
1623
+ c.desc 'Show items that *don\'t* match search string'
1624
+ c.switch [:not], default_value: false, negatable: false
1625
+
1626
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
1627
+ c.arg_name 'TYPE'
1628
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1629
+
1432
1630
  c.desc 'Display an interactive menu of results to perform further operations'
1433
1631
  c.switch %i[i interactive], default_value: false, negatable: false
1434
1632
 
1435
1633
  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)
1634
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1437
1635
 
1438
1636
  tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1439
1637
 
1440
1638
  section = wwid.guess_section(options[:section]) if options[:section]
1441
1639
 
1640
+ options[:case] = options[:case].normalize_case
1641
+
1642
+ search = args.join(' ')
1643
+ search.sub!(/^'?/, "'") if options[:exact]
1644
+
1442
1645
  options[:times] = true if options[:totals]
1443
1646
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1444
1647
  options[:highlight] = true
1445
- options[:search] = args.join(' ')
1648
+ options[:search] = search
1446
1649
  options[:section] = section
1447
1650
  options[:tags_color] = tags_color
1448
1651
 
@@ -1548,7 +1751,7 @@ command :today do |c|
1548
1751
  c.flag [:after]
1549
1752
 
1550
1753
  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)
1754
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1552
1755
 
1553
1756
  options[:t] = true if options[:totals]
1554
1757
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
@@ -1595,9 +1798,9 @@ command :on do |c|
1595
1798
  c.flag %i[o output]
1596
1799
 
1597
1800
  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)
1801
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1599
1802
 
1600
- raise Doing::Errors::MissingArgument, 'Missing date argument' if args.empty?
1803
+ raise MissingArgument, 'Missing date argument' if args.empty?
1601
1804
 
1602
1805
  date_string = args.join(' ')
1603
1806
 
@@ -1610,7 +1813,7 @@ command :on do |c|
1610
1813
  finish = false
1611
1814
  end
1612
1815
 
1613
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1816
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
1614
1817
 
1615
1818
  message = "Date interpreted as #{start}"
1616
1819
  message += " to #{finish}" if finish
@@ -1653,9 +1856,9 @@ command :since do |c|
1653
1856
  c.flag %i[o output]
1654
1857
 
1655
1858
  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)
1859
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1657
1860
 
1658
- raise Doing::Errors::MissingArgument, 'Missing date argument' if args.empty?
1861
+ raise MissingArgument, 'Missing date argument' if args.empty?
1659
1862
 
1660
1863
  date_string = args.join(' ')
1661
1864
 
@@ -1665,7 +1868,7 @@ command :since do |c|
1665
1868
  start = wwid.chronify(date_string, guess: :begin)
1666
1869
  finish = Time.now
1667
1870
 
1668
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1871
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
1669
1872
 
1670
1873
  Doing.logger.debug("Date interpreted as #{start} through the current time")
1671
1874
 
@@ -1698,7 +1901,6 @@ command :yesterday do |c|
1698
1901
  c.switch [:totals], default_value: false, negatable: false
1699
1902
 
1700
1903
  c.desc 'Sort tags by (name|time)'
1701
- default = 'time'
1702
1904
  default = settings['tag_sort'] || 'name'
1703
1905
  c.arg_name 'KEY'
1704
1906
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
@@ -1716,7 +1918,7 @@ command :yesterday do |c|
1716
1918
  c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
1717
1919
 
1718
1920
  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)
1921
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1720
1922
 
1721
1923
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1722
1924
 
@@ -1761,8 +1963,18 @@ command :last do |c|
1761
1963
  c.arg_name 'QUERY'
1762
1964
  c.flag [:search]
1763
1965
 
1966
+ c.desc 'Force exact search string matching (case sensitive)'
1967
+ c.switch %i[x exact], default_value: false, negatable: false
1968
+
1969
+ c.desc 'Show items that *don\'t* match search string or tag filter'
1970
+ c.switch [:not], default_value: false, negatable: false
1971
+
1972
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
1973
+ c.arg_name 'TYPE'
1974
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1975
+
1764
1976
  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]
1977
+ raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
1766
1978
 
1767
1979
  if options[:tag].nil?
1768
1980
  tags = []
@@ -1779,11 +1991,20 @@ command :last do |c|
1779
1991
 
1780
1992
  end
1781
1993
 
1994
+ options[:case] = options[:case].normalize_case
1995
+
1996
+ search = nil
1997
+
1998
+ if options[:search]
1999
+ search = options[:search]
2000
+ search.sub!(/^'?/, "'") if options[:exact]
2001
+ end
2002
+
1782
2003
  if options[:editor]
1783
- wwid.edit_last(section: options[:s], options: { search: options[:search], tag: tags, tag_bool: options[:bool] })
2004
+ wwid.edit_last(section: options[:s], options: { search: search, case: options[:case], tag: tags, tag_bool: options[:bool], not: options[:not] })
1784
2005
  else
1785
2006
  Doing::Pager::page wwid.last(times: true, section: options[:s],
1786
- options: { search: options[:search], tag: tags, tag_bool: options[:bool] }).strip
2007
+ options: { search: search, case: options[:case], negate: options[:not], tag: tags, tag_bool: options[:bool] }).strip
1787
2008
  end
1788
2009
  end
1789
2010
  end
@@ -1791,7 +2012,7 @@ end
1791
2012
  desc 'List sections'
1792
2013
  command :sections do |c|
1793
2014
  c.desc 'List in single column'
1794
- c.switch %i[c column], default_value: false
2015
+ c.switch %i[c column], negatable: false, default_value: false
1795
2016
 
1796
2017
  c.action do |_global_options, options, _args|
1797
2018
  joiner = options[:c] ? "\n" : "\t"
@@ -1814,7 +2035,7 @@ command :add_section do |c|
1814
2035
  c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file'
1815
2036
 
1816
2037
  c.action do |_global_options, _options, args|
1817
- raise Doing::Errors::InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
2038
+ raise InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
1818
2039
 
1819
2040
  wwid.add_section(args.join(' ').cap_first)
1820
2041
  wwid.write(wwid.doing_file)
@@ -1853,16 +2074,54 @@ command :plugins do |c|
1853
2074
 
1854
2075
  c.desc 'List plugins of type (import, export)'
1855
2076
  c.arg_name 'TYPE'
1856
- c.flag %i[t type], must_match: /^[iea].*$/i, default_value: 'all'
2077
+ c.flag %i[t type], must_match: /^(?:[iea].*)$/i, default_value: 'all'
1857
2078
 
1858
2079
  c.desc 'List in single column for completion'
1859
- c.switch %i[c column], default_value: false
2080
+ c.switch %i[c column], negatable: false, default_value: false
1860
2081
 
1861
2082
  c.action do |_global_options, options, _args|
1862
2083
  Doing::Plugins.list_plugins(options)
1863
2084
  end
1864
2085
  end
1865
2086
 
2087
+ desc 'Generate shell completion scripts'
2088
+ command :completion do |c|
2089
+ c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
2090
+ c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
2091
+ c.example 'doing completion --type fish --file ~/.config/fish/completions/doing.fish', desc: 'Output fish completions to file'
2092
+ c.example 'doing completion --type bash --file ~/.bash_it/completion/enabled/doing.bash', desc: 'Output bash completions to file'
2093
+
2094
+ c.desc 'Shell to generate for (bash, zsh, fish)'
2095
+ c.arg_name 'SHELL'
2096
+ c.flag %i[t type], must_match: /^[bzf](?:[ai]?sh)?$/i, default_value: 'zsh'
2097
+
2098
+ c.desc 'File to write output to'
2099
+ c.arg_name 'PATH'
2100
+ c.flag %i[f file], default_value: 'stdout'
2101
+
2102
+ c.action do |_global_options, options, _args|
2103
+ script_dir = File.join(File.dirname(__FILE__), '..', 'scripts')
2104
+
2105
+ case options[:type]
2106
+ when /^b/
2107
+ result = `ruby #{File.join(script_dir, 'generate_bash_completions.rb')}`
2108
+ when /^z/
2109
+ result = `ruby #{File.join(script_dir, 'generate_zsh_completions.rb')}`
2110
+ when /^f/
2111
+ result = `ruby #{File.join(script_dir, 'generate_fish_completions.rb')}`
2112
+ end
2113
+
2114
+ if options[:file] =~ /^stdout$/i
2115
+ $stdout.puts result
2116
+ else
2117
+ File.open(File.expand_path(options[:file]), 'w') do |f|
2118
+ f.puts result
2119
+ end
2120
+ Doing.logger.warn('File written:', "#{options[:type]} completions written to #{options[:file]}")
2121
+ end
2122
+ end
2123
+ end
2124
+
1866
2125
  desc 'Display a user-created view'
1867
2126
  long_desc 'Command line options override view configuration'
1868
2127
  arg_name 'VIEW_NAME'
@@ -1903,6 +2162,16 @@ command :view do |c|
1903
2162
  c.arg_name 'QUERY'
1904
2163
  c.flag [:search]
1905
2164
 
2165
+ c.desc 'Force exact search string matching (case sensitive)'
2166
+ c.switch %i[x exact], default_value: false, negatable: false
2167
+
2168
+ c.desc 'Show items that *don\'t* match search string'
2169
+ c.switch [:not], default_value: false, negatable: false
2170
+
2171
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
2172
+ c.arg_name 'TYPE'
2173
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2174
+
1906
2175
  c.desc 'Sort tags by (name|time)'
1907
2176
  c.arg_name 'KEY'
1908
2177
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i
@@ -1925,15 +2194,22 @@ command :view do |c|
1925
2194
  c.desc 'Select from a menu of matching entries to perform additional operations'
1926
2195
  c.switch %i[i interactive], negatable: false, default_value: false
1927
2196
 
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)
2197
+ c.action do |global_options, options, args|
2198
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1930
2199
 
1931
- raise Doing::Errors::InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
2200
+ raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
1932
2201
 
1933
2202
  title = if args.empty?
1934
2203
  wwid.choose_view
1935
2204
  else
1936
- wwid.guess_view(args[0])
2205
+ begin
2206
+ wwid.guess_view(args[0])
2207
+ rescue WrongCommand => exception
2208
+ cmd = commands[:show]
2209
+ options[:sort] = 'asc'
2210
+ action = cmd.send(:get_action, nil)
2211
+ return action.call(global_options, options, args)
2212
+ end
1937
2213
  end
1938
2214
 
1939
2215
  if options[:section]
@@ -2023,11 +2299,21 @@ command :view do |c|
2023
2299
  start = wwid.chronify(date_string, guess: :begin)
2024
2300
  finish = false
2025
2301
  end
2026
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
2302
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
2027
2303
  dates = [start, finish]
2028
2304
  end
2029
2305
 
2030
- opts = options
2306
+ options[:case] = options[:case].normalize_case
2307
+
2308
+ search = nil
2309
+
2310
+ if options[:search]
2311
+ search = options[:search]
2312
+ search.sub!(/^'?/, "'") if options[:exact]
2313
+ end
2314
+
2315
+ opts = options.dup
2316
+ opts[:search] = search
2031
2317
  opts[:output] = output_format
2032
2318
  opts[:count] = count
2033
2319
  opts[:format] = date_format
@@ -2046,9 +2332,9 @@ command :view do |c|
2046
2332
 
2047
2333
  Doing::Pager.page wwid.list_section(opts)
2048
2334
  elsif title.instance_of?(FalseClass)
2049
- exit_now! 'Cancelled'
2335
+ raise UserCancelled, 'Cancelled' unless res
2050
2336
  else
2051
- raise Doing::Errors::InvalidView, "View #{title} not found in config"
2337
+ raise InvalidView, "View #{title} not found in config"
2052
2338
  end
2053
2339
  end
2054
2340
  end
@@ -2100,6 +2386,16 @@ command %i[archive move] do |c|
2100
2386
  c.arg_name 'QUERY'
2101
2387
  c.flag [:search]
2102
2388
 
2389
+ c.desc 'Force exact search string matching (case sensitive)'
2390
+ c.switch %i[x exact], default_value: false, negatable: false
2391
+
2392
+ c.desc 'Show items that *don\'t* match search string'
2393
+ c.switch [:not], default_value: false, negatable: false
2394
+
2395
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
2396
+ c.arg_name 'TYPE'
2397
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2398
+
2103
2399
  c.desc 'Archive entries older than date
2104
2400
  (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
2105
2401
  c.arg_name 'DATE_STRING'
@@ -2119,11 +2415,21 @@ command %i[archive move] do |c|
2119
2415
  tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
2120
2416
  end
2121
2417
 
2122
- raise Doing::Errors::InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
2418
+ raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
2123
2419
 
2124
2420
  tags.concat(options[:tag].to_tags) if options[:tag]
2125
2421
 
2126
- opts = options
2422
+ search = nil
2423
+
2424
+ options[:case] = options[:case].normalize_case
2425
+
2426
+ if options[:search]
2427
+ search = options[:search]
2428
+ search.sub!(/^'?/, "'") if options[:exact]
2429
+ end
2430
+
2431
+ opts = options.dup
2432
+ opts[:search] = search
2127
2433
  opts[:bool] = options[:bool].normalize_bool
2128
2434
  opts[:destination] = options[:to]
2129
2435
  opts[:tags] = tags
@@ -2158,6 +2464,16 @@ command :rotate do |c|
2158
2464
  c.arg_name 'QUERY'
2159
2465
  c.flag [:search]
2160
2466
 
2467
+ c.desc 'Force exact search string matching (case sensitive)'
2468
+ c.switch %i[x exact], default_value: false, negatable: false
2469
+
2470
+ c.desc 'Rotate items that *don\'t* match search string or tag filter'
2471
+ c.switch [:not], default_value: false, negatable: false
2472
+
2473
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
2474
+ c.arg_name 'TYPE'
2475
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2476
+
2161
2477
  c.desc 'Rotate entries older than date
2162
2478
  (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
2163
2479
  c.arg_name 'DATE_STRING'
@@ -2170,6 +2486,16 @@ command :rotate do |c|
2170
2486
 
2171
2487
  options[:bool] = options[:bool].normalize_bool
2172
2488
 
2489
+ options[:case] = options[:case].normalize_case
2490
+
2491
+ search = nil
2492
+
2493
+ if options[:search]
2494
+ search = options[:search]
2495
+ search.sub!(/^'?/, "'") if options[:exact]
2496
+ options[:search] = search
2497
+ end
2498
+
2173
2499
  wwid.rotate(options)
2174
2500
  end
2175
2501
  end
@@ -2208,7 +2534,7 @@ command :open do |c|
2208
2534
  system %(open "#{File.expand_path(wwid.doing_file)}")
2209
2535
  end
2210
2536
  else
2211
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
2537
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
2212
2538
 
2213
2539
  system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
2214
2540
  end
@@ -2236,7 +2562,7 @@ command :config do |c|
2236
2562
  c.flag %i[e editor], default_value: nil
2237
2563
 
2238
2564
  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]
2565
+ c.switch %i[d dump], negatable: false
2240
2566
 
2241
2567
  c.desc 'Format for --dump (json|yaml|raw)'
2242
2568
  c.arg_name 'FORMAT'
@@ -2261,7 +2587,6 @@ command :config do |c|
2261
2587
  c.action do |_global_options, options, args|
2262
2588
  if options[:update]
2263
2589
  config.configure({rewrite: true, ignore_local: true})
2264
- # Doing.logger.warn("Config file rewritten: #{config.config_file}")
2265
2590
  return
2266
2591
  end
2267
2592
 
@@ -2276,7 +2601,6 @@ command :config do |c|
2276
2601
  when /^r/
2277
2602
  cfg
2278
2603
  else
2279
- # cfg = { last_key => cfg } unless last_key.nil?
2280
2604
  YAML.dump(cfg)
2281
2605
  end
2282
2606
  else
@@ -2291,7 +2615,7 @@ command :config do |c|
2291
2615
  choices.concat(config.additional_configs)
2292
2616
  res = wwid.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to edit > ')
2293
2617
 
2294
- raise Doing::Errors::UserCancelled, 'Cancelled' unless res
2618
+ raise UserCancelled, 'Cancelled' unless res
2295
2619
 
2296
2620
  config_file = res.strip || config.config_file
2297
2621
  else
@@ -2308,7 +2632,7 @@ command :config do |c|
2308
2632
  `open -a "#{editor}" "#{config_file}"`
2309
2633
  end
2310
2634
  else
2311
- raise Doing::Errors::InvalidArgument, 'No viable editor found in config or environment.'
2635
+ raise InvalidArgument, 'No viable editor found in config or environment.'
2312
2636
  end
2313
2637
  elsif options[:a] || options[:b]
2314
2638
  if options[:a]
@@ -2319,7 +2643,7 @@ command :config do |c|
2319
2643
  else
2320
2644
  editor = options[:e] || Doing::Util.find_default_editor('config')
2321
2645
 
2322
- raise Doing::Errors::MissingEditor, 'No viable editor defined in config or environment' unless editor
2646
+ raise MissingEditor, 'No viable editor defined in config or environment' unless editor
2323
2647
 
2324
2648
  if Doing::Util.exec_available(editor)
2325
2649
  system %(#{editor} "#{config_file}")
@@ -2329,7 +2653,7 @@ command :config do |c|
2329
2653
  end
2330
2654
  else
2331
2655
  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)
2656
+ raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
2333
2657
 
2334
2658
  system %(#{editor} "#{config_file}")
2335
2659
  end
@@ -2360,6 +2684,16 @@ command :import do |c|
2360
2684
  c.arg_name 'QUERY'
2361
2685
  c.flag [:search]
2362
2686
 
2687
+ c.desc 'Force exact search string matching (case sensitive)'
2688
+ c.switch %i[x exact], default_value: false, negatable: false
2689
+
2690
+ c.desc 'Import items that *don\'t* match search/tag/date filters'
2691
+ c.switch [:not], default_value: false, negatable: false
2692
+
2693
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
2694
+ c.arg_name 'TYPE'
2695
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2696
+
2363
2697
  c.desc 'Only import items with recorded time intervals'
2364
2698
  c.switch [:only_timed], default_value: false, negatable: false
2365
2699
 
@@ -2413,17 +2747,19 @@ command :import do |c|
2413
2747
  start = wwid.chronify(date_string, guess: :begin)
2414
2748
  finish = false
2415
2749
  end
2416
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
2750
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
2417
2751
  dates = [start, finish]
2418
2752
  end
2419
2753
 
2754
+ options[:case] = options[:case].normalize_case
2755
+
2420
2756
  if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
2421
2757
  options[:no_overlap] = !options[:overlap]
2422
2758
  options[:date_filter] = dates
2423
2759
  wwid.import(args, options)
2424
2760
  wwid.write(wwid.doing_file)
2425
2761
  else
2426
- raise Doing::Errors::InvalidPluginType, "Invalid import type: #{options[:type]}"
2762
+ raise InvalidPluginType, "Invalid import type: #{options[:type]}"
2427
2763
  end
2428
2764
  end
2429
2765
  end
@@ -2448,14 +2784,13 @@ pre do |global, _command, _options, _args|
2448
2784
  end
2449
2785
 
2450
2786
  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
2787
+ if exception.kind_of?(SystemExit)
2788
+ false
2789
+ else
2790
+ # Doing.logger.error('Fatal:', exception)
2791
+ Doing.logger.output_results
2792
+ true
2793
+ end
2459
2794
  end
2460
2795
 
2461
2796
  post do |global, _command, _options, _args|