doing 2.1.19 → 2.1.24

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +19 -16
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +70 -0
  6. data/Gemfile.lock +11 -11
  7. data/README.md +1 -1
  8. data/Rakefile +12 -4
  9. data/bin/doing +297 -234
  10. data/docs/doc/Array.html +7 -30
  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 +6 -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 +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/Types.html +201 -0
  40. data/docs/doc/Doing/Util/Backup.html +3 -3
  41. data/docs/doc/Doing/Util.html +4 -7
  42. data/docs/doc/Doing/WWID.html +66 -8
  43. data/docs/doc/Doing.html +6 -6
  44. data/docs/doc/GLI/Commands/Help.html +185 -0
  45. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +3 -3
  46. data/docs/doc/GLI/Commands.html +7 -5
  47. data/docs/doc/GLI.html +6 -4
  48. data/docs/doc/Hash.html +80 -16
  49. data/docs/doc/Numeric.html +3 -3
  50. data/docs/doc/PhraseParser/Operator.html +3 -3
  51. data/docs/doc/PhraseParser/PhraseClause.html +3 -3
  52. data/docs/doc/PhraseParser/Query.html +3 -3
  53. data/docs/doc/PhraseParser/QueryParser.html +3 -3
  54. data/docs/doc/PhraseParser/QueryTransformer.html +3 -3
  55. data/docs/doc/PhraseParser/TermClause.html +3 -3
  56. data/docs/doc/PhraseParser.html +3 -3
  57. data/docs/doc/Status.html +3 -3
  58. data/docs/doc/String.html +195 -26
  59. data/docs/doc/Symbol.html +3 -3
  60. data/docs/doc/Time.html +3 -3
  61. data/docs/doc/_index.html +22 -8
  62. data/docs/doc/class_list.html +1 -1
  63. data/docs/doc/file.README.html +4 -4
  64. data/docs/doc/frames.html +1 -1
  65. data/docs/doc/index.html +4 -4
  66. data/docs/doc/method_list.html +334 -270
  67. data/docs/doc/top-level-namespace.html +3 -3
  68. data/docs/index.md +1 -1
  69. data/doing.gemspec +1 -1
  70. data/doing.rdoc +173 -15
  71. data/lib/completion/_doing.zsh +20 -20
  72. data/lib/completion/doing.bash +37 -26
  73. data/lib/completion/doing.fish +116 -17
  74. data/lib/doing/array.rb +5 -4
  75. data/lib/doing/array_chronify.rb +4 -3
  76. data/lib/doing/changelog/change.rb +115 -0
  77. data/lib/doing/changelog/changes.rb +73 -0
  78. data/lib/doing/changelog/entry.rb +21 -0
  79. data/lib/doing/changelog/version.rb +97 -0
  80. data/lib/doing/changelog.rb +6 -0
  81. data/lib/doing/completion/fish_completion.rb +82 -12
  82. data/lib/doing/configuration.rb +17 -8
  83. data/lib/doing/hash.rb +25 -6
  84. data/lib/doing/help_monkey_patch.rb +31 -0
  85. data/lib/doing/hooks.rb +5 -1
  86. data/lib/doing/item.rb +10 -25
  87. data/lib/doing/items.rb +3 -1
  88. data/lib/doing/log_adapter.rb +1 -1
  89. data/lib/doing/pager.rb +2 -2
  90. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  91. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  92. data/lib/doing/plugins/export/template_export.rb +9 -3
  93. data/lib/doing/prompt.rb +4 -2
  94. data/lib/doing/string.rb +40 -11
  95. data/lib/doing/string_chronify.rb +56 -18
  96. data/lib/doing/template_string.rb +7 -0
  97. data/lib/doing/types.rb +25 -0
  98. data/lib/doing/util.rb +2 -1
  99. data/lib/doing/version.rb +1 -1
  100. data/lib/doing/wwid.rb +91 -67
  101. data/lib/doing.rb +2 -0
  102. data/lib/examples/commands/later.rb +32 -0
  103. data/lib/helpers/threaded_tests.rb +286 -0
  104. metadata +17 -6
data/bin/doing CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
5
5
  require 'gli'
6
+ require 'doing/help_monkey_patch'
6
7
  require 'doing'
7
8
  require 'tempfile'
8
9
  require 'pp'
@@ -21,16 +22,12 @@ end
21
22
 
22
23
  include GLI::App
23
24
  include Doing::Errors
25
+
24
26
  version Doing::VERSION
25
27
  hide_commands_without_desc true
26
28
  autocomplete_commands true
27
29
 
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)
30
+ include Doing::Types
34
31
 
35
32
  colors = Doing::Color
36
33
  wwid = Doing::WWID.new
@@ -70,7 +67,49 @@ if settings.dig('plugins', 'command_path')
70
67
  commands_from File.expand_path(settings.dig('plugins', 'command_path'))
71
68
  end
72
69
 
73
- class TagArray < Array; end
70
+ accept TemplateName do |value|
71
+ res = settings['templates'].keys.select { |k| k =~ value.to_rx(distance: 2) }
72
+ raise InvalidArgument, "Unknown template: #{value}" if res.empty?
73
+
74
+ res.group_by(&:length).min.last[0]
75
+ end
76
+
77
+ accept DateBeginString do |value|
78
+ if value =~ REGEX_TIME
79
+ res = value
80
+ else
81
+ res = value.chronify(guess: :begin, future: false)
82
+ end
83
+ raise InvalidTimeExpression, 'Invalid start date' unless res
84
+
85
+ res
86
+ end
87
+
88
+ accept DateEndString do |value|
89
+ if value =~ REGEX_TIME
90
+ res = value
91
+ else
92
+ res = value.chronify(guess: :end, future: false)
93
+ end
94
+ raise InvalidTimeExpression, 'Invalid end date' unless res
95
+
96
+ res
97
+ end
98
+
99
+ accept DateRangeString do |value|
100
+ start, finish = value.split_date_range
101
+ raise InvalidTimeExpression, 'Invalid range' unless start
102
+
103
+ finish ||= Time.now
104
+ [start, finish]
105
+ end
106
+
107
+ accept DateIntervalString do |value|
108
+ res = value.chronify_qty
109
+ raise InvalidTimeExpression, 'Invalid time quantity' unless res
110
+
111
+ res
112
+ end
74
113
 
75
114
  accept TagArray do |value|
76
115
  value.gsub(/[, ]+/, ' ').split(' ').map { |tag| tag.sub(/^@/, '')}.map(&:strip)
