doing 2.1.17 → 2.1.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +15 -14
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +117 -53
  6. data/Gemfile.lock +11 -11
  7. data/README.md +1 -1
  8. data/Rakefile +12 -4
  9. data/bin/doing +161 -205
  10. data/docs/doc/Array.html +9 -37
  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 +3 -3
  18. data/docs/doc/Doing/Completion.html +3 -3
  19. data/docs/doc/Doing/Configuration.html +4 -3
  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 +3 -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 +7 -7
  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 +6 -6
  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 +78 -6
  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 +156 -17
  57. data/docs/doc/Symbol.html +3 -3
  58. data/docs/doc/Time.html +3 -3
  59. data/docs/doc/_index.html +23 -16
  60. data/docs/doc/class_list.html +1 -1
  61. data/docs/doc/file.README.html +4 -4
  62. data/docs/doc/frames.html +1 -1
  63. data/docs/doc/index.html +4 -4
  64. data/docs/doc/method_list.html +331 -283
  65. data/docs/doc/top-level-namespace.html +3 -3
  66. data/doing.gemspec +1 -1
  67. data/doing.rdoc +26 -12
  68. data/lib/completion/_doing.zsh +5 -5
  69. data/lib/completion/doing.bash +8 -8
  70. data/lib/completion/doing.fish +93 -15
  71. data/lib/doing/array.rb +5 -4
  72. data/lib/doing/array_chronify.rb +4 -3
  73. data/lib/doing/completion/fish_completion.rb +80 -11
  74. data/lib/doing/configuration.rb +2 -1
  75. data/lib/doing/hash.rb +22 -4
  76. data/lib/doing/item.rb +2 -2
  77. data/lib/doing/items.rb +3 -1
  78. data/lib/doing/log_adapter.rb +1 -1
  79. data/lib/doing/pager.rb +2 -2
  80. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  81. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  82. data/lib/doing/plugins/export/template_export.rb +8 -2
  83. data/lib/doing/prompt.rb +4 -2
  84. data/lib/doing/string.rb +25 -2
  85. data/lib/doing/string_chronify.rb +55 -17
  86. data/lib/doing/template_string.rb +7 -0
  87. data/lib/doing/types.rb +23 -0
  88. data/lib/doing/version.rb +1 -1
  89. data/lib/doing/wwid.rb +71 -50
  90. data/lib/doing.rb +1 -0
  91. data/lib/examples/commands/later.rb +32 -0
  92. data/lib/helpers/threaded_tests.rb +273 -0
  93. metadata +9 -6
data/bin/doing CHANGED
@@ -25,12 +25,7 @@ version Doing::VERSION
25
25
  hide_commands_without_desc true
26
26
  autocomplete_commands true
27
27
 
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
- InvalidExportType = Class.new(RuntimeError)
33
- MissingConfigFile = Class.new(RuntimeError)
28
+ include Doing::Types
34
29
 
35
30
  colors = Doing::Color
36
31
  wwid = Doing::WWID.new
@@ -70,7 +65,42 @@ if settings.dig('plugins', 'command_path')
70
65
  commands_from File.expand_path(settings.dig('plugins', 'command_path'))
71
66
  end
72
67
 
73
- class TagArray < Array; end
68
+ accept DateBeginString do |value|
69
+ if value =~ REGEX_TIME
70
+ res = value
71
+ else
72
+ res = value.chronify(guess: :begin, future: false)
73
+ end
74
+ raise InvalidTimeExpression, 'Invalid start date' unless res
75
+
76
+ res
77
+ end
78
+
79
+ accept DateEndString do |value|
80
+ if value =~ REGEX_TIME
81
+ res = value
82
+ else
83
+ res = value.chronify(guess: :end, future: false)
84
+ end
85
+ raise InvalidTimeExpression, 'Invalid end date' unless res
86
+
87
+ res
88
+ end
89
+
90
+ accept DateRangeString do |value|
91
+ start, finish = value.split_date_range
92
+ raise InvalidTimeExpression, 'Invalid range' unless start
93
+
94
+ finish ||= Time.now
95
+ [start, finish]
96
+ end
97
+
98
+ accept DateIntervalString do |value|
99
+ res = value.chronify_qty
100
+ raise InvalidTimeExpression, 'Invalid time quantity' unless res
101
+
102
+ res
103
+ end
74
104
 
75
105
  accept TagArray do |value|
76
106
  value.gsub(/[, ]+/, ' ').split(' ').map { |tag| tag.sub(/^@/, '')}.map(&:strip)
@@ -97,7 +127,7 @@ desc 'Use a pager when output is longer than screen'
97
127
  switch %i[p pager], default_value: settings['paginate']
98
128
 
