doing 2.1.16 → 2.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +13 -12
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +136 -53
  6. data/Gemfile.lock +11 -11
  7. data/README.md +1 -1
  8. data/Rakefile +10 -4
  9. data/bin/doing +146 -169
  10. data/docs/doc/Array.html +3 -3
  11. data/docs/doc/BooleanTermParser/Clause.html +3 -3
  12. data/docs/doc/BooleanTermParser/Operator.html +3 -3
  13. data/docs/doc/BooleanTermParser/Query.html +3 -3
  14. data/docs/doc/BooleanTermParser/QueryParser.html +3 -3
  15. data/docs/doc/BooleanTermParser/QueryTransformer.html +3 -3
  16. data/docs/doc/BooleanTermParser.html +3 -3
  17. data/docs/doc/Doing/Color.html +8 -4
  18. data/docs/doc/Doing/Completion.html +3 -3
  19. data/docs/doc/Doing/Configuration.html +7 -5
  20. data/docs/doc/Doing/Errors/DoingNoTraceError.html +3 -3
  21. data/docs/doc/Doing/Errors/DoingRuntimeError.html +3 -3
  22. data/docs/doc/Doing/Errors/DoingStandardError.html +3 -3
  23. data/docs/doc/Doing/Errors/EmptyInput.html +3 -3
  24. data/docs/doc/Doing/Errors/NoResults.html +3 -3
  25. data/docs/doc/Doing/Errors/PluginException.html +3 -3
  26. data/docs/doc/Doing/Errors/UserCancelled.html +3 -3
  27. data/docs/doc/Doing/Errors/WrongCommand.html +3 -3
  28. data/docs/doc/Doing/Errors.html +3 -3
  29. data/docs/doc/Doing/Hooks.html +3 -3
  30. data/docs/doc/Doing/Item.html +121 -3
  31. data/docs/doc/Doing/Items.html +3 -3
  32. data/docs/doc/Doing/LogAdapter.html +3 -3
  33. data/docs/doc/Doing/Note.html +3 -3
  34. data/docs/doc/Doing/Pager.html +3 -3
  35. data/docs/doc/Doing/Plugins.html +3 -3
  36. data/docs/doc/Doing/Prompt.html +3 -3
  37. data/docs/doc/Doing/Section.html +3 -3
  38. data/docs/doc/Doing/TemplateString.html +4 -4
  39. data/docs/doc/Doing/Util/Backup.html +3 -3
  40. data/docs/doc/Doing/Util.html +3 -3
  41. data/docs/doc/Doing/WWID.html +66 -8
  42. data/docs/doc/Doing.html +4 -4
  43. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +3 -3
  44. data/docs/doc/GLI/Commands.html +3 -3
  45. data/docs/doc/GLI.html +3 -3
  46. data/docs/doc/Hash.html +3 -3
  47. data/docs/doc/Numeric.html +3 -3
  48. data/docs/doc/PhraseParser/Operator.html +3 -3
  49. data/docs/doc/PhraseParser/PhraseClause.html +3 -3
  50. data/docs/doc/PhraseParser/Query.html +3 -3
  51. data/docs/doc/PhraseParser/QueryParser.html +3 -3
  52. data/docs/doc/PhraseParser/QueryTransformer.html +3 -3
  53. data/docs/doc/PhraseParser/TermClause.html +3 -3
  54. data/docs/doc/PhraseParser.html +3 -3
  55. data/docs/doc/Status.html +3 -3
  56. data/docs/doc/String.html +230 -17
  57. data/docs/doc/Symbol.html +3 -3
  58. data/docs/doc/Time.html +3 -3
  59. data/docs/doc/_index.html +4 -4
  60. data/docs/doc/file.README.html +4 -4
  61. data/docs/doc/frames.html +1 -1
  62. data/docs/doc/index.html +4 -4
  63. data/docs/doc/method_list.html +311 -239
  64. data/docs/doc/top-level-namespace.html +94 -3
  65. data/doing.gemspec +1 -1
  66. data/doing.rdoc +35 -6
  67. data/lib/completion/_doing.zsh +10 -10
  68. data/lib/completion/doing.bash +16 -16
  69. data/lib/completion/doing.fish +97 -15
  70. data/lib/doing/colors.rb +4 -0
  71. data/lib/doing/completion/fish_completion.rb +80 -11
  72. data/lib/doing/configuration.rb +3 -1
  73. data/lib/doing/hash.rb +1 -1
  74. data/lib/doing/item.rb +51 -0
  75. data/lib/doing/items.rb +3 -1
  76. data/lib/doing/log_adapter.rb +2 -2
  77. data/lib/doing/pager.rb +1 -1
  78. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  79. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  80. data/lib/doing/plugins/export/template_export.rb +2 -0
  81. data/lib/doing/prompt.rb +16 -5
  82. data/lib/doing/string.rb +54 -0
  83. data/lib/doing/string_chronify.rb +55 -17
  84. data/lib/doing/types.rb +19 -0
  85. data/lib/doing/version.rb +1 -1
  86. data/lib/doing/wwid.rb +80 -52
  87. data/lib/examples/commands/later.rb +32 -0
  88. data/lib/helpers/threaded_tests.rb +250 -0
  89. metadata +9 -6