@@ -97,7 +136,7 @@ desc 'Use a pager when output is longer than screen'
97
136
  switch %i[p pager], default_value: settings['paginate']
98
137
 
99
138
  desc 'Answer yes/no menus with default option'
100
- switch [:default], default_value: false
139
+ switch [:default], default_value: false, negatable: false
101
140
 
102
141
  desc 'Answer all yes/no menus with yes'
103
142
  switch [:yes], negatable: false
@@ -143,6 +182,10 @@ command %i[again resume] do |c|
143
182
  c.arg_name 'SECTION_NAME'
144
183
  c.flag [:in]
145
184
 
185
+ c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
186
+ c.arg_name 'DATE_STRING'
187
+ c.flag %i[b back started], type: DateBeginString
188
+
146
189
  c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?)'
147
190
  c.arg_name 'TAG'
148
191
  c.flag [:tag], type: TagArray
@@ -198,6 +241,13 @@ command %i[again resume] do |c|
198
241
  options[:search] = search
199
242
  end
200
243
 
244
+ if options[:back]
245
+ date = options[:back]
246
+ raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
247
+ else
248
+ date = Time.now
249
+ end
250
+
201
251
  note = Doing::Note.new(options[:note])
202
252
  note.add(Doing::Prompt.read_lines(prompt: 'Add a note')) if options[:ask]
203
253
 
@@ -208,6 +258,7 @@ command %i[again resume] do |c|
208
258
  opts[:tag] = tags
209
259
  opts[:tag_bool] = options[:bool].normalize_bool
210
260
  opts[:interactive] = options[:interactive]
261
+ opts[:date] = date
211
262
 
212
263
  wwid.repeat_last(opts)
213
264
  end
@@ -321,7 +372,7 @@ desc 'Add a completed item with @done(date). No argument finishes last entry'
321
372
  long_desc 'Use this command to add an entry after you\'ve already finished it. It will be immediately marked as @done.
322
373
  You can modify the start and end times of the entry using the --back, --took, and --at flags, making it an easy
323
374
  way to add entries in post and maintain accurate (albeit manual) time tracking.'
324
- arg_name 'ENTRY'
375
+ arg_name 'ENTRY', optional: true
325
376
  command %i[done did] do |c|
326
377
  c.example 'doing done', desc: 'Tag the last entry @done'
327
378
  c.example 'doing done I already finished this', desc: 'Add a new entry and immediately mark it @done'
@@ -340,24 +391,24 @@ command %i[done did] do |c|
340
391
  c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm).
341
392
  Used with --took, backdates start date)
342
393
  c.arg_name 'DATE_STRING'
343
- c.flag %i[at finished]
394
+ c.flag %i[at finished], type: DateEndString
344
395
 
345
396
  c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
346
397
  c.arg_name 'DATE_STRING'
347
- c.flag %i[b back started]
398
+ c.flag %i[b back started], type: DateBeginString
348
399
 
349
400
  c.desc %(
350
401
  Start and end times as a date/time range `doing done --from "1am to 8am"`.
351
402
  Overrides other date flags.
352
403
  )
353
404
  c.arg_name 'TIME_RANGE'
354
- c.flag [:from]
405
+ c.flag [:from], must_match: REGEX_RANGE
355
406
 
356
407
  c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM).
357
408
  If used without the --back option, the start date will be moved back to allow
358
409
  the completion date to be the current time.)
359
410
  c.arg_name 'INTERVAL'
360
- c.flag %i[t took for]
411
+ c.flag %i[t took for], type: DateIntervalString
361
412
 
362
413
  c.desc 'Section'
363
414
  c.arg_name 'NAME'
@@ -385,23 +436,27 @@ command %i[done did] do |c|
385
436
  donedate = nil
386
437
 
387
438
  if options[:from]