99
129
  desc 'Answer yes/no menus with default option'
100
- switch [:default], default_value: false
130
+ switch [:default], default_value: false, negatable: false
101
131
 
102
132
  desc 'Answer all yes/no menus with yes'
103
133
  switch [:yes], negatable: false
@@ -143,6 +173,10 @@ command %i[again resume] do |c|
143
173
  c.arg_name 'SECTION_NAME'
144
174
  c.flag [:in]
145
175
 
176
+ c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
177
+ c.arg_name 'DATE_STRING'
178
+ c.flag %i[b back started], type: DateBeginString
179
+
146
180
  c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?)'
147
181
  c.arg_name 'TAG'
148
182
  c.flag [:tag], type: TagArray
@@ -198,6 +232,13 @@ command %i[again resume] do |c|
198
232
  options[:search] = search
199
233
  end
200
234
 
235
+ if options[:back]
236
+ date = options[:back]
237
+ raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
238
+ else
239
+ date = Time.now
240
+ end
241
+
201
242
  note = Doing::Note.new(options[:note])
202
243
  note.add(Doing::Prompt.read_lines(prompt: 'Add a note')) if options[:ask]
203
244
 
@@ -208,6 +249,7 @@ command %i[again resume] do |c|
208
249
  opts[:tag] = tags
209
250
  opts[:tag_bool] = options[:bool].normalize_bool
210
251
  opts[:interactive] = options[:interactive]
252
+ opts[:date] = date
211
253
 
212
254
  wwid.repeat_last(opts)
213
255
  end
@@ -321,7 +363,7 @@ desc 'Add a completed item with @done(date). No argument finishes last entry'
321
363
  long_desc 'Use this command to add an entry after you\'ve already finished it. It will be immediately marked as @done.
322
364
  You can modify the start and end times of the entry using the --back, --took, and --at flags, making it an easy
323
365
  way to add entries in post and maintain accurate (albeit manual) time tracking.'
324
- arg_name 'ENTRY'
366
+ arg_name 'ENTRY', optional: true
325
367
  command %i[done did] do |c|
326
368
  c.example 'doing done', desc: 'Tag the last entry @done'
327
369
  c.example 'doing done I already finished this', desc: 'Add a new entry and immediately mark it @done'
@@ -340,24 +382,24 @@ command %i[done did] do |c|
340
382
  c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm).
341
383
  Used with --took, backdates start date)
342
384
  c.arg_name 'DATE_STRING'
343
- c.flag %i[at finished]
385
+ c.flag %i[at finished], type: DateEndString
344
386
 
345
387
  c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
346
388
  c.arg_name 'DATE_STRING'
347
- c.flag %i[b back started]
389
+ c.flag %i[b back started], type: DateBeginString
348
390
 
349
391
  c.desc %(
350
392
  Start and end times as a date/time range `doing done --from "1am to 8am"`.
351
393
  Overrides other date flags.
352
394
  )
353
395
  c.arg_name 'TIME_RANGE'
354
- c.flag [:from]
396
+ c.flag [:from], must_match: REGEX_RANGE
355
397
 
356
398
  c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM).
357
399
  If used without the --back option, the start date will be moved back to allow
358
400
  the completion date to be the current time.)
359
401
  c.arg_name 'INTERVAL'
360
- c.flag %i[t took for]
402
+ c.flag %i[t took for], type: DateIntervalString
361
403
 
362
404
  c.desc 'Section'
363
405
  c.arg_name 'NAME'
@@ -385,23 +427,27 @@ command %i[done did] do |c|
385
427
  donedate = nil
386
428
 
387
429
  if options[:from]