data/bin/doing CHANGED
@@ -4,6 +4,7 @@
4
4
  $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
5
5
  require 'gli'
6
6
  require 'doing'
7
+ require 'doing/types'
7
8
  require 'tempfile'
8
9
  require 'pp'
9
10
 
@@ -25,12 +26,13 @@ version Doing::VERSION
25
26
  hide_commands_without_desc true
26
27
  autocomplete_commands true
27
28
 
28
- REGEX_BOOL = /^(?:and|all|any|or|not|none|p(?:at(?:tern)?)?)$/i
29
- REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i
30
- REGEX_VALUE_QUERY = /^(?:!)?@?(?:\S+) +(?:!?[<>=][=*]?|[$*^]=) +(?:.*?)$/
31
-
32
29
  InvalidExportType = Class.new(RuntimeError)
33
30
  MissingConfigFile = Class.new(RuntimeError)
31
+ TagArray = Class.new(Array)
32
+ DateBeginString = Class.new(DateTime)
33
+ DateEndString = Class.new(DateTime)
34
+ DateRangeString = Class.new(Array)
35
+ DateIntervalString = Class.new(DateTime)
34
36
 
35
37
  colors = Doing::Color
36
38
  wwid = Doing::WWID.new
@@ -70,7 +72,42 @@ if settings.dig('plugins', 'command_path')
70
72
  commands_from File.expand_path(settings.dig('plugins', 'command_path'))
71
73
  end
72
74
 
73
- class TagArray < Array; end
75
+ accept DateBeginString do |value|
76
+ if value =~ REGEX_TIME
77
+ res = value
78
+ else
79
+ res = value.chronify(guess: :begin, future: false)
80
+ end
81
+ raise InvalidTimeExpression, 'Invalid start date' unless res
82
+
83
+ res
84
+ end
85
+
86
+ accept DateEndString do |value|
87
+ if value =~ REGEX_TIME
88
+ res = value
89
+ else
90
+ res = value.chronify(guess: :end, future: false)
91
+ end
92
+ raise InvalidTimeExpression, 'Invalid end date' unless res
93
+
94
+ res
95
+ end
96
+
97
+ accept DateRangeString do |value|
98
+ start, finish = value.split_date_range
99
+ raise InvalidTimeExpression, 'Invalid range' unless start
100
+
101
+ finish ||= Time.now
102
+ [start, finish]
103
+ end
104
+
105
+ accept DateIntervalString do |value|
106
+ res = value.chronify_qty
107
+ raise InvalidTimeExpression, 'Invalid time quantity' unless res
108
+
109
+ res
110
+ end
74
111
 
75
112
  accept TagArray do |value|
76
113
  value.gsub(/[, ]+/, ' ').split(' ').map { |tag| tag.sub(/^@/, '')}.map(&:strip)
@@ -143,6 +180,10 @@ command %i[again resume] do |c|
143
180
  c.arg_name 'SECTION_NAME'
144
181
  c.flag [:in]
145
182
 
183
+ c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
184
+ c.arg_name 'DATE_STRING'
185
+ c.flag %i[b back started], type: DateBeginString
186
+
146
187
  c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?)'
147
188
  c.arg_name 'TAG'
148
189
  c.flag [:tag], type: TagArray
@@ -198,6 +239,13 @@ command %i[again resume] do |c|
198
239
  options[:search] = search
199
240
  end
200
241
 
242
+ if options[:back]
243
+ date = options[:back]
244
+ raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
245
+ else
246
+ date = Time.now
247
+ end
248
+
201
249
  note = Doing::Note.new(options[:note])
202
250
  note.add(Doing::Prompt.read_lines(prompt: 'Add a note')) if options[:ask]
203
251
 
@@ -208,6 +256,7 @@ command %i[again resume] do |c|
208
256
  opts[:tag] = tags
209
257
  opts[:tag_bool] = options[:bool].normalize_bool
210
258
  opts[:interactive] = options[:interactive]
259
+ opts[:date] = date
211
260
 
212
261
  wwid.repeat_last(opts)
213
262
  end
@@ -340,24 +389,24 @@ command %i[done did] do |c|
340
389
  c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm).
341
390
  Used with --took, backdates start date)
342
391
  c.arg_name 'DATE_STRING'
343
- c.flag %i[at finished]
392
+ c.flag %i[at finished], type: DateEndString
344
393
 
345
394
  c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
346
395
  c.arg_name 'DATE_STRING'