388
- date, finish_date = options[:from].split_date_range
439
+ options[:from] = options[:from].split(/#{REGEX_RANGE_INDICATOR}/).map do |time|
440
+ time =~ REGEX_TIME ? "today #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}" : time
441
+ end.join(' to ').split_date_range
442
+ date, finish_date = options[:from]
389
443
  finish_date ||= Time.now
390
444
  else
391
445
  if options[:took]
392
- took = options[:took].chronify_qty
446
+ took = options[:took]
393
447
  raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
394
448
  end
395
449
 
396
450
  if options[:back]
397
- date = options[:back].chronify(guess: :begin)
451
+ date = options[:back]
398
452
  raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
399
453
  else
400
454
  date = options[:took] ? Time.now - took : Time.now
401
455
  end
402
456
 
403
457
  if options[:at]
404
- finish_date = options[:at].chronify(guess: :begin)
458
+ finish_date = options[:at]
459
+ finish_date = finish_date.chronify(guess: :begin) if finish_date.is_a? String
405
460
  raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
406
461
 
407
462
  if options[:took]
@@ -417,6 +472,7 @@ command %i[done did] do |c|
417
472
  end
418
473
 
419
474
  if options[:date]
475
+ date = date.chronify(guess: :begin, context: :today) if date =~ REGEX_TIME
420
476
  finish_date = wwid.verify_duration(date, finish_date) unless options[:took] || options[:from]
421
477
 
422
478
  donedate = finish_date.strftime('%F %R')
@@ -528,7 +584,7 @@ command %i[done did] do |c|
528
584
  wwid.content.push(new_entry)
529
585
  Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
530
586
  wwid.write(wwid.doing_file)
531
- Doing.logger.info('Entry Added:', new_entry.title)
587
+ Doing.logger.info('New entry:', %(added "#{new_entry.date.relative_date}: #{new_entry.title}" to #{section}))
532
588
  elsif $stdin.stat.size.positive?
533
589
  note = Doing::Note.new(options[:note])
534
590
  d, title, note = wwid.format_input($stdin.read.strip)
@@ -553,7 +609,7 @@ command %i[done did] do |c|
553
609
  Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
554
610
 
555
611
  wwid.write(wwid.doing_file)
556
- Doing.logger.info('Entry Added:', new_entry.title)
612
+ Doing.logger.info('New entry:', %(added "#{new_entry.date.relative_date}: #{new_entry.title}" to #{section}))
557
613
  else
558
614
  raise EmptyInput, 'You must provide content when creating a new entry'
559
615
  end
@@ -563,7 +619,7 @@ end
563
619
  # @@finish
564
620
  desc 'Mark last X entries as @done'
565
621
  long_desc 'Marks the last X entries with a @done tag and current date. Does not alter already completed entries.'
566
- arg_name 'COUNT'
622
+ arg_name 'COUNT', optional: true
567
623
  command :finish do |c|
568
624
  c.example 'doing finish', desc: 'Mark the last entry @done'
569
625
  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 +630,15 @@ command :finish do |c|
574
630
 
575
631
  c.desc 'Backdate completed date to date string [4pm|20m|2h|yesterday noon]'
576
632
  c.arg_name 'DATE_STRING'
577
- c.flag %i[b back]
633
+ c.flag %i[b back started], type: DateBeginString
578
634
 
579
635
  c.desc 'Set the completed date to the start date plus XX[hmd]'
580
636
  c.arg_name 'INTERVAL'
581
- c.flag %i[t took for]
637
+ c.flag %i[t took for], type: DateIntervalString
582
638
 
583
639
  c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.)
584
640
  c.arg_name 'DATE_STRING'
585
- c.flag [:at]
641
+ c.flag %i[at finished], type: DateEndString
586
642
 
587
643
  c.desc 'Finish the last X entries containing TAG.
588
644
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
@@ -639,7 +695,7 @@ command :finish do |c|
639
695
  options[:fuzzy] = false
640
696
  unless options[:auto]
641
697
  if options[:took]
642
- took = options[:took].chronify_qty
698
+ took = options[:took]
643
699
  raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
644
700
  end
645
701
 
@@ -648,12 +704,13 @@ command :finish do |c|
648
704
  raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
649
705
 
650
706
  if options[:at]
651
- finish_date = options[:at].chronify(guess: :begin)
707
+ finish_date = options[:at]
708
+ finish_date = finish_date.chronify(guess: :begin) if finish_date.is_a? String
652
709
  raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
653
710
 
654
711
  date = options[:took] ? finish_date - took : finish_date
655
712
  elsif options[:back]
656
- date = options[:back].chronify()
713
+ date = options[:back]
657
714
 
658
715
  raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
659
716
  else
@@ -661,8 +718,6 @@ command :finish do |c|
661
718
  end
662
719
  end
663
720
 
664
- options[:took] = options[:took].chronify_qty if options[:took]
665
-
666
721
  if options[:tag].nil?
667
722
  tags = []
668
723
  else
@@ -711,92 +766,6 @@ command :finish do |c|
711
766
  end
712
767
  end
713
768
 
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
769
  # @@mark @@flag
801
770
  desc 'Mark last entry as flagged'
802
771
  command %i[mark flag] do |c|
@@ -930,7 +899,7 @@ long_desc 'The @meanwhile tag allows you to have long-running entries that encom
930
899
  This command makes it easy to start and stop these overarching entries. Just run `doing meanwhile Starting work on this
931
900
  big project` to start a @meanwhile entry, add other entries as you work on the project, then use `doing meanwhile` by
932
901
  itself to mark the entry as @done.'
933
- arg_name 'ENTRY'
902
+ arg_name 'ENTRY', optional: true
934
903
  command :meanwhile do |c|
935
904
  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
905
  c.example 'doing meanwhile', desc: 'Finish any open @meanwhile entry'
@@ -949,7 +918,7 @@ command :meanwhile do |c|
949
918
 
950
919
  c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]'
951
920
  c.arg_name 'DATE_STRING'
952
- c.flag %i[b back]
921
+ c.flag %i[b back started], type: DateBeginString
953
922
 
954
923
  c.desc 'Note'
955
924
  c.arg_name 'TEXT'
@@ -960,7 +929,7 @@ command :meanwhile do |c|
960
929
 
961
930
  c.action do |_global_options, options, args|
962
931
  if options[:back]
963
- date = options[:back].chronify(guess: :begin)
932
+ date = options[:back]
964
933
 
965
934
  raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
966
935
  else
@@ -1020,7 +989,7 @@ long_desc %(
1020
989
 
1021
990
  Use -e to load the last entry in a text editor where you can append a note.
1022
991
  )
1023
- arg_name 'NOTE_TEXT'
992
+ arg_name 'NOTE_TEXT', optional: true
1024
993
  command :note do |c|
1025
994
  c.example 'doing note', desc: 'Open the last entry in $EDITOR to append a note'
1026
995
  c.example 'doing note "Just a quick annotation"', desc: 'Add a quick note to the last entry'
@@ -1088,7 +1057,6 @@ command :note do |c|
1088
1057
  options[:search] = search
1089
1058
  end
1090
1059
 
1091
-
1092
1060
  last_entry = wwid.last_entry(options)
1093
1061
 
1094
1062
  unless last_entry
@@ -1098,37 +1066,37 @@ command :note do |c|
1098
1066
 
1099
1067
  last_note = last_entry.note || Doing::Note.new
1100
1068
  new_note = Doing::Note.new
1101
- ask_note = options[:ask] ? Doing::Prompt.read_lines(prompt: 'Add a note') : ''
1102
1069
 
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?
1070
+ if $stdin.stat.size.positive?
1071
+ new_note.add($stdin.read.strip)
1072
+ end
1073
+
1074
+ unless args.empty?
1075
+ new_note.add(args.join(' '))
1076
+ end
1105
1077
 
1106
- input = !args.empty? ? args.join(' ') : ''
1078
+ if options[:editor]
1079
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
1107
1080
 
1108
1081
  if options[:remove]
1109
- prev_input = Doing::Note.new
1082
+ input = Doing::Note.new
1110
1083
  else
1111
- prev_input = last_entry.note || Doing::Note.new
1084
+ input = last_entry.note || Doing::Note.new
1112
1085
  end
1113
1086
 
1087
+ input.add(new_note)
1114
1088
 
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
1089
+ new_note = Doing::Note.new(wwid.fork_editor(input.strip_lines.join("\n"), message: nil).strip)
1120
1090
  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?
1091
+ end
1128
1092
 
1093
+ if (new_note.empty? && !options[:remove]) || options[:ask]
1094
+ $stderr.puts last_note unless last_note.empty?
1095
+ $stderr.puts new_note unless new_note.empty?
1096
+ new_note.add(Doing::Prompt.read_lines(prompt: 'Add a note'))
1129
1097
  end
1130
1098
 
1131
- new_note.add(ask_note) unless ask_note.empty?
1099
+ raise EmptyInput, 'You must provide content when adding a note' unless options[:remove] || !new_note.empty?
1132
1100
 
1133
1101
  if last_note.equal?(new_note)
1134
1102
  Doing.logger.debug('Skipped:', 'No note change')
@@ -1169,7 +1137,7 @@ command %i[now next] do |c|
1169
1137
 
1170
1138
  c.desc 'Backdate start time [4pm|20m|2h|"yesterday noon"]'
1171
1139
  c.arg_name 'DATE_STRING'
1172
- c.flag %i[b back started]
1140
+ c.flag %i[b back started], type: DateBeginString
1173
1141
 
1174
1142
  c.desc 'Timed entry, marks last entry in section as @done'
1175
1143
  c.switch %i[f finish_last], negatable: false, default_value: false
@@ -1187,7 +1155,7 @@ command %i[now next] do |c|
1187
1155
 
1188
1156
  c.action do |_global_options, options, args|
1189
1157
  if options[:back]
1190
- date = options[:back].chronify(guess: :begin)
1158
+ date = options[:back]
1191
1159
 
1192
1160
  raise InvalidTimeExpression.new('unable to parse date string', topic: 'Parser:') if date.nil?
1193
1161
  else
@@ -1211,7 +1179,7 @@ command %i[now next] do |c|
1211
1179
  input += "\n#{ask_note}" unless ask_note.empty?
1212
1180
  input = wwid.fork_editor(input).strip
1213
1181
 
1214
- date, title, note = wwid.format_input(input)
1182
+ d, title, note = wwid.format_input(input)
1215
1183
  raise EmptyInput, 'No content' if title.strip.empty?
1216
1184
 
1217
1185
  if ask_note.empty? && options[:ask]
@@ -1219,6 +1187,7 @@ command %i[now next] do |c|
1219
1187
  note.add(ask_note) unless ask_note.empty?
1220
1188
  end
1221
1189
 
1190
+ date = d.nil? ? date : d
1222
1191
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1223
1192
  wwid.write(wwid.doing_file)
1224
1193
  elsif args.length.positive?
@@ -1265,7 +1234,7 @@ desc 'Reset the start time of an entry'
1265
1234
  long_desc 'Update the start time of the last entry or the last entry matching a tag/search filter.
1266
1235
  If no argument is provided, the start time will be reset to the current time.
1267
1236
  If a date string is provided as an argument, the start time will be set to the parsed result.'
1268
- arg_name 'DATE_STRING'
1237
+ arg_name 'DATE_STRING', optional: true
1269
1238
  command %i[reset begin] do |c|
1270
1239
  c.example 'doing reset', desc: 'Reset the start time of the last entry to the current time'
1271
1240
  c.example 'doing reset --tag project1', desc: 'Reset the start time of the most recent entry tagged @project1 to the current time'
@@ -1279,6 +1248,9 @@ command %i[reset begin] do |c|
1279
1248
  c.desc 'Resume entry (remove @done)'
1280
1249
  c.switch %i[r resume], default_value: true
1281
1250
 
1251
+ c.desc 'Change start date but do not remove @done (shortcut for --no-resume)'
1252
+ c.switch [:n]
1253
+
1282
1254
  c.desc 'Reset last entry matching tag. Wildcards allowed (*, ?)'
1283
1255
  c.arg_name 'TAG'
1284
1256
  c.flag [:tag]
@@ -1416,11 +1388,11 @@ command :select do |c|
1416
1388
 
1417
1389
  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
1390
  c.arg_name 'DATE_STRING'
1419
- c.flag [:before]
1391
+ c.flag [:before], type: DateBeginString
1420
1392
 
1421
1393
  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
1394
  c.arg_name 'DATE_STRING'
1423
- c.flag [:after]
1395
+ c.flag [:after], type: DateEndString
1424
1396
 
1425
1397
  c.desc %(
1426
1398
  Date range to show, or a single day to filter date on.
@@ -1431,7 +1403,7 @@ command :select do |c|
1431
1403
  by time of day.
1432
1404
  )
1433
1405
  c.arg_name 'DATE_OR_RANGE'
1434
- c.flag [:from]
1406
+ c.flag [:from], type: DateRangeString
1435
1407
 
1436
1408
  c.desc 'Force exact search string matching (case sensitive)'
1437
1409
  c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
@@ -1698,11 +1670,11 @@ command %i[grep search] do |c|
1698
1670
 
1699
1671
  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
1672
  c.arg_name 'DATE_STRING'
1701
- c.flag [:before]
1673
+ c.flag [:before], type: DateBeginString
1702
1674
 
1703
1675
  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
1676
  c.arg_name 'DATE_STRING'
1705
- c.flag [:after]
1677
+ c.flag [:after], type: DateEndString
1706
1678
 
1707
1679
  c.desc %(
1708
1680
  Date range to show, or a single day to filter date on.
@@ -1713,12 +1685,20 @@ command %i[grep search] do |c|
1713
1685
  by time of day.
1714
1686
  )
1715
1687
  c.arg_name 'DATE_OR_RANGE'
1716
- c.flag [:from]
1688
+ c.flag [:from], type: DateRangeString
1717
1689
 
1718
1690
  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1719
1691
  c.arg_name 'FORMAT'
1720
1692
  c.flag %i[o output]
1721
1693
 
1694
+ c.desc "Output using a template from configuration"
1695
+ c.arg_name 'TEMPLATE_KEY'
1696
+ c.flag [:config_template], type: TemplateName, default_value: 'default'
1697
+
1698
+ c.desc 'Override output format with a template string containing %placeholders'
1699
+ c.arg_name 'TEMPLATE_STRING'
1700
+ c.flag [:template]
1701
+
1722
1702
  c.desc 'Show time intervals on @done tasks'
1723
1703
  c.switch %i[t times], default_value: true, negatable: true
1724
1704
 
@@ -1773,7 +1753,7 @@ command %i[grep search] do |c|
1773
1753
  options[:fuzzy] = false
1774
1754
  raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1775
1755
 
1776
- template = settings['templates']['default'].deep_merge(settings)
1756
+ template = settings['templates'][options[:config_template]].deep_merge(settings)
1777
1757
  tags_color = template.key?('tags_color') ? template['tags_color'] : nil
1778
1758
 
1779
1759
  section = wwid.guess_section(options[:section]) if options[:section]
@@ -1830,6 +1810,14 @@ command :last do |c|
1830
1810
  c.arg_name 'QUERY'
1831
1811
  c.flag [:search]
1832
1812
 
1813
+ c.desc "Output using a template from configuration"
1814
+ c.arg_name 'TEMPLATE_KEY'
1815
+ c.flag [:config_template], type: TemplateName, default_value: 'last'
1816
+
1817
+ c.desc 'Override output format with a template string containing %placeholders'
1818
+ c.arg_name 'TEMPLATE_STRING'
1819
+ c.flag [:template]
1820
+
1833
1821
  c.desc "Highlight search matches in output. Only affects command line output"
1834
1822
  c.switch %i[h hilite], default_value: settings.dig('search', 'highlight')
1835
1823
 
@@ -1883,6 +1871,8 @@ command :last do |c|
1883
1871
  else
1884
1872
  last = wwid.last(times: true, section: options[:section],
1885
1873
  options: {
1874
+ config_template: options[:config_template],
1875
+ template: options[:template],
1886
1876
  duration: options[:duration],
1887
1877
  search: options[:search],
1888
1878
  fuzzy: options[:fuzzy],
@@ -1917,6 +1907,14 @@ command :recent do |c|
1917
1907
  c.desc 'Show time intervals on @done tasks'
1918
1908
  c.switch %i[t times], default_value: true, negatable: true
1919
1909
 
1910
+ c.desc "Output using a template from configuration"
1911
+ c.arg_name 'TEMPLATE_KEY'
1912
+ c.flag [:config_template], type: TemplateName, default_value: 'recent'
1913
+
1914
+ c.desc 'Override output format with a template string containing %placeholders'
1915
+ c.arg_name 'TEMPLATE_STRING'
1916
+ c.flag [:template]
1917
+
1920
1918
  c.desc 'Show elapsed time on entries without @done tag'
1921
1919
  c.switch [:duration]
1922
1920
 
@@ -1960,7 +1958,9 @@ command :recent do |c|
1960
1958
  times: options[:times],
1961
1959
  totals: options[:totals],
1962
1960
  interactive: options[:interactive],
1963
- duration: options[:duration]
1961
+ duration: options[:duration],
1962
+ config_template: options[:config_template],
1963
+ template: options[:template]
1964
1964
  }
1965
1965
 
1966
1966
  Doing::Pager::page wwid.recent(count, section.cap_first, opts)
@@ -2011,11 +2011,11 @@ command :show do |c|
2011
2011
 
2012
2012
  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
2013
  c.arg_name 'DATE_STRING'
2014
- c.flag [:before]
2014
+ c.flag [:before], type: DateBeginString
2015
2015
 
2016
2016
  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
2017
  c.arg_name 'DATE_STRING'
2018
- c.flag [:after]
2018
+ c.flag [:after], type: DateEndString
2019
2019
 
2020
2020
  c.desc %(
2021
2021
  Date range to show, or a single day to filter date on.
@@ -2027,7 +2027,7 @@ command :show do |c|
2027
2027
  )
2028
2028
 
2029
2029
  c.arg_name 'DATE_OR_RANGE'
2030
- c.flag [:from]
2030
+ c.flag [:from], type: DateRangeString
2031
2031
 
2032
2032
  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2033
2033
  c.arg_name 'QUERY'
@@ -2075,6 +2075,14 @@ command :show do |c|
2075
2075
  c.desc 'Only show items with recorded time intervals'
2076
2076
  c.switch [:only_timed], default_value: false, negatable: false
2077
2077
 
2078
+ c.desc "Output using a template from configuration"
2079
+ c.arg_name 'TEMPLATE_KEY'
2080
+ c.flag [:config_template], type: TemplateName, default_value: 'default'
2081
+
2082
+ c.desc 'Override output format with a template string containing %placeholders'
2083
+ c.arg_name 'TEMPLATE_STRING'
2084
+ c.flag [:template]
2085
+
2078
2086
  c.desc 'Select section or tag to display from a menu'
2079
2087
  c.switch %i[m menu], negatable: false, default_value: false
2080
2088
 
@@ -2131,7 +2139,7 @@ command :show do |c|
2131
2139
 
2132
2140
  options[:times] = true if options[:totals]
2133
2141
 
2134
- template = settings['templates']['default'].deep_merge({
2142
+ template = settings['templates'][options[:config_template]].deep_merge({
2135
2143
  'wrap_width' => settings['wrap_width'] || 0,
2136
2144
  'date_format' => settings['default_date_format'],
2137
2145
  'order' => settings['order'] || 'asc',
@@ -2195,6 +2203,7 @@ end
2195
2203
 
2196
2204
  # @@tags
2197
2205
  desc 'List all tags in the current Doing file'
2206
+ arg_name 'MAX_COUNT', optional: true, type: Integer
2198
2207
  command :tags do |c|
2199
2208
  c.desc 'Section'
2200
2209
  c.arg_name 'SECTION_NAME'
@@ -2203,6 +2212,9 @@ command :tags do |c|
2203
2212
  c.desc 'Show count of occurrences'
2204
2213
  c.switch %i[c counts]
2205
2214
 
2215
+ c.desc 'Output in a single line with @ symbols. Ignored if --counts is specified.'
2216
+ c.switch %i[l line]
2217
+
2206
2218
  c.desc 'Sort by name or count'
2207
2219
  c.arg_name 'SORT_ORDER'
2208
2220
  c.flag %i[sort], default_value: 'name', must_match: /^(?:n(?:ame)?|c(?:ount)?)$/
@@ -2246,6 +2258,7 @@ command :tags do |c|
2246
2258
 
2247
2259
  c.action do |_global, options, args|
2248
2260
  section = wwid.guess_section(options[:section]) || options[:section].cap_first
2261
+ options[:count] = args.count.positive? ? args[0].to_i : 0
2249
2262
 
2250
2263
  items = wwid.filter_items([], opt: options)
2251
2264
 
@@ -2273,7 +2286,11 @@ command :tags do |c|
2273
2286
  if options[:counts]
2274
2287
  tags.each { |t, c| puts "#{t} (#{c})" }
2275
2288
  else
2276
- tags.each { |t, c| puts "#{t}" }
2289
+ if options[:line]
2290
+ puts tags.map { |t, c| t }.to_tags.join(' ')
2291
+ else
2292
+ tags.each { |t, c| puts "#{t}" }
2293
+ end
2277
2294
  end
2278
2295
  end
2279
2296
  end
@@ -2311,6 +2328,14 @@ command :today do |c|
2311
2328
  c.arg_name 'FORMAT'
2312
2329
  c.flag %i[o output]
2313
2330
 
2331
+ c.desc "Output using a template from configuration"
2332
+ c.arg_name 'TEMPLATE_KEY'
2333
+ c.flag [:config_template], type: TemplateName, default_value: 'today'
2334
+
2335
+ c.desc 'Override output format with a template string containing %placeholders'
2336
+ c.arg_name 'TEMPLATE_STRING'
2337
+ c.flag [:template]
2338
+
2314
2339
  c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
2315
2340
  c.arg_name 'TIME_STRING'
2316
2341
  c.flag [:before]
@@ -2322,21 +2347,21 @@ command :today do |c|
2322
2347
  c.desc %(
2323
2348
  Time range to show `doing today --from "12pm to 4pm"`
2324
2349
  )
2325
- c.arg_name 'DATE_OR_RANGE'
2326
- c.flag [:from]
2350
+ c.arg_name 'TIME_RANGE'
2351
+ c.flag [:from], type: DateRangeString
2327
2352
 
2328
2353
  c.action do |_global_options, options, _args|
2329
2354
  raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
2330
2355
 
2331
2356
  options[:times] = true if options[:totals]
2332
2357
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
2333
- filter_options = %i[after before duration from section sort_tags totals].each_with_object({}) { |k, hsh| hsh[k] = options[k] }
2358
+ filter_options = %i[after before duration from section sort_tags totals template config_template].each_with_object({}) { |k, hsh| hsh[k] = options[k] }
2334
2359
 
2335
2360
  Doing::Pager.page wwid.today(options[:times], options[:output], filter_options).chomp
2336
2361
  end
2337
2362
  end
2338
2363
 
2339
- # @on
2364
+ # @@on
2340
2365
  desc 'List entries for a date'
2341
2366
  long_desc %(Date argument can be natural language. "thursday" would be interpreted as "last thursday,"
2342
2367
  and "2d" would be interpreted as "two days ago." If you use "to" or "through" between two dates,
@@ -2370,21 +2395,24 @@ command :on do |c|
2370
2395
  c.arg_name 'FORMAT'
2371
2396
  c.flag %i[o output]
2372
2397
 
2398
+ c.desc "Output using a template from configuration"
2399
+ c.arg_name 'TEMPLATE_KEY'
2400
+ c.flag [:config_template], type: TemplateName, default_value: 'default'
2401
+
2402
+ c.desc 'Override output format with a template string containing %placeholders'
2403
+ c.arg_name 'TEMPLATE_STRING'
2404
+ c.flag [:template]
2405
+
2373
2406
  c.action do |_global_options, options, args|
2374
2407
  raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
2375
2408
 
2376
2409
  raise MissingArgument, 'Missing date argument' if args.empty?
2377
2410
 
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
2411
+ date_string = args.join(' ').strip
2412
+ if date_string =~ /^tod(?:ay)?/i
2413
+ date_string = 'today to tomorrow 12am'
2387
2414
  end
2415
+ start, finish = date_string.split_date_range
2388
2416
 
2389
2417
  raise InvalidTimeExpression, 'Unrecognized date string' unless start
2390
2418
 
@@ -2396,7 +2424,7 @@ command :on do |c|
2396
2424
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
2397
2425
 
2398
2426
  Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
2399
- { duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
2427
+ { template: options[:template], config_template: options[:config_template], duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
2400
2428
  end
2401
2429
  end
2402
2430
 
@@ -2432,6 +2460,14 @@ command :since do |c|
2432
2460
  c.arg_name 'FORMAT'
2433
2461
  c.flag %i[o output]
2434
2462
 
2463
+ c.desc "Output using a template from configuration"
2464
+ c.arg_name 'TEMPLATE_KEY'
2465
+ c.flag [:config_template], type: TemplateName, default_value: 'default'
2466
+
2467
+ c.desc 'Override output format with a template string containing %placeholders'
2468
+ c.arg_name 'TEMPLATE_STRING'
2469
+ c.flag [:template]
2470
+
2435
2471
  c.action do |_global_options, options, args|
2436
2472
  raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
2437
2473
 
@@ -2453,7 +2489,7 @@ command :since do |c|
2453
2489
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
2454
2490
 
2455
2491
  Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
2456
- { duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
2492
+ { template: options[:template], config_template: options[:config_template], duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
2457
2493
  end
2458
2494
  end
2459
2495
 
@@ -2536,11 +2572,11 @@ command :view do |c|
2536
2572
 
2537
2573
  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
2574
  c.arg_name 'DATE_STRING'
2539
- c.flag [:before]
2575
+ c.flag [:before], type: DateBeginString
2540
2576
 
2541
2577
  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
2578
  c.arg_name 'DATE_STRING'
2543
- c.flag [:after]
2579
+ c.flag [:after], type: DateEndString
2544
2580
 
2545
2581
  c.desc %(
2546
2582
  Date range to show, or a single day to filter date on.
@@ -2551,7 +2587,7 @@ command :view do |c|
2551
2587
  by time of day.
2552
2588
  )
2553
2589
  c.arg_name 'DATE_OR_RANGE'
2554
- c.flag [:from]
2590
+ c.flag [:from], type: DateRangeString
2555
2591
 
2556
2592
  c.desc 'Only show items with recorded time intervals (override view settings)'
2557
2593
  c.switch [:only_timed], default_value: false, negatable: false
@@ -2588,17 +2624,17 @@ command :view do |c|
2588
2624
  view = wwid.get_view(title)
2589
2625
 
2590
2626
  if view
2591
- page_title = view.key?('title') ? view['title'] : title.cap_first
2627
+ page_title = view['title'] || title.cap_first
2592
2628
  only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed]
2593
2629
  true
2594
2630
  else
2595
2631
  false
2596
2632
  end
2597
2633
 
2598
- template = view.key?('template') ? view['template'] : nil
2599
- date_format = view.key?('date_format') ? view['date_format'] : nil
2634
+ template = view['template'] || nil
2635
+ date_format = view['date_format'] || nil
2600
2636
 
2601
- tags_color = view.key?('tags_color') ? view['tags_color'] : nil
2637
+ tags_color = view['tags_color'] || nil
2602
2638
  tag_filter = false
2603
2639
  if options[:tag]
2604
2640
  tag_filter = { 'tags' => [], 'bool' => 'OR' }
@@ -2628,19 +2664,19 @@ command :view do |c|
2628
2664
  section = if options[:section]
2629
2665
  section
2630
2666
  else
2631
- view.key?('section') ? view['section'] : settings['current_section']
2667
+ view['section'] || settings['current_section']
2632
2668
  end
2633
- order = view.key?('order') ? view['order'].normalize_order : 'asc'
2669
+ order = view['order']&.normalize_order || 'asc'
2634
2670
 
2635
2671
  totals = if options[:totals]
2636
2672
  true
2637
2673
  else
2638
- view.key?('totals') ? view['totals'] : false
2674
+ view['totals'] || false
2639
2675
  end
2640
2676
  tag_order = if options[:tag_order]
2641
2677
  options[:tag_order].normalize_order
2642
2678
  else
2643
- view.key?('tag_order') ? view['tag_order'].normalize_order : 'asc'
2679
+ view['tag_order']&.normalize_order || 'asc'
2644
2680
  end
2645
2681
 
2646
2682
  options[:times] = true if totals
@@ -2669,7 +2705,6 @@ command :view do |c|
2669
2705
 
2670
2706
  opts = options.dup
2671
2707
  opts[:age] = options[:age].normalize_age(:newest)
2672
- opts[:view_template] = title
2673
2708
  opts[:count] = count
2674
2709
  opts[:format] = date_format
2675
2710
  opts[:highlight] = options[:color]
@@ -2686,6 +2721,7 @@ command :view do |c|
2686
2721
  opts[:tags_color] = tags_color
2687
2722
  opts[:template] = template
2688
2723
  opts[:totals] = totals
2724
+ opts[:view_template] = title
2689
2725
 
2690
2726
  Doing::Pager.page wwid.list_section(opts)
2691
2727
  elsif title.instance_of?(FalseClass)
@@ -2713,6 +2749,14 @@ command :yesterday do |c|
2713
2749
  c.arg_name 'FORMAT'
2714
2750
  c.flag %i[o output]
2715
2751
 
2752
+ c.desc "Output using a template from configuration"
2753
+ c.arg_name 'TEMPLATE_KEY'
2754
+ c.flag [:config_template], type: TemplateName, default_value: 'today'
2755
+
2756
+ c.desc 'Override output format with a template string containing %placeholders'
2757
+ c.arg_name 'TEMPLATE_STRING'
2758
+ c.flag [:template]
2759
+
2716
2760
  c.desc 'Show time intervals on @done tasks'
2717
2761
  c.switch %i[t times], default_value: true, negatable: true
2718
2762
 
@@ -2735,11 +2779,9 @@ command :yesterday do |c|
2735
2779
  c.arg_name 'TIME_STRING'
2736
2780
  c.flag [:after]
2737
2781
 
2738
- c.desc %(
2739
- Time range to show, e.g. `doing yesterday --from "1am to 8am"`
2740
- )
2782
+ c.desc 'Time range to show, e.g. `doing yesterday --from "1am to 8am"`'
2741
2783
  c.arg_name 'TIME_RANGE'
2742
- c.flag [:from]
2784
+ c.flag [:from], must_match: REGEX_TIME_RANGE
2743
2785
 
2744
2786
  c.desc 'Tag sort direction (asc|desc)'
2745
2787
  c.arg_name 'DIRECTION'
@@ -2751,21 +2793,15 @@ command :yesterday do |c|
2751
2793
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
2752
2794
 
2753
2795
  if options[:from]
2754
- options[:from] = options[:from].split(/ (?:to|through|thru|(?:un)?til|-+) /).map do |time|
2796
+ options[:from] = options[:from].split(/#{REGEX_RANGE_INDICATOR}/).map do |time|
2755
2797
  "yesterday #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}"
2756
- end.join(' to ')
2757
- end
2758
-
2759
- opt = {
2760
- after: options[:after],
2761
- before: options[:before],
2762
- duration: options[:duration],
2763
- from: options[:from],
2764
- sort_tags: options[:sort_tags],
2765
- tag_order: options[:tag_order].normalize_order,
2766
- totals: options[:totals],
2767
- order: settings.dig('templates', 'today', 'order')
2768
- }
2798
+ end.join(' to ').split_date_range
2799
+ end
2800
+
2801
+ opt = options.dup
2802
+ opt[:tag_order] = options[:tag_order].normalize_order
2803
+ opt[:order] = settings.dig('templates', options[:config_template], 'order')
2804
+
2769
2805
  Doing::Pager.page wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp
2770
2806
  end
2771
2807
  end
@@ -2991,8 +3027,7 @@ command :config do |c|
2991
3027
  value = options[:remove] ? nil : args.pop
2992
3028
  keypath = args.join('.')
2993
3029
  real_path = config.resolve_key_path(keypath, create: true)
2994
-
2995
- old_value = settings.dig(*real_path) || nil
3030
+ old_value = settings.dig(*real_path)
2996
3031
  old_type = old_value&.class.to_s || nil
2997
3032
 
2998
3033
  if old_value.is_a?(Hash) && !options[:remove]
@@ -3018,7 +3053,6 @@ command :config do |c|
3018
3053
  else
3019
3054
  current_value = cfg.dig(*real_path)
3020
3055
  cfg.deep_set(real_path, value.set_type(old_type))
3021
-
3022
3056
  $stderr.puts "#{' Key path:'.yellow} #{real_path.join('->').boldwhite}"
3023
3057
  $stderr.puts "#{'Inherited:'.yellow} #{(old_value ? old_value.to_s : 'empty').boldwhite}"
3024
3058
  $stderr.puts "#{' Current:'.yellow} #{ (current_value ? current_value.to_s : 'empty').boldwhite }"
@@ -3178,7 +3212,7 @@ command %i[archive move] do |c|
3178
3212
  c.desc 'Archive entries older than date
3179
3213
  (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
3180
3214
  c.arg_name 'DATE_STRING'
3181
- c.flag [:before]
3215
+ c.flag [:before], type: DateEndString
3182
3216
 
3183
3217
  c.action do |_global_options, options, args|
3184
3218
  options[:fuzzy] = false
@@ -3269,11 +3303,11 @@ command :import do |c|
3269
3303
  # TODO: Allow time range filtering
3270
3304
  c.desc 'Import entries older than date'
3271
3305
  c.arg_name 'DATE_STRING'
3272
- c.flag [:before]
3306
+ c.flag [:before], type: DateBeginString
3273
3307
 
3274
3308
  c.desc 'Import entries newer than date'
3275
3309
  c.arg_name 'DATE_STRING'
3276
- c.flag [:after]
3310
+ c.flag [:after], type: DateEndString
3277
3311
 
3278
3312
  c.desc %(
3279
3313
  Date range to import. Date range argument should be quoted. Date specifications can be natural language.
@@ -3281,7 +3315,7 @@ command :import do |c|
3281
3315
  Has no effect unless the import plugin has implemented date range filtering.
3282
3316
  )
3283
3317
  c.arg_name 'DATE_OR_RANGE'
3284
- c.flag %i[f from]
3318
+ c.flag %i[f from], type: DateRangeString
3285
3319
 
3286
3320
  c.desc 'Allow entries that overlap existing times'
3287
3321
  c.switch [:overlap], negatable: true
@@ -3299,24 +3333,19 @@ command :import do |c|
3299
3333
  end
3300
3334
 
3301
3335
  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]
3336
+ options[:date_filter] = options[:from]
3337
+
3338
+ raise InvalidTimeExpression, 'Unrecognized date string' unless options[:date_filter][0]
3339
+ elsif options[:before] || options[:after]
3340
+ options[:date_filter] = [nil, nil]
3341
+ options[:date_filter][1] = options[:before] || Time.now + (1 << 64)
3342
+ options[:date_filter][0] = options[:after] || Time.now - (1 << 64)
3313
3343
  end
3314
3344
 
3315
3345
  options[:case] = options[:case].normalize_case
3316
3346
 
3317
3347
  if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
3318
3348
  options[:no_overlap] = !options[:overlap]
3319
- options[:date_filter] = dates
3320
3349
  wwid.import(args, options)
3321
3350
  wwid.write(wwid.doing_file)
3322
3351
  else
@@ -3633,17 +3662,44 @@ end
3633
3662
 
3634
3663
  # @@changelog @@changes
3635
3664
  desc 'List recent changes in Doing'
3636
- long_desc 'Display a formatted list of changes in recent versions, latest at the top'
3637
- command %i[changelog changes] do |c|
3665
+ long_desc %(Display a formatted list of changes in recent versions.
3666
+
3667
+ Without flags, displays only the most recent version.
3668
+ Use --lookup or --all for history.)
3669
+ command %i[changes changelog] do |c|
3670
+ c.desc 'Display all versions'
3671
+ c.switch %i[a all], default_value: false, negatable: false
3672
+
3673
+ c.desc %(Look up a specific version. Specify versions as "MAJ.MIN.PATCH", MIN
3674
+ and PATCH are optional. Use > or < to see all changes since or prior
3675
+ to a version.)
3676
+ c.arg_name 'VERSION'
3677
+ c.flag %i[l lookup], must_match: /^(?:(?:(?:[<>=]|p(?:rior)|b(?:efore)|o(?:lder)|s(?:ince)|a(?:fter)|n(?:ewer))? *[\d.*?]+ *)+|(?:[\d.]+ *-+ *[\d.]+))$/
3678
+
3679
+ c.desc %(Show changelogs matching search terms (uses pattern-based searching).
3680
+ Add slashes to search with regular expressions, e.g. `--search "/output.*flag/"`)
3681
+ c.flag %i[s search]
3682
+
3683
+ c.example 'doing changes', desc: 'View changes in the current version'
3684
+ c.example 'doing changes --all', desc: 'See the entire changelog'
3685
+ c.example 'doing changes --lookup 2.0.21', desc: 'See changes from version 2.0.21'
3686
+ c.example 'doing changes --lookup "> 2.1"', desc: 'See all changes since 2.1.0'
3687
+ c.example 'doing changes --search "tags +bool"', desc: 'See all changes containing "tags" and "bool"'
3688
+ c.example 'doing changes -l "> 2.1" -s "pattern"', desc: 'Lookup and search can be combined'
3689
+
3690
+
3638
3691
  c.action do |_global_options, options, args|
3639
- changelog = File.expand_path(File.join(File.dirname(__FILE__), '..', 'CHANGELOG.md'))
3640
- if File.exist?(changelog)
3641
- parsed = TTY::Markdown.parse(IO.read(changelog), width: 80, symbols: {override: {bullet: "•"}})
3642
- Doing::Pager.paginate = true
3643
- Doing::Pager.page parsed
3644
- else
3645
- raise "Error locating changelog"
3646
- end
3692
+ cl = Doing::Changes.new(lookup: options[:lookup], search: options[:search])
3693
+
3694
+ content = if options[:all] || options[:search] || options[:lookup]
3695
+ cl.to_s
3696
+ else
3697
+ cl.latest
3698
+ end
3699
+
3700
+ parsed = TTY::Markdown.parse(content, width: 80, symbols: {override: {bullet: "•"}})
3701
+ Doing::Pager.paginate = true
3702
+ Doing::Pager.page parsed
3647
3703
  end
3648
3704
  end
3649
3705
 
@@ -3667,9 +3723,9 @@ command :commands_accepting do |c|
3667
3723
  end
3668
3724
 
3669
3725
  if o[:column]
3670
- puts cmds
3726
+ puts cmds.sort
3671
3727
  else
3672
- puts "Commands accepting --#{option}: #{cmds.join(', ')}"
3728
+ puts "Commands accepting --#{option}: #{cmds.sort.join(', ')}"
3673
3729
  end
3674
3730
  end
3675
3731
  end
@@ -3700,7 +3756,7 @@ pre do |global, _command, _options, _args|
3700
3756
  Doing::Pager.paginate = global[:pager]
3701
3757
 
3702
3758
  $stdout.puts "doing v#{Doing::VERSION}" if global[:version]
3703
- unless STDOUT.isatty
3759
+ unless $stdout.isatty
3704
3760
  Doing::Color.coloring = global[:pager] ? global[:color] : false
3705
3761
  else
3706
3762
  Doing::Color.coloring = global[:color]
@@ -3714,7 +3770,9 @@ pre do |global, _command, _options, _args|
3714
3770
  end
3715
3771
 
3716
3772
  on_error do |exception|
3717
- if exception.kind_of?(SystemExit)
3773
+ if exception.kind_of?(GLI::UnknownCommand)
3774
+ exit run(['now'].concat(ARGV))
3775
+ elsif exception.kind_of?(SystemExit)
3718
3776
  false
3719
3777
  else
3720
3778
  # Doing.logger.error('Fatal:', exception)
@@ -3748,7 +3806,12 @@ around do |global, command, options, arguments, code|
3748
3806
  Doing::Prompt.force_answer = false
3749
3807
  Doing.config.force_answer = false
3750
3808
  else
3751
- Doing::Prompt.default_answer = global[:default]
3809
+ Doing::Prompt.default_answer = if $stdout.isatty
3810
+ global[:default]
3811
+ else
3812
+ true
3813
+ end
3814
+
3752
3815
  Doing.config.force_answer = global[:default] ? true : false
3753
3816
  end
3754
3817