388
- date, finish_date = options[:from].split_date_range
430
+ options[:from] = options[:from].split(/#{REGEX_RANGE_INDICATOR}/).map do |time|
431
+ time =~ REGEX_TIME ? "today #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}" : time
432
+ end.join(' to ').split_date_range
433
+ date, finish_date = options[:from]
389
434
  finish_date ||= Time.now
390
435
  else
391
436
  if options[:took]
392
- took = options[:took].chronify_qty
437
+ took = options[:took]
393
438
  raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
394
439
  end
395
440
 
396
441
  if options[:back]
397
- date = options[:back].chronify(guess: :begin)
442
+ date = options[:back]
398
443
  raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
399
444
  else
400
445
  date = options[:took] ? Time.now - took : Time.now
401
446
  end
402
447
 
403
448
  if options[:at]
404
- finish_date = options[:at].chronify(guess: :begin)
449
+ finish_date = options[:at]
450
+ finish_date = finish_date.chronify(guess: :begin) if finish_date.is_a? String
405
451
  raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
406
452
 
407
453
  if options[:took]
@@ -417,6 +463,7 @@ command %i[done did] do |c|
417
463
  end
418
464
 
419
465
  if options[:date]
466
+ date = date.chronify(guess: :begin, context: :today) if date =~ REGEX_TIME
420
467
  finish_date = wwid.verify_duration(date, finish_date) unless options[:took] || options[:from]
421
468
 
422
469
  donedate = finish_date.strftime('%F %R')
@@ -528,7 +575,7 @@ command %i[done did] do |c|
528
575
  wwid.content.push(new_entry)
529
576
  Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
530
577
  wwid.write(wwid.doing_file)
531
- Doing.logger.info('Entry Added:', new_entry.title)
578
+ Doing.logger.info('New entry:', %(added "#{new_entry.date.relative_date}: #{new_entry.title}" to #{section}))
532
579
  elsif $stdin.stat.size.positive?
533
580
  note = Doing::Note.new(options[:note])
534
581
  d, title, note = wwid.format_input($stdin.read.strip)
@@ -553,7 +600,7 @@ command %i[done did] do |c|
553
600
  Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
554
601
 
555
602
  wwid.write(wwid.doing_file)
556
- Doing.logger.info('Entry Added:', new_entry.title)
603
+ Doing.logger.info('New entry:', %(added "#{new_entry.date.relative_date}: #{new_entry.title}" to #{section}))
557
604
  else
558
605
  raise EmptyInput, 'You must provide content when creating a new entry'
559
606
  end
@@ -563,7 +610,7 @@ end
563
610
  # @@finish
564
611
  desc 'Mark last X entries as @done'
565
612
  long_desc 'Marks the last X entries with a @done tag and current date. Does not alter already completed entries.'
566
- arg_name 'COUNT'
613
+ arg_name 'COUNT', optional: true
567
614
  command :finish do |c|
568
615
  c.example 'doing finish', desc: 'Mark the last entry @done'
569
616
  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'
@@ -574,15 +621,15 @@ command :finish do |c|
574
621
 
575
622
  c.desc 'Backdate completed date to date string [4pm|20m|2h|yesterday noon]'
576
623
  c.arg_name 'DATE_STRING'
577
- c.flag %i[b back]
624
+ c.flag %i[b back started], type: DateBeginString
578
625
 
579
626
  c.desc 'Set the completed date to the start date plus XX[hmd]'
580
627
  c.arg_name 'INTERVAL'
581
- c.flag %i[t took for]
628
+ c.flag %i[t took for], type: DateIntervalString
582
629
 
583
630
  c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.)
584
631
  c.arg_name 'DATE_STRING'
585
- c.flag [:at]
632
+ c.flag %i[at finished], type: DateEndString
586
633
 
587
634
  c.desc 'Finish the last X entries containing TAG.
588
635
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
@@ -639,7 +686,7 @@ command :finish do |c|
639
686
  options[:fuzzy] = false
640
687
  unless options[:auto]
641
688
  if options[:took]
642
- took = options[:took].chronify_qty
689
+ took = options[:took]
643
690
  raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
644
691
  end
645
692
 
@@ -648,12 +695,13 @@ command :finish do |c|
648
695
  raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
649
696
 
650
697
  if options[:at]
651
- finish_date = options[:at].chronify(guess: :begin)
698
+ finish_date = options[:at]
699
+ finish_date = finish_date.chronify(guess: :begin) if finish_date.is_a? String
652
700
  raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
653
701
 
654
702
  date = options[:took] ? finish_date - took : finish_date
655
703
  elsif options[:back]
656
- date = options[:back].chronify()
704
+ date = options[:back]
657
705
 
658
706
  raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
659
707
  else
@@ -661,8 +709,6 @@ command :finish do |c|
661
709
  end
662
710
  end
663
711
 
664
- options[:took] = options[:took].chronify_qty if options[:took]
665
-
666
712
  if options[:tag].nil?
667
713
  tags = []
668
714
  else
@@ -711,92 +757,6 @@ command :finish do |c|
711
757
  end
712
758
  end
713
759
 
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
760
  # @@mark @@flag
801
761
  desc 'Mark last entry as flagged'
802
762
  command %i[mark flag] do |c|
@@ -930,7 +890,7 @@ long_desc 'The @meanwhile tag allows you to have long-running entries that encom
930
890
  This command makes it easy to start and stop these overarching entries. Just run `doing meanwhile Starting work on this
931
891
  big project` to start a @meanwhile entry, add other entries as you work on the project, then use `doing meanwhile` by
932
892
  itself to mark the entry as @done.'
933
- arg_name 'ENTRY'
893
+ arg_name 'ENTRY', optional: true
934
894
  command :meanwhile do |c|
935
895
  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'
936
896
  c.example 'doing meanwhile', desc: 'Finish any open @meanwhile entry'
@@ -949,7 +909,7 @@ command :meanwhile do |c|
949
909
 
950
910
  c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]'
951
911
  c.arg_name 'DATE_STRING'
952
- c.flag %i[b back]
912
+ c.flag %i[b back started], type: DateBeginString
953
913
 
954
914
  c.desc 'Note'
955
915
  c.arg_name 'TEXT'
@@ -960,7 +920,7 @@ command :meanwhile do |c|
960
920
 
961
921
  c.action do |_global_options, options, args|
962
922
  if options[:back]
963
- date = options[:back].chronify(guess: :begin)
923
+ date = options[:back]
964
924
 
965
925
  raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
966
926
  else
@@ -1020,7 +980,7 @@ long_desc %(
1020
980
 
1021
981
  Use -e to load the last entry in a text editor where you can append a note.
1022
982
  )
1023
- arg_name 'NOTE_TEXT'
983
+ arg_name 'NOTE_TEXT', optional: true
1024
984
  command :note do |c|
1025
985
  c.example 'doing note', desc: 'Open the last entry in $EDITOR to append a note'
1026
986
  c.example 'doing note "Just a quick annotation"', desc: 'Add a quick note to the last entry'
@@ -1088,7 +1048,6 @@ command :note do |c|
1088
1048
  options[:search] = search
1089
1049
  end
1090
1050
 
1091
-
1092
1051
  last_entry = wwid.last_entry(options)
1093
1052
 
1094
1053
  unless last_entry
@@ -1098,37 +1057,37 @@ command :note do |c|
1098
1057
 
1099
1058
  last_note = last_entry.note || Doing::Note.new
1100
1059
  new_note = Doing::Note.new
1101
- ask_note = options[:ask] ? Doing::Prompt.read_lines(prompt: 'Add a note') : ''
1102
1060
 
1103
- if options[:editor] || (args.empty? && $stdin.stat.size.zero? && !options[:remove] && !options[:ask])
1104
- raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
1061
+ if $stdin.stat.size.positive?
1062
+ new_note.add($stdin.read.strip)
1063
+ end
1105
1064
 
1106
- input = !args.empty? ? args.join(' ') : ''
1065
+ unless args.empty?
1066
+ new_note.add(args.join(' '))
1067
+ end
1068
+
1069
+ if options[:editor]
1070
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
1107
1071
 
1108
1072
  if options[:remove]
1109
- prev_input = Doing::Note.new
1073
+ input = Doing::Note.new
1110
1074
  else
1111
- prev_input = last_entry.note || Doing::Note.new
1075
+ input = last_entry.note || Doing::Note.new
1112
1076
  end
1113
1077
 
1078
+ input.add(new_note)
1114
1079
 
1115
- input = prev_input.add(input)
1116
- input.add(ask_note) unless ask_note.empty?
1117
-
1118
- input = wwid.fork_editor(prev_input.strip_lines.join("\n"), message: nil).strip
1119
- note = input
1080
+ new_note = Doing::Note.new(wwid.fork_editor(input.strip_lines.join("\n"), message: nil).strip)
1120
1081
  options[:remove] = true
1121
- new_note.add(note)
1122
- elsif !args.empty?
1123
- new_note.add(args.join(' '))
1124
- elsif $stdin.stat.size.positive?
1125
- new_note.add($stdin.read.strip)
1126
- else
1127
- raise EmptyInput, 'You must provide content when adding a note' unless options[:remove] || !ask_note.empty?
1082
+ end
1128
1083
 
1084
+ if (new_note.empty? && !options[:remove]) || options[:ask]
1085
+ $stderr.puts last_note unless last_note.empty?
1086
+ $stderr.puts new_note unless new_note.empty?
1087
+ new_note.add(Doing::Prompt.read_lines(prompt: 'Add a note'))
1129
1088
  end
1130
1089
 
1131
- new_note.add(ask_note) unless ask_note.empty?
1090
+ raise EmptyInput, 'You must provide content when adding a note' unless options[:remove] || !new_note.empty?
1132
1091
 
1133
1092
  if last_note.equal?(new_note)
1134
1093
  Doing.logger.debug('Skipped:', 'No note change')
@@ -1169,7 +1128,7 @@ command %i[now next] do |c|
1169
1128
 
1170
1129
  c.desc 'Backdate start time [4pm|20m|2h|"yesterday noon"]'
1171
1130
  c.arg_name 'DATE_STRING'
1172
- c.flag %i[b back started]
1131
+ c.flag %i[b back started], type: DateBeginString
1173
1132
 
1174
1133
  c.desc 'Timed entry, marks last entry in section as @done'
1175
1134
  c.switch %i[f finish_last], negatable: false, default_value: false
@@ -1187,7 +1146,7 @@ command %i[now next] do |c|
1187
1146
 
1188
1147
  c.action do |_global_options, options, args|
1189
1148
  if options[:back]
1190
- date = options[:back].chronify(guess: :begin)
1149
+ date = options[:back]
1191
1150
 
1192
1151
  raise InvalidTimeExpression.new('unable to parse date string', topic: 'Parser:') if date.nil?
1193
1152
  else
@@ -1211,7 +1170,7 @@ command %i[now next] do |c|
1211
1170
  input += "\n#{ask_note}" unless ask_note.empty?
1212
1171
  input = wwid.fork_editor(input).strip
1213
1172
 
1214
- date, title, note = wwid.format_input(input)
1173
+ d, title, note = wwid.format_input(input)
1215
1174
  raise EmptyInput, 'No content' if title.strip.empty?
1216
1175
 
1217
1176
  if ask_note.empty? && options[:ask]
@@ -1219,6 +1178,7 @@ command %i[now next] do |c|
1219
1178
  note.add(ask_note) unless ask_note.empty?
1220
1179
  end
1221
1180
 
1181
+ date = d.nil? ? date : d
1222
1182
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1223
1183
  wwid.write(wwid.doing_file)
1224
1184
  elsif args.length.positive?
@@ -1265,7 +1225,7 @@ desc 'Reset the start time of an entry'
1265
1225
  long_desc 'Update the start time of the last entry or the last entry matching a tag/search filter.
1266
1226
  If no argument is provided, the start time will be reset to the current time.
1267
1227
  If a date string is provided as an argument, the start time will be set to the parsed result.'
1268
- arg_name 'DATE_STRING'
1228
+ arg_name 'DATE_STRING', optional: true
1269
1229
  command %i[reset begin] do |c|
1270
1230
  c.example 'doing reset', desc: 'Reset the start time of the last entry to the current time'
1271
1231
  c.example 'doing reset --tag project1', desc: 'Reset the start time of the most recent entry tagged @project1 to the current time'
@@ -1279,6 +1239,9 @@ command %i[reset begin] do |c|
1279
1239
  c.desc 'Resume entry (remove @done)'
1280
1240
  c.switch %i[r resume], default_value: true
1281
1241
 
1242
+ c.desc 'Change start date but do not remove @done (shortcut for --no-resume)'
1243
+ c.switch [:n]
1244
+
1282
1245
  c.desc 'Reset last entry matching tag. Wildcards allowed (*, ?)'
1283
1246
  c.arg_name 'TAG'
1284
1247
  c.flag [:tag]
@@ -1416,11 +1379,11 @@ command :select do |c|
1416
1379
 
1417
1380
  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'
1418
1381
  c.arg_name 'DATE_STRING'
1419
- c.flag [:before]
1382
+ c.flag [:before], type: DateBeginString
1420
1383
 
1421
1384
  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'
1422
1385
  c.arg_name 'DATE_STRING'
1423
- c.flag [:after]
1386
+ c.flag [:after], type: DateEndString
1424
1387
 
1425
1388
  c.desc %(
1426
1389
  Date range to show, or a single day to filter date on.
@@ -1431,7 +1394,7 @@ command :select do |c|
1431
1394
  by time of day.
1432
1395
  )
1433
1396
  c.arg_name 'DATE_OR_RANGE'
1434
- c.flag [:from]
1397
+ c.flag [:from], type: DateRangeString
1435
1398
 
1436
1399
  c.desc 'Force exact search string matching (case sensitive)'
1437
1400
  c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
@@ -1698,11 +1661,11 @@ command %i[grep search] do |c|
1698
1661
 
1699
1662
  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'
1700
1663
  c.arg_name 'DATE_STRING'
1701
- c.flag [:before]
1664
+ c.flag [:before], type: DateBeginString
1702
1665
 
1703
1666
  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'
1704
1667
  c.arg_name 'DATE_STRING'
1705
- c.flag [:after]
1668
+ c.flag [:after], type: DateEndString
1706
1669
 
1707
1670
  c.desc %(
1708
1671
  Date range to show, or a single day to filter date on.
@@ -1713,7 +1676,7 @@ command %i[grep search] do |c|
1713
1676
  by time of day.
1714
1677
  )
1715
1678
  c.arg_name 'DATE_OR_RANGE'
1716
- c.flag [:from]
1679
+ c.flag [:from], type: DateRangeString
1717
1680
 
1718
1681
  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1719
1682
  c.arg_name 'FORMAT'
@@ -2011,11 +1974,11 @@ command :show do |c|
2011
1974
 
2012
1975
  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'
2013
1976
  c.arg_name 'DATE_STRING'
2014
- c.flag [:before]
1977
+ c.flag [:before], type: DateBeginString
2015
1978
 
2016
1979
  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'
2017
1980
  c.arg_name 'DATE_STRING'
2018
- c.flag [:after]
1981
+ c.flag [:after], type: DateEndString
2019
1982
 
2020
1983
  c.desc %(
2021
1984
  Date range to show, or a single day to filter date on.
@@ -2027,7 +1990,7 @@ command :show do |c|
2027
1990
  )
2028
1991
 
2029
1992
  c.arg_name 'DATE_OR_RANGE'
2030
- c.flag [:from]
1993
+ c.flag [:from], type: DateRangeString
2031
1994
 
2032
1995
  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2033
1996
  c.arg_name 'QUERY'
@@ -2322,8 +2285,8 @@ command :today do |c|
2322
2285
  c.desc %(
2323
2286
  Time range to show `doing today --from "12pm to 4pm"`
2324
2287
  )
2325
- c.arg_name 'DATE_OR_RANGE'
2326
- c.flag [:from]
2288
+ c.arg_name 'TIME_RANGE'
2289
+ c.flag [:from], type: DateRangeString
2327
2290
 
2328
2291
  c.action do |_global_options, options, _args|
2329
2292
  raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
@@ -2336,7 +2299,7 @@ command :today do |c|
2336
2299
  end
2337
2300
  end
2338
2301
 
2339
- # @on
2302
+ # @@on
2340
2303
  desc 'List entries for a date'
2341
2304
  long_desc %(Date argument can be natural language. "thursday" would be interpreted as "last thursday,"
2342
2305
  and "2d" would be interpreted as "two days ago." If you use "to" or "through" between two dates,
@@ -2375,16 +2338,11 @@ command :on do |c|
2375
2338
 
2376
2339
  raise MissingArgument, 'Missing date argument' if args.empty?
2377
2340
 
2378
- date_string = args.join(' ')
2379
-
2380
- if date_string =~ / (to|through|thru) /
2381
- dates = date_string.split(/ (to|through|thru) /)
2382
- start = dates[0].chronify(guess: :begin)
2383
- finish = dates[2].chronify(guess: :end)
2384
- else
2385
- start = date_string.chronify(guess: :begin)
2386
- finish = false
2341
+ date_string = args.join(' ').strip
2342
+ if date_string =~ /^tod(?:ay)?/i
2343
+ date_string = 'today to tomorrow 12am'
2387
2344
  end
2345
+ start, finish = date_string.split_date_range
2388
2346
 
2389
2347
  raise InvalidTimeExpression, 'Unrecognized date string' unless start
2390
2348
 
@@ -2536,11 +2494,11 @@ command :view do |c|
2536
2494
 
2537
2495
  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'
2538
2496
  c.arg_name 'DATE_STRING'
2539
- c.flag [:before]
2497
+ c.flag [:before], type: DateBeginString
2540
2498
 
2541
2499
  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'
2542
2500
  c.arg_name 'DATE_STRING'
2543
- c.flag [:after]
2501
+ c.flag [:after], type: DateEndString
2544
2502
 
2545
2503
  c.desc %(
2546
2504
  Date range to show, or a single day to filter date on.
@@ -2551,7 +2509,7 @@ command :view do |c|
2551
2509
  by time of day.
2552
2510
  )
2553
2511
  c.arg_name 'DATE_OR_RANGE'
2554
- c.flag [:from]
2512
+ c.flag [:from], type: DateRangeString
2555
2513
 
2556
2514
  c.desc 'Only show items with recorded time intervals (override view settings)'
2557
2515
  c.switch [:only_timed], default_value: false, negatable: false
@@ -2588,17 +2546,17 @@ command :view do |c|
2588
2546
  view = wwid.get_view(title)
2589
2547
 
2590
2548
  if view
2591
- page_title = view.key?('title') ? view['title'] : title.cap_first
2549
+ page_title = view['title'] || title.cap_first
2592
2550
  only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed]
2593
2551
  true
2594
2552
  else
2595
2553
  false
2596
2554
  end
2597
2555
 
2598
- template = view.key?('template') ? view['template'] : nil
2599
- date_format = view.key?('date_format') ? view['date_format'] : nil
2556
+ template = view['template'] || nil
2557
+ date_format = view['date_format'] || nil
2600
2558
 
2601
- tags_color = view.key?('tags_color') ? view['tags_color'] : nil
2559
+ tags_color = view['tags_color'] || nil
2602
2560
  tag_filter = false
2603
2561
  if options[:tag]
2604
2562
  tag_filter = { 'tags' => [], 'bool' => 'OR' }
@@ -2628,19 +2586,19 @@ command :view do |c|
2628
2586
  section = if options[:section]
2629
2587
  section
2630
2588
  else
2631
- view.key?('section') ? view['section'] : settings['current_section']
2589
+ view['section'] || settings['current_section']
2632
2590
  end
2633
- order = view.key?('order') ? view['order'].normalize_order : 'asc'
2591
+ order = view['order']&.normalize_order || 'asc'
2634
2592
 
2635
2593
  totals = if options[:totals]
2636
2594
  true
2637
2595
  else
2638
- view.key?('totals') ? view['totals'] : false
2596
+ view['totals'] || false
2639
2597
  end
2640
2598
  tag_order = if options[:tag_order]
2641
2599
  options[:tag_order].normalize_order
2642
2600
  else
2643
- view.key?('tag_order') ? view['tag_order'].normalize_order : 'asc'
2601
+ view['tag_order']&.normalize_order || 'asc'
2644
2602
  end
2645
2603
 
2646
2604
  options[:times] = true if totals
@@ -2669,7 +2627,6 @@ command :view do |c|
2669
2627
 
2670
2628
  opts = options.dup
2671
2629
  opts[:age] = options[:age].normalize_age(:newest)
2672
- opts[:view_template] = title
2673
2630
  opts[:count] = count
2674
2631
  opts[:format] = date_format
2675
2632
  opts[:highlight] = options[:color]
@@ -2686,6 +2643,7 @@ command :view do |c|
2686
2643
  opts[:tags_color] = tags_color
2687
2644
  opts[:template] = template
2688
2645
  opts[:totals] = totals
2646
+ opts[:view_template] = title
2689
2647
 
2690
2648
  Doing::Pager.page wwid.list_section(opts)
2691
2649
  elsif title.instance_of?(FalseClass)
@@ -2735,11 +2693,9 @@ command :yesterday do |c|
2735
2693
  c.arg_name 'TIME_STRING'
2736
2694
  c.flag [:after]
2737
2695
 
2738
- c.desc %(
2739
- Time range to show, e.g. `doing yesterday --from "1am to 8am"`
2740
- )
2696
+ c.desc 'Time range to show, e.g. `doing yesterday --from "1am to 8am"`'
2741
2697
  c.arg_name 'TIME_RANGE'
2742
- c.flag [:from]
2698
+ c.flag [:from], must_match: REGEX_TIME_RANGE
2743
2699
 
2744
2700
  c.desc 'Tag sort direction (asc|desc)'
2745
2701
  c.arg_name 'DIRECTION'
@@ -2751,9 +2707,9 @@ command :yesterday do |c|
2751
2707
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
2752
2708
 
2753
2709
  if options[:from]
2754
- options[:from] = options[:from].split(/ (?:to|through|thru|(?:un)?til|-+) /).map do |time|
2710
+ options[:from] = options[:from].split(/#{REGEX_RANGE_INDICATOR}/).map do |time|
2755
2711
  "yesterday #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}"
2756
- end.join(' to ')
2712
+ end.join(' to ').split_date_range
2757
2713
  end
2758
2714
 
2759
2715
  opt = {
@@ -2991,8 +2947,7 @@ command :config do |c|
2991
2947
  value = options[:remove] ? nil : args.pop
2992
2948
  keypath = args.join('.')
2993
2949
  real_path = config.resolve_key_path(keypath, create: true)
2994
-
2995
- old_value = settings.dig(*real_path) || nil
2950
+ old_value = settings.dig(*real_path)
2996
2951
  old_type = old_value&.class.to_s || nil
2997
2952
 
2998
2953
  if old_value.is_a?(Hash) && !options[:remove]
@@ -3018,7 +2973,6 @@ command :config do |c|
3018
2973
  else
3019
2974
  current_value = cfg.dig(*real_path)
3020
2975
  cfg.deep_set(real_path, value.set_type(old_type))
3021
-
3022
2976
  $stderr.puts "#{' Key path:'.yellow} #{real_path.join('->').boldwhite}"
3023
2977
  $stderr.puts "#{'Inherited:'.yellow} #{(old_value ? old_value.to_s : 'empty').boldwhite}"
3024
2978
  $stderr.puts "#{' Current:'.yellow} #{ (current_value ? current_value.to_s : 'empty').boldwhite }"
@@ -3178,7 +3132,7 @@ command %i[archive move] do |c|
3178
3132
  c.desc 'Archive entries older than date
3179
3133
  (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
3180
3134
  c.arg_name 'DATE_STRING'
3181
- c.flag [:before]
3135
+ c.flag [:before], type: DateEndString
3182
3136
 
3183
3137
  c.action do |_global_options, options, args|
3184
3138
  options[:fuzzy] = false
@@ -3269,11 +3223,11 @@ command :import do |c|
3269
3223
  # TODO: Allow time range filtering
3270
3224
  c.desc 'Import entries older than date'
3271
3225
  c.arg_name 'DATE_STRING'
3272
- c.flag [:before]
3226
+ c.flag [:before], type: DateBeginString
3273
3227
 
3274
3228
  c.desc 'Import entries newer than date'
3275
3229
  c.arg_name 'DATE_STRING'
3276
- c.flag [:after]
3230
+ c.flag [:after], type: DateEndString
3277
3231
 
3278
3232
  c.desc %(
3279
3233
  Date range to import. Date range argument should be quoted. Date specifications can be natural language.
@@ -3281,7 +3235,7 @@ command :import do |c|
3281
3235
  Has no effect unless the import plugin has implemented date range filtering.
3282
3236
  )
3283
3237
  c.arg_name 'DATE_OR_RANGE'
3284
- c.flag %i[f from]
3238
+ c.flag %i[f from], type: DateRangeString
3285
3239
 
3286
3240
  c.desc 'Allow entries that overlap existing times'
3287
3241
  c.switch [:overlap], negatable: true
@@ -3299,24 +3253,19 @@ command :import do |c|
3299
3253
  end
3300
3254
 
3301
3255
  if options[:from]
3302
- date_string = options[:from]
3303
- if date_string =~ / (to|through|thru|(un)?til|-+) /
3304
- dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
3305
- start = dates[0].chronify(guess: :begin)
3306
- finish = dates[2].chronify(guess: :end)
3307
- else
3308
- start = date_string.chronify(guess: :begin)
3309
- finish = date_string.chronify(guess: :end)
3310
- end
3311
- raise InvalidTimeExpression, 'Unrecognized date string' unless start
3312
- dates = [start, finish]
3256
+ options[:date_filter] = options[:from]
3257
+
3258
+ raise InvalidTimeExpression, 'Unrecognized date string' unless options[:date_filter][0]
3259
+ elsif options[:before] || options[:after]
3260
+ options[:date_filter] = [nil, nil]
3261
+ options[:date_filter][1] = options[:before] || Time.now + (1 << 64)
3262
+ options[:date_filter][0] = options[:after] || Time.now - (1 << 64)
3313
3263
  end
3314
3264
 
3315
3265
  options[:case] = options[:case].normalize_case
3316
3266
 
3317
3267
  if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
3318
3268
  options[:no_overlap] = !options[:overlap]
3319
- options[:date_filter] = dates
3320
3269
  wwid.import(args, options)
3321
3270
  wwid.write(wwid.doing_file)
3322
3271
  else
@@ -3667,9 +3616,9 @@ command :commands_accepting do |c|
3667
3616
  end
3668
3617
 
3669
3618
  if o[:column]
3670
- puts cmds
3619
+ puts cmds.sort
3671
3620
  else
3672
- puts "Commands accepting --#{option}: #{cmds.join(', ')}"
3621
+ puts "Commands accepting --#{option}: #{cmds.sort.join(', ')}"
3673
3622
  end
3674
3623
  end
3675
3624
  end
@@ -3700,7 +3649,7 @@ pre do |global, _command, _options, _args|
3700
3649
  Doing::Pager.paginate = global[:pager]
3701
3650
 
3702
3651
  $stdout.puts "doing v#{Doing::VERSION}" if global[:version]
3703
- unless STDOUT.isatty
3652
+ unless $stdout.isatty
3704
3653
  Doing::Color.coloring = global[:pager] ? global[:color] : false
3705
3654
  else
3706
3655
  Doing::Color.coloring = global[:color]
@@ -3714,7 +3663,9 @@ pre do |global, _command, _options, _args|
3714
3663
  end
3715
3664
 
3716
3665
  on_error do |exception|
3717
- if exception.kind_of?(SystemExit)
3666
+ if exception.kind_of?(GLI::UnknownCommand)
3667
+ exit run(['now'].concat(ARGV))
3668
+ elsif exception.kind_of?(SystemExit)
3718
3669
  false
3719
3670
  else
3720
3671
  # Doing.logger.error('Fatal:', exception)
@@ -3748,7 +3699,12 @@ around do |global, command, options, arguments, code|
3748
3699
  Doing::Prompt.force_answer = false
3749
3700
  Doing.config.force_answer = false
3750
3701
  else
3751
- Doing::Prompt.default_answer = global[:default]
3702
+ Doing::Prompt.default_answer = if $stdout.isatty
3703
+ global[:default]
3704
+ else
3705
+ true
3706
+ end
3707
+
3752
3708
  Doing.config.force_answer = global[:default] ? true : false
3753
3709
  end
3754
3710