347
- c.flag %i[b back started]
396
+ c.flag %i[b back started], type: DateBeginString
348
397
 
349
398
  c.desc %(
350
399
  Start and end times as a date/time range `doing done --from "1am to 8am"`.
351
400
  Overrides other date flags.
352
401
  )
353
402
  c.arg_name 'TIME_RANGE'
354
- c.flag [:from]
403
+ c.flag [:from], must_match: REGEX_RANGE
355
404
 
356
405
  c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM).
357
406
  If used without the --back option, the start date will be moved back to allow
358
407
  the completion date to be the current time.)
359
408
  c.arg_name 'INTERVAL'
360
- c.flag %i[t took for]
409
+ c.flag %i[t took for], type: DateIntervalString
361
410
 
362
411
  c.desc 'Section'
363
412
  c.arg_name 'NAME'
@@ -385,23 +434,27 @@ command %i[done did] do |c|
385
434
  donedate = nil
386
435
 
387
436
  if options[:from]
388
- date, finish_date = options[:from].split_date_range
437
+ options[:from] = options[:from].split(/#{REGEX_RANGE_INDICATOR}/).map do |time|
438
+ time =~ REGEX_TIME ? "today #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}" : time
439
+ end.join(' to ').split_date_range
440
+ date, finish_date = options[:from]
389
441
  finish_date ||= Time.now
390
442
  else
391
443
  if options[:took]
392
- took = options[:took].chronify_qty
444
+ took = options[:took]
393
445
  raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
394
446
  end
395
447
 
396
448
  if options[:back]
397
- date = options[:back].chronify(guess: :begin)
449
+ date = options[:back]
398
450
  raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
399
451
  else
400
452
  date = options[:took] ? Time.now - took : Time.now
401
453
  end
402
454
 
403
455
  if options[:at]
404
- finish_date = options[:at].chronify(guess: :begin)
456
+ finish_date = options[:at]
457
+ finish_date = finish_date.chronify(guess: :begin) if finish_date.is_a? String
405
458
  raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
406
459
 
407
460
  if options[:took]
@@ -417,6 +470,7 @@ command %i[done did] do |c|
417
470
  end
418
471
 
419
472
  if options[:date]
473
+ date = date.chronify(guess: :begin, context: :today) if date =~ REGEX_TIME
420
474
  finish_date = wwid.verify_duration(date, finish_date) unless options[:took] || options[:from]
421
475
 
422
476
  donedate = finish_date.strftime('%F %R')
@@ -528,7 +582,7 @@ command %i[done did] do |c|
528
582
  wwid.content.push(new_entry)
529
583
  Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
530
584
  wwid.write(wwid.doing_file)
531
- Doing.logger.info('Entry Added:', new_entry.title)
585
+ Doing.logger.info('New entry:', %(added "#{new_entry.date.relative_date}: #{new_entry.title}" to #{section}))
532
586
  elsif $stdin.stat.size.positive?
533
587
  note = Doing::Note.new(options[:note])
534
588
  d, title, note = wwid.format_input($stdin.read.strip)
@@ -553,7 +607,7 @@ command %i[done did] do |c|
553
607
  Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
554
608
 
555
609
  wwid.write(wwid.doing_file)
556
- Doing.logger.info('Entry Added:', new_entry.title)
610
+ Doing.logger.info('New entry:', %(added "#{new_entry.date.relative_date}: #{new_entry.title}" to #{section}))
557
611
  else
558
612
  raise EmptyInput, 'You must provide content when creating a new entry'
559
613
  end
@@ -574,15 +628,15 @@ command :finish do |c|
574
628
 
575
629
  c.desc 'Backdate completed date to date string [4pm|20m|2h|yesterday noon]'
576
630
  c.arg_name 'DATE_STRING'
577
- c.flag %i[b back]
631
+ c.flag %i[b back started], type: DateBeginString
578
632
 
579
633
  c.desc 'Set the completed date to the start date plus XX[hmd]'
580
634
  c.arg_name 'INTERVAL'
581
- c.flag %i[t took for]
635
+ c.flag %i[t took for], type: DateIntervalString
582
636
 
583
637
  c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.)
584
638
  c.arg_name 'DATE_STRING'
585
- c.flag [:at]
639
+ c.flag %i[at finished], type: DateEndString
586
640
 
587
641
  c.desc 'Finish the last X entries containing TAG.
588
642
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
@@ -639,7 +693,7 @@ command :finish do |c|
639
693
  options[:fuzzy] = false
640
694
  unless options[:auto]
641
695
  if options[:took]
642
- took = options[:took].chronify_qty
696
+ took = options[:took]
643
697
  raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
644
698
  end
645
699
 
@@ -648,12 +702,13 @@ command :finish do |c|
648
702
  raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
649
703
 
650
704
  if options[:at]
651
- finish_date = options[:at].chronify(guess: :begin)
705
+ finish_date = options[:at]
706
+ finish_date = finish_date.chronify(guess: :begin) if finish_date.is_a? String
652
707
  raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
653
708
 
654
709
  date = options[:took] ? finish_date - took : finish_date
655
710
  elsif options[:back]
656
- date = options[:back].chronify()
711
+ date = options[:back]
657
712
 
658
713
  raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
659
714
  else
@@ -661,8 +716,6 @@ command :finish do |c|
661
716
  end
662
717
  end
663
718
 
664
- options[:took] = options[:took].chronify_qty if options[:took]
665
-
666
719
  if options[:tag].nil?
667
720
  tags = []
668
721
  else
@@ -711,92 +764,6 @@ command :finish do |c|
711
764
  end
712
765
  end
713
766
 
714
- # @@later
715
- desc 'Add an item to the Later section'
716
- arg_name 'ENTRY'
717
- command :later do |c|
718
- c.example 'doing later "Something I\'ll think about tomorrow"', desc: 'Add an entry to the Later section'
719
- c.example 'doing later -e', desc: 'Open $EDITOR to create an entry and optional note'
720
-
721
- c.desc "Edit entry with #{Doing::Util.default_editor}"
722
- c.switch %i[e editor], negatable: false, default_value: false
723
-
724
- c.desc 'Backdate start time to date string [4pm|20m|2h|yesterday noon]'
725
- c.arg_name 'DATE_STRING'
726
- c.flag %i[b back]
727
-
728
- c.desc 'Note'
729
- c.arg_name 'TEXT'
730
- c.flag %i[n note]
731
-
732
- c.desc 'Prompt for note via multi-line input'
733
- c.switch %i[ask], negatable: false, default_value: false
734
-
735
- c.action do |_global_options, options, args|
736
- if options[:back]
737
- date = options[:back].chronify(guess: :begin)
738
- raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
739
- else
740
- date = Time.now
741
- end
742
-
743
- ask_note = options[:ask] && !options[:editor] && args.count.positive? ? Doing::Prompt.read_lines(prompt: 'Add a note') : ''
744
-
745
- if options[:editor]
746
- raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
747
-
748
- input = ''
749
- input += date.strftime('%F %R | ')
750
- input += args.empty? ? '' : args.join(' ')
751
- input += "\n#{options[:note]}" if options[:note]
752
- input += "\n#{ask_note}" unless ask_note.empty?
753
-
754
- input = wwid.fork_editor(input).strip
755
-
756
- d, title, note = wwid.format_input(input)
757
- raise EmptyInput, 'No content' if title.empty?
758
-
759
- note.add(options[:note]) if options[:note]
760
- if ask_note.empty? && options[:ask]
761
- ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
762
- note.add(ask_note) unless ask_note.empty?
763
- end
764
-
765
- date = d.nil? ? date : d
766
- wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
767
- wwid.write(wwid.doing_file)
768
- elsif !args.empty?
769
- d, title, note = wwid.format_input(args.join(' '))
770
- date = d.nil? ? date : d
771
- note.add(options[:note]) if options[:note]
772
- note.add(ask_note) unless ask_note.empty?
773
- wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
774
- wwid.write(wwid.doing_file)
775
- elsif $stdin.stat.size.positive?
776
- d, title, note = wwid.format_input($stdin.read)
777
- unless d.nil?
778
- Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
779
- date = d
780
- end
781
- note.add(options[:note]) if options[:note]
782
- note.add(ask_note) unless ask_note.empty?
783
- wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
784
- wwid.write(wwid.doing_file)
785
- else
786
- title = Doing::Prompt.read_line(prompt: 'Entry content')
787
- raise EmptyInput, 'You must provide content when creating a new entry' if title.strip.empty?
788
-
789
- note = Doing::Note.new
790
- res = Doing::Prompt.yn('Add a note', default_response: false)
791
- ask_note = res ? Doing::Prompt.read_lines(prompt: 'Enter note') : []
792
- note.add(ask_note)
793
-
794
- wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
795
- wwid.write(wwid.doing_file)
796
- end
797
- end
798
- end
799
-
800
767
  # @@mark @@flag
801
768
  desc 'Mark last entry as flagged'
802
769
  command %i[mark flag] do |c|
@@ -949,7 +916,7 @@ command :meanwhile do |c|
949
916
 
950
917
  c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]'
951
918
  c.arg_name 'DATE_STRING'
952
- c.flag %i[b back]
919
+ c.flag %i[b back started], type: DateBeginString
953
920
 
954
921
  c.desc 'Note'
955
922
  c.arg_name 'TEXT'
@@ -960,7 +927,7 @@ command :meanwhile do |c|
960
927
 
961
928
  c.action do |_global_options, options, args|
962
929
  if options[:back]
963
- date = options[:back].chronify(guess: :begin)
930
+ date = options[:back]
964
931
 
965
932
  raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
966
933
  else
@@ -1169,7 +1136,7 @@ command %i[now next] do |c|
1169
1136
 
1170
1137
  c.desc 'Backdate start time [4pm|20m|2h|"yesterday noon"]'
1171
1138
  c.arg_name 'DATE_STRING'
1172
- c.flag %i[b back started]
1139
+ c.flag %i[b back started], type: DateBeginString
1173
1140
 
1174
1141
  c.desc 'Timed entry, marks last entry in section as @done'
1175
1142
  c.switch %i[f finish_last], negatable: false, default_value: false
@@ -1187,7 +1154,7 @@ command %i[now next] do |c|
1187
1154
 
1188
1155
  c.action do |_global_options, options, args|
1189
1156
  if options[:back]
1190
- date = options[:back].chronify(guess: :begin)
1157
+ date = options[:back]
1191
1158
 
1192
1159
  raise InvalidTimeExpression.new('unable to parse date string', topic: 'Parser:') if date.nil?
1193
1160
  else
@@ -1211,7 +1178,7 @@ command %i[now next] do |c|
1211
1178
  input += "\n#{ask_note}" unless ask_note.empty?
1212
1179
  input = wwid.fork_editor(input).strip
1213
1180
 
1214
- date, title, note = wwid.format_input(input)
1181
+ d, title, note = wwid.format_input(input)
1215
1182
  raise EmptyInput, 'No content' if title.strip.empty?
1216
1183
 
1217
1184
  if ask_note.empty? && options[:ask]
@@ -1219,6 +1186,7 @@ command %i[now next] do |c|
1219
1186
  note.add(ask_note) unless ask_note.empty?
1220
1187
  end
1221
1188
 
1189
+ date = d.nil? ? date : d
1222
1190
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1223
1191
  wwid.write(wwid.doing_file)
1224
1192
  elsif args.length.positive?
@@ -1236,14 +1204,20 @@ command %i[now next] do |c|
1236
1204
  date = d
1237
1205
  end
1238
1206
  note.add(options[:note]) if options[:note]
1239
- note.add(ask_note) unless ask_note.empty?
1207
+ if ask_note.empty? && options[:ask]
1208
+ ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
1209
+ note.add(ask_note) unless ask_note.empty?
1210
+ end
1240
1211
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1241
1212
  wwid.write(wwid.doing_file)
1242
1213
  else
1243
- title = Doing::Prompt.read_line(prompt: 'Entry content')
1214
+ tags = wwid.all_tags(wwid.content)
1215
+ $stderr.puts Doing::Color.boldgreen("Add a new entry. Tab will autocomplete known tags. Ctrl-c to cancel.")
1216
+ title = Doing::Prompt.read_line(prompt: 'Entry content', completions: tags)
1244
1217
  raise EmptyInput, 'You must provide content when creating a new entry' if title.strip.empty?
1245
1218
 
1246
1219
  note = Doing::Note.new
1220
+ note.add(options[:note]) if options[:note]
1247
1221
  res = Doing::Prompt.yn('Add a note', default_response: false)
1248
1222
  ask_note = res ? Doing::Prompt.read_lines(prompt: 'Enter note') : []
1249
1223
  note.add(ask_note)
@@ -1410,11 +1384,11 @@ command :select do |c|
1410
1384
 
1411
1385
  c.desc 'Select from entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
1412
1386
  c.arg_name 'DATE_STRING'
1413
- c.flag [:before]
1387
+ c.flag [:before], type: DateBeginString
1414
1388
 
1415
1389
  c.desc 'Select from entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
1416
1390
  c.arg_name 'DATE_STRING'
1417
- c.flag [:after]
1391
+ c.flag [:after], type: DateEndString
1418
1392
 
1419
1393
  c.desc %(
1420
1394
  Date range to show, or a single day to filter date on.
@@ -1425,7 +1399,7 @@ command :select do |c|
1425
1399
  by time of day.
1426
1400
  )
1427
1401
  c.arg_name 'DATE_OR_RANGE'
1428
- c.flag [:from]
1402
+ c.flag [:from], type: DateRangeString
1429
1403
 
1430
1404
  c.desc 'Force exact search string matching (case sensitive)'
1431
1405
  c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
@@ -1692,11 +1666,11 @@ command %i[grep search] do |c|
1692
1666
 
1693
1667
  c.desc 'Search entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
1694
1668
  c.arg_name 'DATE_STRING'
1695
- c.flag [:before]
1669
+ c.flag [:before], type: DateBeginString
1696
1670
 
1697
1671
  c.desc 'Search entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
1698
1672
  c.arg_name 'DATE_STRING'
1699
- c.flag [:after]
1673
+ c.flag [:after], type: DateEndString
1700
1674
 
1701
1675
  c.desc %(
1702
1676
  Date range to show, or a single day to filter date on.
@@ -1707,7 +1681,7 @@ command %i[grep search] do |c|
1707
1681
  by time of day.
1708
1682
  )
1709
1683
  c.arg_name 'DATE_OR_RANGE'
1710
- c.flag [:from]
1684
+ c.flag [:from], type: DateRangeString
1711
1685
 
1712
1686
  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1713
1687
  c.arg_name 'FORMAT'
@@ -1744,6 +1718,9 @@ command %i[grep search] do |c|
1744
1718
  c.arg_name 'TYPE'
1745
1719
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1746
1720
 
1721
+ c.desc "Highlight search matches in output. Only affects command line output"
1722
+ c.switch %i[h hilite], default_value: settings.dig('search', 'highlight')
1723
+
1747
1724
  c.desc "Edit matching entries with #{Doing::Util.default_editor}"
1748
1725
  c.switch %i[e editor], negatable: false, default_value: false
1749
1726
 
@@ -1821,6 +1798,9 @@ command :last do |c|
1821
1798
  c.arg_name 'QUERY'
1822
1799
  c.flag [:search]
1823
1800
 
1801
+ c.desc "Highlight search matches in output. Only affects command line output"
1802
+ c.switch %i[h hilite], default_value: settings.dig('search', 'highlight')
1803
+
1824
1804
  c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1825
1805
  c.arg_name 'QUERY'
1826
1806
  c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
@@ -1875,6 +1855,7 @@ command :last do |c|
1875
1855
  search: options[:search],
1876
1856
  fuzzy: options[:fuzzy],
1877
1857
  case: options[:case],
1858
+ hilite: options[:hilite],
1878
1859
  negate: options[:not],
1879
1860
  tag: options[:tag],
1880
1861
  tag_bool: options[:bool],
@@ -1998,11 +1979,11 @@ command :show do |c|
1998
1979
 
1999
1980
  c.desc 'Show entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
2000
1981
  c.arg_name 'DATE_STRING'
2001
- c.flag [:before]
1982
+ c.flag [:before], type: DateBeginString
2002
1983
 
2003
1984
  c.desc 'Show entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
2004
1985
  c.arg_name 'DATE_STRING'
2005
- c.flag [:after]
1986
+ c.flag [:after], type: DateEndString
2006
1987
 
2007
1988
  c.desc %(
2008
1989
  Date range to show, or a single day to filter date on.
@@ -2014,12 +1995,15 @@ command :show do |c|
2014
1995
  )
2015
1996
 
2016
1997
  c.arg_name 'DATE_OR_RANGE'
2017
- c.flag [:from]
1998
+ c.flag [:from], type: DateRangeString
2018
1999
 
2019
2000
  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2020
2001
  c.arg_name 'QUERY'
2021
2002
  c.flag [:search]
2022
2003
 
2004
+ c.desc "Highlight search matches in output. Only affects command line output"
2005
+ c.switch %i[h hilite], default_value: settings.dig('search', 'highlight')
2006
+
2023
2007
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
2024
2008
  # c.switch [:fuzzy], default_value: false, negatable: false
2025
2009
 
@@ -2167,6 +2151,7 @@ command :show do |c|
2167
2151
  opt[:sort_tags] = options[:tag_sort] =~ /^n/i
2168
2152
  opt[:count] = options[:count].to_i
2169
2153
  opt[:highlight] = true
2154
+ opt[:hilite] = options[:hilite]
2170
2155
  opt[:order] = options[:sort].normalize_order
2171
2156
  opt[:tag] = nil
2172
2157
  opt[:tag_order] = options[:tag_order].normalize_order
@@ -2305,8 +2290,8 @@ command :today do |c|
2305
2290
  c.desc %(
2306
2291
  Time range to show `doing today --from "12pm to 4pm"`
2307
2292
  )
2308
- c.arg_name 'DATE_OR_RANGE'
2309
- c.flag [:from]
2293
+ c.arg_name 'TIME_RANGE'
2294
+ c.flag [:from], type: DateRangeString
2310
2295
 
2311
2296
  c.action do |_global_options, options, _args|
2312
2297
  raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
@@ -2319,7 +2304,7 @@ command :today do |c|
2319
2304
  end
2320
2305
  end
2321
2306
 
2322
- # @on
2307
+ # @@on
2323
2308
  desc 'List entries for a date'
2324
2309
  long_desc %(Date argument can be natural language. "thursday" would be interpreted as "last thursday,"
2325
2310
  and "2d" would be interpreted as "two days ago." If you use "to" or "through" between two dates,
@@ -2358,16 +2343,11 @@ command :on do |c|
2358
2343
 
2359
2344
  raise MissingArgument, 'Missing date argument' if args.empty?
2360
2345
 
2361
- date_string = args.join(' ')
2362
-
2363
- if date_string =~ / (to|through|thru) /
2364
- dates = date_string.split(/ (to|through|thru) /)
2365
- start = dates[0].chronify(guess: :begin)
2366
- finish = dates[2].chronify(guess: :end)
2367
- else
2368
- start = date_string.chronify(guess: :begin)
2369
- finish = false
2346
+ date_string = args.join(' ').strip
2347
+ if date_string =~ /^tod(?:ay)?/i
2348
+ date_string = 'today to tomorrow 12am'
2370
2349
  end
2350
+ start, finish = date_string.split_date_range
2371
2351
 
2372
2352
  raise InvalidTimeExpression, 'Unrecognized date string' unless start
2373
2353
 
@@ -2493,6 +2473,9 @@ command :view do |c|
2493
2473
  c.arg_name 'QUERY'
2494
2474
  c.flag [:search]
2495
2475
 
2476
+ c.desc "Highlight search matches in output. Only affects command line output"
2477
+ c.switch %i[h hilite], default_value: settings.dig('search', 'highlight')
2478
+
2496
2479
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
2497
2480
  # c.switch [:fuzzy], default_value: false, negatable: false
2498
2481
 
@@ -2516,11 +2499,11 @@ command :view do |c|
2516
2499
 
2517
2500
  c.desc 'View entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
2518
2501
  c.arg_name 'DATE_STRING'
2519
- c.flag [:before]
2502
+ c.flag [:before], type: DateBeginString
2520
2503
 
2521
2504
  c.desc 'View entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
2522
2505
  c.arg_name 'DATE_STRING'
2523
- c.flag [:after]
2506
+ c.flag [:after], type: DateEndString
2524
2507
 
2525
2508
  c.desc %(
2526
2509
  Date range to show, or a single day to filter date on.
@@ -2531,7 +2514,7 @@ command :view do |c|
2531
2514
  by time of day.
2532
2515
  )
2533
2516
  c.arg_name 'DATE_OR_RANGE'
2534
- c.flag [:from]
2517
+ c.flag [:from], type: DateRangeString
2535
2518
 
2536
2519
  c.desc 'Only show items with recorded time intervals (override view settings)'
2537
2520
  c.switch [:only_timed], default_value: false, negatable: false
@@ -2649,10 +2632,10 @@ command :view do |c|
2649
2632
 
2650
2633
  opts = options.dup
2651
2634
  opts[:age] = options[:age].normalize_age(:newest)
2652
- opts[:view_template] = title
2653
2635
  opts[:count] = count
2654
2636
  opts[:format] = date_format
2655
2637
  opts[:highlight] = options[:color]
2638
+ opts[:hilite] = options[:hilite]
2656
2639
  opts[:only_timed] = only_timed
2657
2640
  opts[:order] = order
2658
2641
  opts[:output] = options[:interactive] ? nil : options[:output]
@@ -2665,6 +2648,7 @@ command :view do |c|
2665
2648
  opts[:tags_color] = tags_color
2666
2649
  opts[:template] = template
2667
2650
  opts[:totals] = totals
2651
+ opts[:view_template] = title
2668
2652
 
2669
2653
  Doing::Pager.page wwid.list_section(opts)
2670
2654
  elsif title.instance_of?(FalseClass)
@@ -2714,11 +2698,9 @@ command :yesterday do |c|
2714
2698
  c.arg_name 'TIME_STRING'
2715
2699
  c.flag [:after]
2716
2700
 
2717
- c.desc %(
2718
- Time range to show, e.g. `doing yesterday --from "1am to 8am"`
2719
- )
2701
+ c.desc 'Time range to show, e.g. `doing yesterday --from "1am to 8am"`'
2720
2702
  c.arg_name 'TIME_RANGE'
2721
- c.flag [:from]
2703
+ c.flag [:from], must_match: REGEX_TIME_RANGE
2722
2704
 
2723
2705
  c.desc 'Tag sort direction (asc|desc)'
2724
2706
  c.arg_name 'DIRECTION'
@@ -2730,9 +2712,9 @@ command :yesterday do |c|
2730
2712
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
2731
2713
 
2732
2714
  if options[:from]
2733
- options[:from] = options[:from].split(/ (?:to|through|thru|(?:un)?til|-+) /).map do |time|
2715
+ options[:from] = options[:from].split(/#{REGEX_RANGE_INDICATOR}/).map do |time|
2734
2716
  "yesterday #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}"
2735
- end.join(' to ')
2717
+ end.join(' to ').split_date_range
2736
2718
  end
2737
2719
 
2738
2720
  opt = {
@@ -2970,8 +2952,7 @@ command :config do |c|
2970
2952
  value = options[:remove] ? nil : args.pop
2971
2953
  keypath = args.join('.')
2972
2954
  real_path = config.resolve_key_path(keypath, create: true)
2973
-
2974
- old_value = settings.dig(*real_path) || nil
2955
+ old_value = settings.dig(*real_path)
2975
2956
  old_type = old_value&.class.to_s || nil
2976
2957
 
2977
2958
  if old_value.is_a?(Hash) && !options[:remove]
@@ -2997,7 +2978,6 @@ command :config do |c|
2997
2978
  else
2998
2979
  current_value = cfg.dig(*real_path)
2999
2980
  cfg.deep_set(real_path, value.set_type(old_type))
3000
-
3001
2981
  $stderr.puts "#{' Key path:'.yellow} #{real_path.join('->').boldwhite}"
3002
2982
  $stderr.puts "#{'Inherited:'.yellow} #{(old_value ? old_value.to_s : 'empty').boldwhite}"
3003
2983
  $stderr.puts "#{' Current:'.yellow} #{ (current_value ? current_value.to_s : 'empty').boldwhite }"
@@ -3157,7 +3137,7 @@ command %i[archive move] do |c|
3157
3137
  c.desc 'Archive entries older than date
3158
3138
  (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
3159
3139
  c.arg_name 'DATE_STRING'
3160
- c.flag [:before]
3140
+ c.flag [:before], type: DateEndString
3161
3141
 
3162
3142
  c.action do |_global_options, options, args|
3163
3143
  options[:fuzzy] = false
@@ -3248,11 +3228,11 @@ command :import do |c|
3248
3228
  # TODO: Allow time range filtering
3249
3229
  c.desc 'Import entries older than date'
3250
3230
  c.arg_name 'DATE_STRING'
3251
- c.flag [:before]
3231
+ c.flag [:before], type: DateBeginString
3252
3232
 
3253
3233
  c.desc 'Import entries newer than date'
3254
3234
  c.arg_name 'DATE_STRING'
3255
- c.flag [:after]
3235
+ c.flag [:after], type: DateEndString
3256
3236
 
3257
3237
  c.desc %(
3258
3238
  Date range to import. Date range argument should be quoted. Date specifications can be natural language.
@@ -3260,7 +3240,7 @@ command :import do |c|
3260
3240
  Has no effect unless the import plugin has implemented date range filtering.
3261
3241
  )
3262
3242
  c.arg_name 'DATE_OR_RANGE'
3263
- c.flag %i[f from]
3243
+ c.flag %i[f from], type: DateRangeString
3264
3244
 
3265
3245
  c.desc 'Allow entries that overlap existing times'
3266
3246
  c.switch [:overlap], negatable: true
@@ -3278,24 +3258,19 @@ command :import do |c|
3278
3258
  end
3279
3259
 
3280
3260
  if options[:from]
3281
- date_string = options[:from]
3282
- if date_string =~ / (to|through|thru|(un)?til|-+) /
3283
- dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
3284
- start = dates[0].chronify(guess: :begin)
3285
- finish = dates[2].chronify(guess: :end)
3286
- else
3287
- start = date_string.chronify(guess: :begin)
3288
- finish = date_string.chronify(guess: :end)
3289
- end
3290
- raise InvalidTimeExpression, 'Unrecognized date string' unless start
3291
- dates = [start, finish]
3261
+ options[:date_filter] = options[:from]
3262
+
3263
+ raise InvalidTimeExpression, 'Unrecognized date string' unless options[:date_filter][0]
3264
+ elsif options[:before] || options[:after]
3265
+ options[:date_filter] = [nil, nil]
3266
+ options[:date_filter][1] = options[:before] || Time.now + (1 << 64)
3267
+ options[:date_filter][0] = options[:after] || Time.now - (1 << 64)
3292
3268
  end
3293
3269
 
3294
3270
  options[:case] = options[:case].normalize_case
3295
3271
 
3296
3272
  if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
3297
3273
  options[:no_overlap] = !options[:overlap]
3298
- options[:date_filter] = dates
3299
3274
  wwid.import(args, options)
3300
3275
  wwid.write(wwid.doing_file)
3301
3276
  else
@@ -3646,9 +3621,9 @@ command :commands_accepting do |c|
3646
3621
  end
3647
3622
 
3648
3623
  if o[:column]
3649
- puts cmds
3624
+ puts cmds.sort
3650
3625
  else
3651
- puts "Commands accepting --#{option}: #{cmds.join(', ')}"
3626
+ puts "Commands accepting --#{option}: #{cmds.sort.join(', ')}"
3652
3627
  end
3653
3628
  end
3654
3629
  end
@@ -3693,7 +3668,9 @@ pre do |global, _command, _options, _args|
3693
3668
  end
3694
3669
 
3695
3670
  on_error do |exception|
3696
- if exception.kind_of?(SystemExit)
3671
+ if exception.kind_of?(GLI::UnknownCommand)
3672
+ exit run(['now'].concat(ARGV))
3673
+ elsif exception.kind_of?(SystemExit)
3697
3674
  false
3698
3675
  else
3699
3676
  # Doing.logger.error('Fatal:', exception)