doing 2.0.23 → 2.1.1pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +19 -15
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +40 -1
  6. data/Gemfile.lock +8 -1
  7. data/README.md +7 -1
  8. data/Rakefile +23 -4
  9. data/bin/doing +431 -256
  10. data/doc/Array.html +354 -1
  11. data/doc/Doing/Color.html +104 -92
  12. data/doc/Doing/Completion.html +216 -0
  13. data/doc/Doing/Configuration.html +340 -5
  14. data/doc/Doing/Content.html +229 -0
  15. data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  16. data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  17. data/doc/Doing/Errors/DoingStandardError.html +1 -1
  18. data/doc/Doing/Errors/EmptyInput.html +1 -1
  19. data/doc/Doing/Errors/NoResults.html +1 -1
  20. data/doc/Doing/Errors/PluginException.html +1 -1
  21. data/doc/Doing/Errors/UserCancelled.html +1 -1
  22. data/doc/Doing/Errors/WrongCommand.html +1 -1
  23. data/doc/Doing/Errors.html +1 -1
  24. data/doc/Doing/Hooks.html +1 -1
  25. data/doc/Doing/Item.html +337 -49
  26. data/doc/Doing/Items.html +444 -35
  27. data/doc/Doing/LogAdapter.html +139 -51
  28. data/doc/Doing/Note.html +253 -22
  29. data/doc/Doing/Pager.html +74 -36
  30. data/doc/Doing/Plugins.html +1 -1
  31. data/doc/Doing/Prompt.html +674 -0
  32. data/doc/Doing/Section.html +354 -0
  33. data/doc/Doing/Util.html +57 -1
  34. data/doc/Doing/WWID.html +517 -890
  35. data/doc/Doing/WWIDFile.html +398 -0
  36. data/doc/Doing.html +5 -5
  37. data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  38. data/doc/GLI/Commands.html +1 -1
  39. data/doc/GLI.html +1 -1
  40. data/doc/Hash.html +97 -1
  41. data/doc/Status.html +37 -3
  42. data/doc/String.html +833 -53
  43. data/doc/Symbol.html +3 -3
  44. data/doc/Time.html +1 -1
  45. data/doc/_index.html +22 -1
  46. data/doc/class_list.html +1 -1
  47. data/doc/file.README.html +8 -2
  48. data/doc/index.html +8 -2
  49. data/doc/method_list.html +460 -180
  50. data/doc/top-level-namespace.html +1 -1
  51. data/doing.gemspec +3 -0
  52. data/doing.rdoc +163 -44
  53. data/example_plugin.rb +5 -5
  54. data/lib/completion/_doing.zsh +42 -42
  55. data/lib/completion/doing.bash +21 -21
  56. data/lib/completion/doing.fish +1 -280
  57. data/lib/doing/array.rb +36 -0
  58. data/lib/doing/colors.rb +70 -66
  59. data/lib/doing/completion/bash_completion.rb +1 -2
  60. data/lib/doing/completion/fish_completion.rb +1 -1
  61. data/lib/doing/completion/zsh_completion.rb +1 -1
  62. data/lib/doing/completion.rb +6 -0
  63. data/lib/doing/configuration.rb +134 -23
  64. data/lib/doing/hash.rb +37 -0
  65. data/lib/doing/item.rb +77 -12
  66. data/lib/doing/items.rb +125 -0
  67. data/lib/doing/log_adapter.rb +58 -4
  68. data/lib/doing/note.rb +53 -1
  69. data/lib/doing/pager.rb +49 -38
  70. data/lib/doing/plugins/export/markdown_export.rb +4 -4
  71. data/lib/doing/plugins/export/template_export.rb +2 -2
  72. data/lib/doing/plugins/import/calendar_import.rb +4 -4
  73. data/lib/doing/plugins/import/doing_import.rb +5 -7
  74. data/lib/doing/plugins/import/timing_import.rb +3 -3
  75. data/lib/doing/prompt.rb +206 -0
  76. data/lib/doing/section.rb +30 -0
  77. data/lib/doing/string.rb +123 -35
  78. data/lib/doing/string_chronify.rb +81 -0
  79. data/lib/doing/util.rb +14 -6
  80. data/lib/doing/version.rb +1 -1
  81. data/lib/doing/wwid.rb +387 -685
  82. data/lib/doing.rb +7 -2
  83. data/lib/examples/plugins/capture_thing_import.rb +162 -0
  84. data/rdoc_to_mmd.rb +14 -8
  85. data/scripts/generate_bash_completions.rb +1 -1
  86. data/scripts/generate_fish_completions.rb +1 -1
  87. data/scripts/generate_zsh_completions.rb +1 -1
  88. metadata +74 -5
  89. data/lib/doing/wwidfile.rb +0 -117
data/bin/doing CHANGED
@@ -35,8 +35,10 @@ colors = Doing::Color
35
35
  wwid = Doing::WWID.new
36
36
 
37
37
  Doing.logger.log_level = :info
38
+ env_log_level = nil
38
39
 
39
40
  if ENV['DOING_LOG_LEVEL'] || ENV['DOING_DEBUG'] || ENV['DOING_QUIET'] || ENV['DOING_VERBOSE'] || ENV['DOING_PLUGIN_DEBUG']
41
+ env_log_level = true
40
42
  # Quiet always wins
41
43
  if ENV['DOING_QUIET'] && ENV['DOING_QUIET'].truthy?
42
44
  Doing.logger.log_level = :error
@@ -82,6 +84,12 @@ switch %i[p pager], default_value: settings['paginate']
82
84
  desc 'Answer yes/no menus with default option'
83
85
  switch [:default], default_value: false
84
86
 
87
+ desc 'Answer all yes/no menus with yes'
88
+ switch [:yes], negatable: false
89
+
90
+ desc 'Answer all yes/no menus with no'
91
+ switch [:no], negatable: false
92
+
85
93
  desc 'Exclude auto tags and default tags'
86
94
  switch %i[x noauto], default_value: false, negatable: false
87
95
 
@@ -121,9 +129,9 @@ command %i[now next] do |c|
121
129
  c.desc "Edit entry with #{Doing::Util.default_editor}"
122
130
  c.switch %i[e editor], negatable: false, default_value: false
123
131
 
124
- c.desc 'Backdate start time [4pm|20m|2h|yesterday noon]'
132
+ c.desc 'Backdate start time [4pm|20m|2h|"yesterday noon"]'
125
133
  c.arg_name 'DATE_STRING'
126
- c.flag %i[b back]
134
+ c.flag %i[b back started]
127
135
 
128
136
  c.desc 'Timed entry, marks last entry in section as @done'
129
137
  c.switch %i[f finish_last], negatable: false, default_value: false
@@ -138,9 +146,9 @@ command %i[now next] do |c|
138
146
 
139
147
  c.action do |_global_options, options, args|
140
148
  if options[:back]
141
- date = wwid.chronify(options[:back], guess: :begin)
149
+ date = options[:back].chronify(guess: :begin)
142
150
 
143
- raise InvalidTimeExpression.new('unable to parse date string', topic: 'Date parser:') if date.nil?
151
+ raise InvalidTimeExpression.new('unable to parse date string', topic: 'Parser:') if date.nil?
144
152
  else
145
153
  date = Time.now
146
154
  end
@@ -151,29 +159,34 @@ command %i[now next] do |c|
151
159
  options[:section] = settings['current_section']
152
160
  end
153
161
 
154
- if options[:e] || (args.empty? && $stdin.stat.size.zero?)
162
+ if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
155
163
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
156
164
 
157
- input = ''
165
+ input = date.strftime('%F %R | ')
158
166
  input += args.join(' ') unless args.empty?
159
167
  input = wwid.fork_editor(input).strip
160
168
 
161
169
  raise EmptyInput, 'No content' if input.empty?
162
170
 
163
- title, note = wwid.format_input(input)
164
- note.push(options[:n]) if options[:n]
165
- wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
171
+ date, title, note = wwid.format_input(input)
172
+ note.add(options[:note]) if options[:note]
173
+ wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
166
174
  wwid.write(wwid.doing_file)
167
175
  elsif args.length.positive?
168
- title, note = wwid.format_input(args.join(' '))
169
- note.push(options[:n]) if options[:n]
170
- wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
176
+ d, title, note = wwid.format_input(args.join(' '))
177
+ date = d.nil? ? date : d
178
+ note.add(options[:note]) if options[:note]
179
+ wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
171
180
  wwid.write(wwid.doing_file)
172
181
  elsif $stdin.stat.size.positive?
173
- input = $stdin.read
174
- title, note = wwid.format_input(input)
175
- note.push(options[:n]) if options[:n]
176
- wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
182
+ input = $stdin.read.strip
183
+ d, title, note = wwid.format_input(input)
184
+ unless d.nil?
185
+ Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
186
+ date = d
187
+ end
188
+ note.add(options[:note]) if options[:note]
189
+ wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
177
190
  wwid.write(wwid.doing_file)
178
191
  else
179
192
  raise EmptyInput, 'You must provide content when creating a new entry'
@@ -238,14 +251,13 @@ command %i[reset begin] do |c|
238
251
  items = wwid.filter_items([], opt: options)
239
252
 
240
253
  if options[:interactive]
241
- last_entry = wwid.choose_from_items(items, {
254
+ last_entry = Doing::Prompt.choose_from_items(items, include_section: options[:section].nil?,
242
255
  menu: true,
243
256
  header: '',
244
257
  prompt: 'Select an entry to start/reset > ',
245
258
  multiple: false,
246
259
  sort: false,
247
- show_if_single: true
248
- }, include_section: options[:section].nil? )
260
+ show_if_single: true)
249
261
  else
250
262
  last_entry = items.last
251
263
  end
@@ -344,7 +356,7 @@ command :note do |c|
344
356
  last_note = last_entry.note || Doing::Note.new
345
357
  new_note = Doing::Note.new
346
358
 
347
- if options[:e] || (args.empty? && $stdin.stat.size.zero? && !options[:r])
359
+ if options[:editor] || (args.empty? && $stdin.stat.size.zero? && !options[:remove])
348
360
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
349
361
 
350
362
  input = !args.empty? ? args.join(' ') : ''
@@ -357,14 +369,14 @@ command :note do |c|
357
369
 
358
370
  input = prev_input.add(input)
359
371
 
360
- input = wwid.fork_editor([last_entry.title, '### Edit below this line', input.to_s].join("\n")).strip
361
- _title, note = wwid.format_input(input)
372
+ input = wwid.fork_editor(prev_input.strip_lines.join("\n"), message: nil).strip
373
+ note = input
362
374
  options[:remove] = true
363
375
  new_note.add(note)
364
376
  elsif !args.empty?
365
377
  new_note.add(args.join(' '))
366
378
  elsif $stdin.stat.size.positive?
367
- new_note.add($stdin.read)
379
+ new_note.add($stdin.read.strip)
368
380
  else
369
381
  raise EmptyInput, 'You must provide content when adding a note' unless options[:remove]
370
382
  end
@@ -409,7 +421,7 @@ command :meanwhile do |c|
409
421
 
410
422
  c.action do |_global_options, options, args|
411
423
  if options[:back]
412
- date = wwid.chronify(options[:back], guess: :begin)
424
+ date = options[:back].chronify(guess: :begin)
413
425
 
414
426
  raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
415
427
  else
@@ -423,31 +435,35 @@ command :meanwhile do |c|
423
435
  end
424
436
  input = ''
425
437
 
426
- if options[:e]
438
+ if options[:editor]
427
439
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
428
-
440
+ input += date.strftime('%F %R | ')
429
441
  input += args.join(' ') unless args.empty?
430
442
  input = wwid.fork_editor(input).strip
431
443
  elsif !args.empty?
432
444
  input = args.join(' ')
433
445
  elsif $stdin.stat.size.positive?
434
- input = $stdin.read
446
+ input = $stdin.read.strip
435
447
  end
436
448
 
437
449
  if input && !input.empty?
438
- input, note = wwid.format_input(input)
450
+ d, input, note = wwid.format_input(input)
451
+ unless d.nil?
452
+ Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
453
+ date = d
454
+ end
439
455
  else
440
456
  input = nil
441
457
  note = []
442
458
  end
443
459
 
444
- if options[:n]
445
- note.push(options[:n])
460
+ if options[:note]
461
+ note.push(options[:note])
446
462
  elsif note.empty?
447
463
  note = nil
448
464
  end
449
465
 
450
- wwid.stop_start('meanwhile', { new_item: input, back: date, section: section, archive: options[:a], note: note })
466
+ wwid.stop_start('meanwhile', { new_item: input, back: date, section: section, archive: options[:archive], note: note })
451
467
  wwid.write(wwid.doing_file)
452
468
  end
453
469
  end
@@ -465,11 +481,11 @@ command :template do |c|
465
481
  c.switch %i[l list], negatable: false
466
482
 
467
483
  c.desc 'List in single column for completion'
468
- c.switch %i[c]
484
+ c.switch %i[c column]
469
485
 
470
486
  c.action do |_global_options, options, args|
471
- if options[:list] || options[:c]
472
- if options[:c]
487
+ if options[:list] || options[:column]
488
+ if options[:column]
473
489
  $stdout.print Doing::Plugins.plugin_templates.join("\n")
474
490
  else
475
491
  $stdout.puts "Available templates: #{Doing::Plugins.plugin_templates.join(', ')}"
@@ -478,7 +494,7 @@ command :template do |c|
478
494
  end
479
495
 
480
496
  if args.empty?
481
- type = wwid.choose_from(Doing::Plugins.plugin_templates, sorted: false, prompt: 'Select template type > ')
497
+ type = Doing::Prompt.choose_from(Doing::Plugins.plugin_templates, sorted: false, prompt: 'Select template type > ')
482
498
  else
483
499
  type = args[0]
484
500
  end
@@ -532,6 +548,25 @@ command :select do |c|
532
548
  c.arg_name 'QUERY'
533
549
  c.flag %i[q query search]
534
550
 
551
+ 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.'
552
+ c.arg_name 'DATE_STRING'
553
+ c.flag [:before]
554
+
555
+ 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.'
556
+ c.arg_name 'DATE_STRING'
557
+ c.flag [:after]
558
+
559
+ c.desc %(
560
+ Date range to show, or a single day to filter date on.
561
+ Date range argument should be quoted. Date specifications can be natural language.
562
+ To specify a range, use "to" or "through": `doing select --from "monday 8am to friday 5pm"`.
563
+
564
+ If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
565
+ by time of day.
566
+ )
567
+ c.arg_name 'DATE_OR_RANGE'
568
+ c.flag [:from]
569
+
535
570
  c.desc 'Force exact search string matching (case sensitive)'
536
571
  c.switch %i[x exact], default_value: false, negatable: false
537
572
 
@@ -604,7 +639,7 @@ command :later do |c|
604
639
 
605
640
  c.action do |_global_options, options, args|
606
641
  if options[:back]
607
- date = wwid.chronify(options[:back], guess: :begin)
642
+ date = options[:back].chronify(guess: :begin)
608
643
  raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
609
644
  else
610
645
  date = Time.now
@@ -613,22 +648,29 @@ command :later do |c|
613
648
  if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
614
649
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
615
650
 
616
- input = args.empty? ? '' : args.join(' ')
651
+ input += date.strftime('%F %R | ')
652
+ input += args.empty? ? '' : args.join(' ')
617
653
  input = wwid.fork_editor(input).strip
618
654
  raise EmptyInput, 'No content' unless input && !input.empty?
619
655
 
620
- title, note = wwid.format_input(input)
621
- note.push(options[:n]) if options[:n]
656
+ d, title, note = wwid.format_input(input)
657
+ date = d.nil? ? date : d
658
+ note.add(options[:note]) if options[:note]
622
659
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
623
660
  wwid.write(wwid.doing_file)
624
661
  elsif !args.empty?
625
- title, note = wwid.format_input(args.join(' '))
626
- note.push(options[:n]) if options[:n]
662
+ d, title, note = wwid.format_input(args.join(' '))
663
+ date = d.nil? ? date : d
664
+ note.add(options[:note]) if options[:note]
627
665
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
628
666
  wwid.write(wwid.doing_file)
629
667
  elsif $stdin.stat.size.positive?
630
- title, note = wwid.format_input($stdin.read)
631
- note.push(options[:n]) if options[:n]
668
+ d, title, note = wwid.format_input($stdin.read)
669
+ unless d.nil?
670
+ Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
671
+ date = d
672
+ end
673
+ note.add(options[:note]) if options[:note]
632
674
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
633
675
  wwid.write(wwid.doing_file)
634
676
  else
@@ -659,9 +701,9 @@ command %i[done did] do |c|
659
701
  c.arg_name 'DATE_STRING'
660
702
  c.flag [:at]
661
703
 
662
- c.desc 'Backdate start date by interval [4pm|20m|2h|yesterday noon]'
704
+ c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
663
705
  c.arg_name 'DATE_STRING'
664
- c.flag %i[b back]
706
+ c.flag %i[b back started]
665
707
 
666
708
  c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM).
667
709
  If used without the --back option, the start date will be moved back to allow
@@ -692,26 +734,24 @@ command %i[done did] do |c|
692
734
  donedate = nil
693
735
 
694
736
  if options[:took]
695
- took = wwid.chronify_qty(options[:took])
737
+ took = options[:took].chronify_qty
696
738
  raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
697
739
  end
698
740
 
699
741
  if options[:back]
700
- date = wwid.chronify(options[:back], guess: :begin)
742
+ date = options[:back].chronify(guess: :begin)
701
743
  raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
702
744
  else
703
745
  date = options[:took] ? Time.now - took : Time.now
704
746
  end
705
747
 
706
748
  if options[:at]
707
- finish_date = wwid.chronify(options[:at], guess: :begin)
749
+ finish_date = options[:at].chronify(guess: :begin)
708
750
  raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
709
751
 
710
752
  date = options[:took] ? finish_date - took : finish_date
711
753
  elsif options[:took]
712
754
  finish_date = date + took
713
- elsif options[:back]
714
- finish_date = date
715
755
  else
716
756
  finish_date = Time.now
717
757
  end
@@ -743,16 +783,17 @@ command %i[done did] do |c|
743
783
 
744
784
  old_entry = last_entry.dup
745
785
  last_entry.note.add(note)
746
- input = [last_entry.title, last_entry.note.to_s].join("\n")
786
+ input = ["#{last_entry.date.strftime('%F %R | ')}#{last_entry.title}", last_entry.note.strip_lines.join("\n")].join("\n")
747
787
  else
748
788
  is_new = true
749
- input = [args.join(' '), note.to_s].join("\n")
789
+ input = ["#{date.strftime('%F %R | ')}#{args.join(' ')}", note.strip_lines.join("\n")].join("\n")
750
790
  end
751
791
 
752
792
  input = wwid.fork_editor(input).strip
753
793
  raise EmptyInput, 'No content' unless input && !input.empty?
754
794
 
755
- title, note = wwid.format_input(input)
795
+ d, title, note = wwid.format_input(input)
796
+ date = d.nil? ? date : d
756
797
  new_entry = Doing::Item.new(date, title, section, note)
757
798
  if new_entry.should_finish?
758
799
  if new_entry.should_time?
@@ -763,23 +804,23 @@ command %i[done did] do |c|
763
804
  end
764
805
 
765
806
  if (is_new)
766
- wwid.content[section][:items].push(new_entry)
807
+ wwid.content.push(new_entry)
767
808
  else
768
- wwid.update_item(old_entry, new_entry)
809
+ wwid.content.update_item(old_entry, new_entry)
769
810
  end
770
811
 
771
- if options[:a]
812
+ if options[:archive]
772
813
  wwid.move_item(new_entry, 'Archive', label: true)
773
814
  end
774
815
 
775
816
  wwid.write(wwid.doing_file)
776
817
  elsif args.empty? && $stdin.stat.size.zero?
777
- if options[:r]
818
+ if options[:remove]
778
819
  wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
779
820
  else
780
821
  note = options[:note] ? Doing::Note.new(options[:note]) : nil
781
822
  opt = {
782
- archive: options[:a],
823
+ archive: options[:archive],
783
824
  back: finish_date,
784
825
  count: 1,
785
826
  date: options[:date],
@@ -793,9 +834,10 @@ command %i[done did] do |c|
793
834
  end
794
835
  elsif !args.empty?
795
836
  note = Doing::Note.new(options[:note])
796
- title, new_note = wwid.format_input([args.join(' '), note.to_s].join("\n"))
837
+ d, title, new_note = wwid.format_input([args.join(' '), note.strip_lines.join("\n")].join("\n"))
838
+ date = d.nil? ? date : d
797
839
  title.chomp!
798
- section = 'Archive' if options[:a]
840
+ section = 'Archive' if options[:archive]
799
841
  new_entry = Doing::Item.new(date, title, section, new_note)
800
842
  if new_entry.should_finish?
801
843
  if new_entry.should_time?
@@ -804,13 +846,17 @@ command %i[done did] do |c|
804
846
  new_entry.tag('done')
805
847
  end
806
848
  end
807
- wwid.content[section][:items].push(new_entry)
849
+ wwid.content.push(new_entry)
808
850
  wwid.write(wwid.doing_file)
809
851
  Doing.logger.info('Entry Added:', new_entry.title)
810
852
  elsif $stdin.stat.size.positive?
811
- title, note = wwid.format_input($stdin.read)
853
+ d, title, note = wwid.format_input($stdin.read.strip)
854
+ unless d.nil?
855
+ Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
856
+ date = d
857
+ end
812
858
  note.add(options[:note]) if options[:note]
813
- section = options[:a] ? 'Archive' : section
859
+ section = options[:archive] ? 'Archive' : section
814
860
  new_entry = Doing::Item.new(date, title, section, note)
815
861
 
816
862
  if new_entry.should_finish?
@@ -821,7 +867,7 @@ command %i[done did] do |c|
821
867
  end
822
868
  end
823
869
 
824
- wwid.content[section][:items].push(new_entry)
870
+ wwid.content.push(new_entry)
825
871
  wwid.write(wwid.doing_file)
826
872
  Doing.logger.info('Entry Added:', new_entry.title)
827
873
  else
@@ -907,7 +953,7 @@ command :cancel do |c|
907
953
  end
908
954
 
909
955
  opts = {
910
- archive: options[:a],
956
+ archive: options[:archive],
911
957
  case: options[:case].normalize_case,
912
958
  count: count,
913
959
  date: false,
@@ -1001,7 +1047,7 @@ command :finish do |c|
1001
1047
  options[:fuzzy] = false
1002
1048
  unless options[:auto]
1003
1049
  if options[:took]
1004
- took = wwid.chronify_qty(options[:took])
1050
+ took = options[:took].chronify_qty
1005
1051
  raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
1006
1052
  end
1007
1053
 
@@ -1010,21 +1056,21 @@ command :finish do |c|
1010
1056
  raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1011
1057
 
1012
1058
  if options[:at]
1013
- finish_date = wwid.chronify(options[:at], guess: :begin)
1059
+ finish_date = options[:at].chronify(guess: :begin)
1014
1060
  raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
1015
1061
 
1016
1062
  date = options[:took] ? finish_date - took : finish_date
1017
1063
  elsif options[:back]
1018
- date = wwid.chronify(options[:back])
1064
+ date = options[:back].chronify()
1019
1065
 
1020
1066
  raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
1021
- elsif options[:took]
1022
- date = wwid.chronify_qty(options[:took])
1023
1067
  else
1024
1068
  date = Time.now
1025
1069
  end
1026
1070
  end
1027
1071
 
1072
+ options[:took] = options[:took].chronify_qty if options[:took]
1073
+
1028
1074
  if options[:tag].nil?
1029
1075
  tags = []
1030
1076
  else
@@ -1064,6 +1110,7 @@ command :finish do |c|
1064
1110
  tag: tags,
1065
1111
  tag_bool: options[:bool].normalize_bool,
1066
1112
  tags: ['done'],
1113
+ took: options[:took],
1067
1114
  unfinished: options[:unfinished]
1068
1115
  }
1069
1116
 
@@ -1188,7 +1235,7 @@ command :tag do |c|
1188
1235
  c.desc 'Tag last entry (or entries) not marked @done'
1189
1236
  c.switch %i[u unfinished], negatable: false, default_value: false
1190
1237
 
1191
- c.desc 'Autotag entries based on autotag configuration in ~/.doingrc'
1238
+ c.desc 'Autotag entries based on autotag configuration in ~/.config/doing/config.yml'
1192
1239
  c.switch %i[a autotag], negatable: false, default_value: false
1193
1240
 
1194
1241
  c.desc 'Tag the last X entries containing TAG.
@@ -1279,15 +1326,15 @@ command :tag do |c|
1279
1326
  end
1280
1327
 
1281
1328
 
1282
- question = if options[:a]
1329
+ question = if options[:aarchive]
1283
1330
  "Are you sure you want to autotag all records#{section_q}"
1284
- elsif options[:r]
1331
+ elsif options[:remove]
1285
1332
  "Are you sure you want to remove #{tags.join(' and ')} from all records#{section_q}"
1286
1333
  else
1287
1334
  "Are you sure you want to add #{tags.join(' and ')} to all records#{section_q}"
1288
1335
  end
1289
1336
 
1290
- res = wwid.yn(question, default_response: false)
1337
+ res = Doing::Prompt.yn(question, default_response: false)
1291
1338
 
1292
1339
  raise UserCancelled unless res
1293
1340
  end
@@ -1408,7 +1455,7 @@ command [:mark, :flag] do |c|
1408
1455
  "Are you sure you want to flag all records#{section_q}"
1409
1456
  end
1410
1457
 
1411
- res = wwid.yn(question, default_response: false)
1458
+ res = Doing::Prompt.yn(question, default_response: false)
1412
1459
 
1413
1460
  exit_now! 'Cancelled' unless res
1414
1461
  end
@@ -1454,14 +1501,26 @@ command :show do |c|
1454
1501
  c.arg_name 'AGE'
1455
1502
  c.flag %i[a age], default_value: 'newest'
1456
1503
 
1457
- c.desc 'View entries older than date'
1504
+ 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.'
1458
1505
  c.arg_name 'DATE_STRING'
1459
1506
  c.flag [:before]
1460
1507
 
1461
- c.desc 'View entries newer than date'
1508
+ 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.'
1462
1509
  c.arg_name 'DATE_STRING'
1463
1510
  c.flag [:after]
1464
1511
 
1512
+ c.desc %(
1513
+ Date range to show, or a single day to filter date on.
1514
+ Date range argument should be quoted. Date specifications can be natural language.
1515
+ To specify a range, use "to" or "through": `doing show --from "monday 8am to friday 5pm"`.
1516
+
1517
+ If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
1518
+ by time of day.
1519
+ )
1520
+
1521
+ c.arg_name 'DATE_OR_RANGE'
1522
+ c.flag [:from]
1523
+
1465
1524
  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
1466
1525
  c.arg_name 'QUERY'
1467
1526
  c.flag [:search]
@@ -1483,14 +1542,6 @@ command :show do |c|
1483
1542
  c.arg_name 'ORDER'
1484
1543
  c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
1485
1544
 
1486
- c.desc %(
1487
- Date range to show, or a single day to filter date on.
1488
- Date range argument should be quoted. Date specifications can be natural language.
1489
- To specify a range, use "to" or "through": `doing show --from "monday to friday"`
1490
- )
1491
- c.arg_name 'DATE_OR_RANGE'
1492
- c.flag %i[f from]
1493
-
1494
1545
  c.desc 'Show time intervals on @done tasks'
1495
1546
  c.switch %i[t times], default_value: true, negatable: true
1496
1547
 
@@ -1565,21 +1616,6 @@ command :show do |c|
1565
1616
  }
1566
1617
  end
1567
1618
 
1568
- if options[:from]
1569
-
1570
- date_string = options[:from]
1571
- if date_string =~ / (to|through|thru|(un)?til|-+) /
1572
- dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
1573
- start = wwid.chronify(dates[0], guess: :begin)
1574
- finish = wwid.chronify(dates[2], guess: :end)
1575
- else
1576
- start = wwid.chronify(date_string, guess: :begin)
1577
- finish = false
1578
- end
1579
- raise InvalidTimeExpression, 'Unrecognized date string' unless start
1580
- dates = [start, finish]
1581
- end
1582
-
1583
1619
  options[:times] = true if options[:totals]
1584
1620
 
1585
1621
  template = settings['templates']['default'].deep_merge({
@@ -1598,10 +1634,8 @@ command :show do |c|
1598
1634
  end
1599
1635
 
1600
1636
  opt = options.dup
1601
-
1602
1637
  opt[:sort_tags] = options[:tag_sort] =~ /^n/i
1603
1638
  opt[:count] = options[:count].to_i
1604
- opt[:date_filter] = dates
1605
1639
  opt[:highlight] = true
1606
1640
  opt[:order] = options[:sort].normalize_order
1607
1641
  opt[:section] = section
@@ -1632,14 +1666,25 @@ command %i[grep search] do |c|
1632
1666
  c.arg_name 'NAME'
1633
1667
  c.flag %i[s section], default_value: 'All'
1634
1668
 
1635
- c.desc 'Constrain search to entries older than date'
1669
+ 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.'
1636
1670
  c.arg_name 'DATE_STRING'
1637
1671
  c.flag [:before]
1638
1672
 
1639
- c.desc 'Constrain search to entries newer than date'
1673
+ 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.'
1640
1674
  c.arg_name 'DATE_STRING'
1641
1675
  c.flag [:after]
1642
1676
 
1677
+ c.desc %(
1678
+ Date range to show, or a single day to filter date on.
1679
+ Date range argument should be quoted. Date specifications can be natural language.
1680
+ To specify a range, use "to" or "through": `doing search --from "monday 8am to friday 5pm"`.
1681
+
1682
+ If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
1683
+ by time of day.
1684
+ )
1685
+ c.arg_name 'DATE_OR_RANGE'
1686
+ c.flag [:from]
1687
+
1643
1688
  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1644
1689
  c.arg_name 'FORMAT'
1645
1690
  c.flag %i[o output]
@@ -1729,7 +1774,7 @@ command :recent do |c|
1729
1774
  c.switch %i[i interactive], negatable: false, default_value: false
1730
1775
 
1731
1776
  c.action do |global_options, options, args|
1732
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
1777
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
1733
1778
 
1734
1779
  unless global_options[:version]
1735
1780
  if settings['templates']['recent'].key?('count')
@@ -1744,7 +1789,7 @@ command :recent do |c|
1744
1789
  count = args.empty? ? config_count : args[0].to_i
1745
1790
  end
1746
1791
 
1747
- options[:t] = true if options[:totals]
1792
+ options[:times] = true if options[:totals]
1748
1793
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1749
1794
 
1750
1795
  template = settings['templates']['recent'].deep_merge(settings['templates']['default'])
@@ -1753,7 +1798,7 @@ command :recent do |c|
1753
1798
  opts = {
1754
1799
  sort_tags: options[:sort_tags],
1755
1800
  tags_color: tags_color,
1756
- times: options[:t],
1801
+ times: options[:times],
1757
1802
  totals: options[:totals],
1758
1803
  interactive: options[:interactive]
1759
1804
  }
@@ -1799,19 +1844,20 @@ command :today do |c|
1799
1844
  c.arg_name 'TIME_STRING'
1800
1845
  c.flag [:after]
1801
1846
 
1847
+ c.desc %(
1848
+ Time range to show `doing today --from "12pm to 4pm"`
1849
+ )
1850
+ c.arg_name 'DATE_OR_RANGE'
1851
+ c.flag [:from]
1852
+
1802
1853
  c.action do |_global_options, options, _args|
1803
1854
  raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1804
1855
 
1805
- options[:t] = true if options[:totals]
1856
+ options[:times] = true if options[:totals]
1806
1857
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1807
- opt = {
1808
- after: options[:after],
1809
- before: options[:before],
1810
- section: options[:section],
1811
- sort_tags: options[:sort_tags],
1812
- totals: options[:totals]
1813
- }
1814
- Doing::Pager.page wwid.today(options[:times], options[:output], opt).chomp
1858
+ filter_options = %i[after before from section sort_tags totals].each_with_object({}) { |k, hsh| hsh[k] = options[k] }
1859
+
1860
+ Doing::Pager.page wwid.today(options[:times], options[:output], filter_options).chomp
1815
1861
  end
1816
1862
  end
1817
1863
 
@@ -1854,23 +1900,23 @@ command :on do |c|
1854
1900
 
1855
1901
  if date_string =~ / (to|through|thru) /
1856
1902
  dates = date_string.split(/ (to|through|thru) /)
1857
- start = wwid.chronify(dates[0], guess: :begin)
1858
- finish = wwid.chronify(dates[2], guess: :end)
1903
+ start = dates[0].chronify(guess: :begin)
1904
+ finish = dates[2].chronify(guess: :end)
1859
1905
  else
1860
- start = wwid.chronify(date_string, guess: :begin)
1906
+ start = date_string.chronify(guess: :begin)
1861
1907
  finish = false
1862
1908
  end
1863
1909
 
1864
1910
  raise InvalidTimeExpression, 'Unrecognized date string' unless start
1865
1911
 
1866
- message = "Date interpreted as #{start}"
1912
+ message = "date interpreted as #{start}"
1867
1913
  message += " to #{finish}" if finish
1868
- Doing.logger.debug(message)
1914
+ Doing.logger.debug('Interpreter:', message)
1869
1915
 
1870
- options[:t] = true if options[:totals]
1916
+ options[:times] = true if options[:totals]
1871
1917
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1872
1918
 
1873
- Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1919
+ Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
1874
1920
  { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1875
1921
  end
1876
1922
  end
@@ -1913,17 +1959,17 @@ command :since do |c|
1913
1959
  date_string.sub!(/(day) (\d)/, '\1 at \2')
1914
1960
  date_string.sub!(/(\d+)d( ago)?/, '\1 days ago')
1915
1961
 
1916
- start = wwid.chronify(date_string, guess: :begin)
1962
+ start = date_string.chronify(guess: :begin)
1917
1963
  finish = Time.now
1918
1964
 
1919
1965
  raise InvalidTimeExpression, 'Unrecognized date string' unless start
1920
1966
 
1921
- Doing.logger.debug("Date interpreted as #{start} through the current time")
1967
+ Doing.logger.debug('Interpreter:', "date interpreted as #{start} through the current time")
1922
1968
 
1923
- options[:t] = true if options[:totals]
1969
+ options[:times] = true if options[:totals]
1924
1970
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1925
1971
 
1926
- Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1972
+ Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
1927
1973
  { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1928
1974
  end
1929
1975
  end
@@ -1961,6 +2007,12 @@ command :yesterday do |c|
1961
2007
  c.arg_name 'TIME_STRING'
1962
2008
  c.flag [:after]
1963
2009
 
2010
+ c.desc %(
2011
+ Time range to show, e.g. `doing yesterday --from "1am to 8am"`
2012
+ )
2013
+ c.arg_name 'TIME_RANGE'
2014
+ c.flag [:from]
2015
+
1964
2016
  c.desc 'Tag sort direction (asc|desc)'
1965
2017
  c.arg_name 'DIRECTION'
1966
2018
  c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
@@ -1970,9 +2022,16 @@ command :yesterday do |c|
1970
2022
 
1971
2023
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1972
2024
 
2025
+ if options[:from]
2026
+ options[:from] = options[:from].split(/ (?:to|through|thru|(?:un)?til|-+) /).map do |time|
2027
+ "yesterday #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}"
2028
+ end.join(' to ')
2029
+ end
2030
+
1973
2031
  opt = {
1974
2032
  after: options[:after],
1975
2033
  before: options[:before],
2034
+ from: options[:from],
1976
2035
  sort_tags: options[:sort_tags],
1977
2036
  tag_order: options[:tag_order].normalize_order,
1978
2037
  totals: options[:totals],
@@ -2053,9 +2112,9 @@ command :last do |c|
2053
2112
  end
2054
2113
 
2055
2114
  if options[:editor]
2056
- wwid.edit_last(section: options[:s], options: { search: search, fuzzy: options[:fuzzy], case: options[:case], tag: tags, tag_bool: options[:bool], not: options[:not] })
2115
+ wwid.edit_last(section: options[:section], options: { search: search, fuzzy: options[:fuzzy], case: options[:case], tag: tags, tag_bool: options[:bool], not: options[:not] })
2057
2116
  else
2058
- Doing::Pager::page wwid.last(times: true, section: options[:s],
2117
+ Doing::Pager::page wwid.last(times: true, section: options[:section],
2059
2118
  options: { search: search, fuzzy: options[:fuzzy], case: options[:case], negate: options[:not], tag: tags, tag_bool: options[:bool] }).strip
2060
2119
  end
2061
2120
  end
@@ -2067,8 +2126,8 @@ command :sections do |c|
2067
2126
  c.switch %i[c column], negatable: false, default_value: false
2068
2127
 
2069
2128
  c.action do |_global_options, options, _args|
2070
- joiner = options[:c] ? "\n" : "\t"
2071
- print wwid.sections.join(joiner)
2129
+ joiner = options[:column] ? "\n" : "\t"
2130
+ print wwid.content.section_titles.join(joiner)
2072
2131
  end
2073
2132
  end
2074
2133
 
@@ -2089,7 +2148,7 @@ command :add_section do |c|
2089
2148
  c.action do |_global_options, _options, args|
2090
2149
  raise InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
2091
2150
 
2092
- wwid.add_section(args.join(' ').cap_first)
2151
+ wwid.content.add_section(args.join(' ').cap_first, log: true)
2093
2152
  wwid.write(wwid.doing_file)
2094
2153
  end
2095
2154
  end
@@ -2219,14 +2278,25 @@ command :view do |c|
2219
2278
  c.arg_name 'DIRECTION'
2220
2279
  c.flag [:tag_order], must_match: REGEX_SORT_ORDER
2221
2280
 
2222
- c.desc 'View entries older than date'
2281
+ 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.'
2223
2282
  c.arg_name 'DATE_STRING'
2224
2283
  c.flag [:before]
2225
2284
 
2226
- c.desc 'View entries newer than date'
2285
+ 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.'
2227
2286
  c.arg_name 'DATE_STRING'
2228
2287
  c.flag [:after]
2229
2288
 
2289
+ c.desc %(
2290
+ Date range to show, or a single day to filter date on.
2291
+ Date range argument should be quoted. Date specifications can be natural language.
2292
+ To specify a range, use "to" or "through": `doing view --from "monday 8am to friday 5pm" view_name`.
2293
+
2294
+ If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
2295
+ by time of day.
2296
+ )
2297
+ c.arg_name 'DATE_OR_RANGE'
2298
+ c.flag [:from]
2299
+
2230
2300
  c.desc 'Only show items with recorded time intervals (override view settings)'
2231
2301
  c.switch [:only_timed], default_value: false, negatable: false
2232
2302
 
@@ -2247,6 +2317,7 @@ command :view do |c|
2247
2317
  rescue WrongCommand => exception
2248
2318
  cmd = commands[:show]
2249
2319
  options[:sort] = 'asc'
2320
+ options[:tag_order] = 'asc'
2250
2321
  action = cmd.send(:get_action, nil)
2251
2322
  return action.call(global_options, options, args)
2252
2323
  end
@@ -2289,12 +2360,9 @@ command :view do |c|
2289
2360
  # If the -o/--output flag was specified, override any default in the view template
2290
2361
  options[:output] ||= view.key?('output_format') ? view['output_format'] : 'template'
2291
2362
 
2292
- count = if options[:c]
2293
- options[:c]
2294
- else
2295
- view.key?('count') ? view['count'] : 10
2296
- end
2297
- section = if options[:s]
2363
+ count = options[:count] ? options[:count] : view.key?('count') ? view['count'] : 10
2364
+
2365
+ section = if options[:section]
2298
2366
  section
2299
2367
  else
2300
2368
  view.key?('section') ? view['section'] : settings['current_section']
@@ -2312,7 +2380,7 @@ command :view do |c|
2312
2380
  view.key?('tag_order') ? view['tag_order'].normalize_order : 'asc'
2313
2381
  end
2314
2382
 
2315
- options[:t] = true if totals
2383
+ options[:times] = true if totals
2316
2384
  output_format = options[:output]&.downcase || 'template'
2317
2385
 
2318
2386
  options[:sort_tags] = if options[:tag_sort]
@@ -2322,27 +2390,8 @@ command :view do |c|
2322
2390
  else
2323
2391
  false
2324
2392
  end
2325
- if view.key?('after') && !options[:after]
2326
- options[:after] = view['after']
2327
- end
2328
2393
 
2329
- if view.key?('before') && !options[:before]
2330
- options[:before] = view['before']
2331
- end
2332
-
2333
- if view.key?('from')
2334
- date_string = view['from']
2335
- if date_string =~ / (to|through|thru|(un)?til|-+) /
2336
- dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
2337
- start = wwid.chronify(dates[0], guess: :begin)
2338
- finish = wwid.chronify(dates[2], guess: :end)
2339
- else
2340
- start = wwid.chronify(date_string, guess: :begin)
2341
- finish = false
2342
- end
2343
- raise InvalidTimeExpression, 'Unrecognized date string' unless start
2344
- dates = [start, finish]
2345
- end
2394
+ %w[before after from].each { |k| options[k.to_sym] = view[k] if view.key?(k) && !options[k.to_sym] }
2346
2395
 
2347
2396
  options[:case] = options[:case].normalize_case
2348
2397
 
@@ -2354,22 +2403,21 @@ command :view do |c|
2354
2403
  end
2355
2404
 
2356
2405
  opts = options.dup
2357
- opts[:search] = search
2358
- opts[:output] = output_format
2359
2406
  opts[:count] = count
2360
2407
  opts[:format] = date_format
2361
2408
  opts[:highlight] = options[:color]
2362
2409
  opts[:only_timed] = only_timed
2363
2410
  opts[:order] = order
2411
+ opts[:output] = options[:interactive] ? nil : options[:output]
2412
+ opts[:output] = output_format
2413
+ opts[:page_title] = page_title
2414
+ opts[:search] = search
2364
2415
  opts[:section] = section
2365
2416
  opts[:tag_filter] = tag_filter
2366
2417
  opts[:tag_order] = tag_order
2367
2418
  opts[:tags_color] = tags_color
2368
2419
  opts[:template] = template
2369
2420
  opts[:totals] = totals
2370
- opts[:page_title] = page_title
2371
- opts[:date_filter] = dates
2372
- opts[:output] = options[:interactive] ? nil : options[:output]
2373
2421
 
2374
2422
  Doing::Pager.page wwid.list_section(opts)
2375
2423
  elsif title.instance_of?(FalseClass)
@@ -2386,7 +2434,7 @@ command :views do |c|
2386
2434
  c.switch %i[c column], default_value: false
2387
2435
 
2388
2436
  c.action do |_global_options, options, _args|
2389
- joiner = options[:c] ? "\n" : "\t"
2437
+ joiner = options[:column] ? "\n" : "\t"
2390
2438
  print wwid.views.join(joiner)
2391
2439
  end
2392
2440
  end
@@ -2552,6 +2600,10 @@ end
2552
2600
  desc 'Open the "doing" file in an editor'
2553
2601
  long_desc "`doing open` defaults to using the editor_app setting in #{config.config_file} (#{settings.key?('editor_app') ? settings['editor_app'] : 'not set'})."
2554
2602
  command :open do |c|
2603
+ c.desc 'Open with editor command (e.g. vim, mate)'
2604
+ c.arg_name 'COMMAND'
2605
+ c.flag %i[e editor]
2606
+
2555
2607
  if `uname` =~ /Darwin/
2556
2608
  c.desc 'Open with app name'
2557
2609
  c.arg_name 'APP_NAME'
@@ -2567,14 +2619,20 @@ command :open do |c|
2567
2619
  params.delete_if do |k, v|
2568
2620
  k.instance_of?(String) || v.nil? || v == false
2569
2621
  end
2570
- if `uname` =~ /Darwin/
2622
+
2623
+ if options[:editor]
2624
+ raise MissingEditor, "Editor #{options[:editor]} not found" unless Doing::Util.exec_available(options[:editor].split(/ /).first)
2625
+
2626
+ editor = TTY::Which.which(options[:editor])
2627
+ system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
2628
+ elsif `uname` =~ /Darwin/
2571
2629
  if options[:app]
2572
- system %(open -a "#{options[:a]}" "#{File.expand_path(wwid.doing_file)}")
2630
+ system %(open -a "#{options[:app]}" "#{File.expand_path(wwid.doing_file)}")
2573
2631
  elsif options[:bundle_id]
2574
- system %(open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}")
2632
+ system %(open -b "#{options[:bundle_id]}" "#{File.expand_path(wwid.doing_file)}")
2575
2633
  elsif Doing::Util.find_default_editor('doing_file')
2576
2634
  editor = Doing::Util.find_default_editor('doing_file')
2577
- if Doing::Util.exec_available(editor)
2635
+ if Doing::Util.exec_available(editor.split(/ /).first)
2578
2636
  system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
2579
2637
  else
2580
2638
  system %(open -a "#{editor}" "#{File.expand_path(wwid.doing_file)}")
@@ -2591,132 +2649,240 @@ command :open do |c|
2591
2649
  end
2592
2650
 
2593
2651
  desc 'Edit the configuration file or output a value from it'
2594
- long_desc %(Run without arguments, `doing config` opens your `.doingrc` in an editor.
2652
+ long_desc %(Run without arguments, `doing config` opens your `config.yml` in an editor.
2595
2653
  If local configurations are found in the path between the current directory
2596
- and `~/.doingrc`, a menu will allow you to select which to open in the editor.
2654
+ and the root (/), a menu will allow you to select which to open in the editor.
2597
2655
 
2598
2656
  It will use the editor defined in `config_editor_app`, or one specified with `--editor`.
2599
2657
 
2600
- Use `doing config -d` to output the configuration to the terminal, and
2658
+ Use `doing config get` to output the configuration to the terminal, and
2601
2659
  provide a dot-separated key path to get a specific value. Shows the current value
2602
2660
  including keys/overrides set by local configs.)
2603
- arg_name 'KEY_PATH'
2604
2661
  command :config do |c|
2605
2662
  c.example 'doing config', desc: "Open an active configuration in #{Doing::Util.find_default_editor('config')}"
2606
- c.example 'doing config -d doing_file', desc: 'Output the value of a config key as YAML'
2607
- c.example 'doing config -d plugins.say.say_voice -o json', desc: 'Output the value of a key path as JSON'
2663
+ c.example 'doing config get doing_file', desc: 'Output the value of a config key as YAML'
2664
+ c.example 'doing config get plugins.plugin_path -o json', desc: 'Output the value of a key path as JSON'
2665
+ c.example 'doing config set plugins.say.say_voice Alex', desc: 'Set the value of a key path and update config file'
2666
+ c.example 'doing config set plug.say.voice Zarvox', desc: 'Key paths for get and set are fuzzy matched'
2608
2667
 
2609
- c.desc 'Editor to use'
2610
- c.arg_name 'EDITOR'
2611
- c.flag %i[e editor], default_value: nil
2668
+ c.default_command :edit
2612
2669
 
2613
- c.desc 'Show a config key value based on arguments. Separate key paths with colons or dots, e.g. "export_templates.html". Empty arguments outputs the entire config.'
2614
- c.switch %i[d dump], negatable: false
2670
+ c.desc 'DEPRECATED'
2671
+ c.switch %i[d dump]
2615
2672
 
2616
- c.desc 'Format for --dump (json|yaml|raw)'
2617
- c.arg_name 'FORMAT'
2618
- c.flag %i[o output], default_value: 'yaml', must_match: /^(?:y(?:aml)?|j(?:son)?|r(?:aw)?)$/
2673
+ c.desc 'DEPRECATED'
2674
+ c.switch %i[u update]
2619
2675
 
2620
- c.desc 'Update config file with missing configuration options'
2621
- c.switch %i[u update], default_value: false, negatable: false
2676
+ c.desc 'List configuration paths, including .doingrc files in the current and parent directories'
2677
+ c.long_desc 'Config files are listed in order of precedence (if there are multiple configs detected).
2678
+ Values defined in the top item in the list will override values in configutations below it.'
2679
+ c.command :list do |list|
2680
+ list.action do |global, options, args|
2681
+ puts config.additional_configs.join("\n")
2682
+ puts config.config_file
2683
+ end
2684
+ end
2622
2685
 
2623
- if `uname` =~ /Darwin/
2624
- c.desc 'Application to use'
2625
- c.arg_name 'APP_NAME'
2626
- c.flag [:a]
2686
+ c.desc 'Open config file in editor'
2687
+ c.command :edit do |edit|
2688
+ edit.example 'doing config edit', desc: 'Open a config file in the default editor'
2689
+ edit.example 'doing config edit --editor vim', desc: 'Open config in specific editor'
2627
2690
 
2628
- c.desc 'Application bundle id to use'
2629
- c.arg_name 'BUNDLE_ID'
2630
- c.flag [:b]
2691
+ edit.desc 'Editor to use'
2692
+ edit.arg_name 'EDITOR'
2693
+ edit.flag %i[e editor], default_value: nil
2631
2694
 
2632
- c.desc "Use the config_editor_app defined in ~/.doingrc (#{settings.key?('config_editor_app') ? settings['config_editor_app'] : 'config_editor_app not set'})"
2633
- c.switch [:x]
2695
+ if `uname` =~ /Darwin/
2696
+ edit.desc 'Application to use'
2697
+ edit.arg_name 'APP_NAME'
2698
+ edit.flag %i[a app]
2699
+
2700
+ edit.desc 'Application bundle id to use'
2701
+ edit.arg_name 'BUNDLE_ID'
2702
+ edit.flag %i[b bundle_id]
2703
+
2704
+ edit.desc "Use the config_editor_app defined in ~/.config/doing/config.yml (#{settings.key?('config_editor_app') ? settings['config_editor_app'] : 'config_editor_app not set'})"
2705
+ edit.switch %i[x default]
2706
+ end
2707
+
2708
+ edit.action do |global, options, args|
2709
+ if options[:update] || options[:dump]
2710
+ cmd = commands[:config]
2711
+ if options[:update]
2712
+ cmd = cmd.commands[:update]
2713
+ elsif options[:dump]
2714
+ cmd = cmd.commands[:get]
2715
+ end
2716
+ action = cmd.send(:get_action, nil)
2717
+ action.call(global, options, args)
2718
+ Doing.logger.warn('Deprecated:', '--dump and --update are deprecated,
2719
+ use `doing config get` and `doing config update`')
2720
+ Doing.logger.output_results
2721
+ return
2722
+ end
2723
+
2724
+ config_file = config.choose_config
2725
+
2726
+ if `uname` =~ /Darwin/
2727
+ if options[:default]
2728
+ editor = Doing::Util.find_default_editor('config')
2729
+ if editor
2730
+ if Doing::Util.exec_available(editor.split(/ /).first)
2731
+ system %(#{editor} "#{config_file}")
2732
+ else
2733
+ `open -a "#{editor}" "#{config_file}"`
2734
+ end
2735
+ else
2736
+ raise InvalidArgument, 'No viable editor found in config or environment.'
2737
+ end
2738
+ elsif options[:app] || options[:bundle_id]
2739
+ if options[:app]
2740
+ `open -a "#{options[:app]}" "#{config_file}"`
2741
+ elsif options[:bundle_id]
2742
+ `open -b #{options[:bundle_id]} "#{config_file}"`
2743
+ end
2744
+ else
2745
+ editor = options[:editor] || Doing::Util.find_default_editor('config')
2746
+
2747
+ raise MissingEditor, 'No viable editor defined in config or environment' unless editor
2748
+
2749
+ if Doing::Util.exec_available(editor.split(/ /).first)
2750
+ system %(#{editor} "#{config_file}")
2751
+ else
2752
+ `open -a "#{editor}" "#{config_file}"`
2753
+ end
2754
+ end
2755
+ else
2756
+ editor = options[:editor] || Doing::Util.default_editor
2757
+ raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor.split(/ /).first)
2758
+
2759
+ system %(#{editor} "#{config_file}")
2760
+ end
2761
+ end
2634
2762
  end
2635
2763
 
2636
- c.action do |_global_options, options, args|
2637
- if options[:update]
2764
+ c.desc 'Update default config file, adding any missing keys'
2765
+ c.command %i[update refresh] do |update|
2766
+ update.action do |_global, options, args|
2638
2767
  config.configure({rewrite: true, ignore_local: true})
2639
- return
2768
+ Doing.logger.warn('Config:', 'config refreshed')
2640
2769
  end
2770
+ end
2771
+
2772
+ c.desc 'Undo the last change to a config file'
2773
+ c.command :undo do |undo|
2774
+ undo.action do |_global, options, args|
2775
+ config_file = config.choose_config
2776
+ wwid.restore_backup(config_file)
2777
+ end
2778
+ end
2779
+
2780
+ c.desc 'Output a key\'s value'
2781
+ c.arg 'KEY_PATH'
2782
+ c.command %i[get dump] do |dump|
2783
+ dump.example 'doing config get', desc: 'Output the entire configuration'
2784
+ dump.example 'doing config get timer_format --output raw', desc: 'Output the value of timer_format as a plain string'
2785
+ dump.example 'doing config get doing_file', desc: 'Output the value of the doing_file setting, respecting local configurations'
2786
+ dump.example 'doing config get -o json plug.plugpath', desc: 'Key path is fuzzy matched: output the value of plugins->plugin_path as JSON'
2787
+
2788
+ dump.desc 'Format for output (json|yaml|raw)'
2789
+ dump.arg_name 'FORMAT'
2790
+ dump.flag %i[o output], default_value: 'yaml', must_match: /^(?:y(?:aml)?|j(?:son)?|r(?:aw)?)$/
2791
+
2792
+ dump.action do |_global, options, args|
2641
2793
 
2642
- if options[:dump]
2643
2794
  keypath = args.join('.')
2644
2795
  cfg = config.value_for_key(keypath)
2796
+ real_path = config.resolve_key_path(keypath)
2645
2797
 
2646
2798
  if cfg
2647
- $stdout.puts case options[:output]
2648
- when /^j/
2649
- JSON.pretty_generate(cfg)
2650
- when /^r/
2651
- cfg.map {|k, v| v.to_s }
2652
- else
2653
- YAML.dump(cfg)
2654
- end
2799
+ val = cfg.map {|k, v| v }[0]
2800
+ if real_path.count.positive?
2801
+ nested_cfg = {}
2802
+ nested_cfg.deep_set(real_path, val)
2803
+ else
2804
+ nested_cfg = val
2805
+ end
2806
+
2807
+ if options[:output] =~ /^r/
2808
+ if val.is_a?(Hash)
2809
+ $stdout.puts YAML.dump(val)
2810
+ elsif val.is_a?(Array)
2811
+ $stdout.puts val.join(', ')
2812
+ else
2813
+ $stdout.puts val.to_s
2814
+ end
2815
+ else
2816
+ $stdout.puts case options[:output]
2817
+ when /^j/
2818
+ JSON.pretty_generate(val)
2819
+ else
2820
+ YAML.dump(nested_cfg)
2821
+ end
2822
+ end
2655
2823
  else
2656
2824
  Doing.logger.log_now(:error, 'Config:', "Key #{keypath} not found")
2657
2825
  end
2658
2826
  Doing.logger.output_results
2659
2827
  return
2660
2828
  end
2829
+ end
2661
2830
 
2662
- if config.additional_configs.count.positive?
2663
- choices = [config.config_file]
2664
- choices.concat(config.additional_configs)
2665
- res = wwid.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to edit > ')
2831
+ c.desc 'Set a key\'s value in the config file'
2832
+ c.arg 'KEY VALUE'
2833
+ c.command :set do |set|
2834
+ set.example 'doing config set timer_format human', desc: 'Set the value of timer_format to "human"'
2835
+ set.example 'doing config set plug.plugpath ~/my_plugins', desc: 'Key path is fuzzy matched: set the value of plugins->plugin_path'
2666
2836
 
2667
- raise UserCancelled, 'Cancelled' unless res
2837
+ set.desc 'Delete specified key'
2838
+ set.switch %i[r remove], default_value: false, negatable: false
2668
2839
 
2669
- config_file = res.strip || config.config_file
2670
- else
2671
- config_file = config.config_file
2672
- end
2840
+ set.action do |_global, options, args|
2841
+ if args.count < 2 && !options[:remove]
2842
+ raise InvalidArgument, 'config set requires at least two arguments, key path and value'
2673
2843
 
2674
- if `uname` =~ /Darwin/
2675
- if options[:x]
2676
- editor = Doing::Util.find_default_editor('config')
2677
- if editor
2678
- if Doing::Util.exec_available(editor)
2679
- system %(#{editor} "#{config_file}")
2680
- else
2681
- `open -a "#{editor}" "#{config_file}"`
2682
- end
2683
- else
2684
- raise InvalidArgument, 'No viable editor found in config or environment.'
2685
- end
2686
- elsif options[:a] || options[:b]
2687
- if options[:a]
2688
- `open -a "#{options[:a]}" "#{config_file}"`
2689
- elsif options[:b]
2690
- `open -b #{options[:b]} "#{config_file}"`
2691
- end
2692
- else
2693
- editor = options[:e] || Doing::Util.find_default_editor('config')
2844
+ end
2694
2845
 
2695
- raise MissingEditor, 'No viable editor defined in config or environment' unless editor
2846
+ value = options[:remove] ? nil : args.pop
2847
+ keypath = args.join('.')
2848
+ old_value = config.value_for_key(keypath).map { |k, v| v.to_s }
2849
+ real_path = config.resolve_key_path(keypath)
2850
+ raise InvalidArgument, 'Invalid key path' if real_path.empty?
2696
2851
 
2697
- if Doing::Util.exec_available(editor)
2698
- system %(#{editor} "#{config_file}")
2699
- else
2700
- `open -a "#{editor}" "#{config_file}"`
2701
- end
2852
+ config_file = config.choose_config
2853
+ cfg = YAML.safe_load_file(config_file) || {}
2854
+
2855
+ $stderr.puts "Updating #{config_file}".yellow
2856
+
2857
+ if options[:remove]
2858
+ cfg.deep_set(real_path, nil)
2859
+ $stderr.puts "#{'Deleting key:'.yellow} #{real_path.join('->').boldwhite}"
2860
+ else
2861
+ old_value = cfg.dig(*real_path) || 'empty'
2862
+ cfg.deep_set(real_path, value.set_type)
2863
+ $stderr.puts "#{'Key path:'.yellow} #{real_path.join('->').boldwhite}"
2864
+ $stderr.puts "#{'Previous:'.yellow} #{old_value.to_s.boldwhite}"
2865
+ $stderr.puts "#{' New:'.yellow} #{value.set_type.to_s.boldwhite}"
2702
2866
  end
2703
- else
2704
- editor = options[:e] || Doing::Util.default_editor
2705
- raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
2706
2867
 
2707
- system %(#{editor} "#{config_file}")
2868
+ res = Doing::Prompt.yn('Update selected config', default_response: true)
2869
+
2870
+ raise UserCancelled, 'Cancelled' unless res
2871
+
2872
+ Doing::Util.write_to_file(config_file, YAML.dump(cfg), backup: true)
2873
+ Doing.logger.warn('Config:', "#{config_file} updated")
2708
2874
  end
2709
2875
  end
2710
2876
  end
2711
2877
 
2712
- desc 'Undo the last change to the doing_file'
2878
+ desc 'Undo the last change to the Doing file'
2713
2879
  command :undo do |c|
2714
2880
  c.desc 'Specify alternate doing file'
2715
2881
  c.arg_name 'PATH'
2716
2882
  c.flag %i[f file], default_value: wwid.doing_file
2717
2883
 
2718
2884
  c.action do |_global_options, options, _args|
2719
- file = options[:f] || wwid.doing_file
2885
+ file = options[:file] || wwid.doing_file
2720
2886
  wwid.restore_backup(file)
2721
2887
  end
2722
2888
  end
@@ -2764,6 +2930,7 @@ command :import do |c|
2764
2930
  c.arg_name 'PREFIX'
2765
2931
  c.flag :prefix
2766
2932
 
2933
+ # TODO: Allow time range filtering
2767
2934
  c.desc 'Import entries older than date'
2768
2935
  c.arg_name 'DATE_STRING'
2769
2936
  c.flag [:before]
@@ -2793,10 +2960,10 @@ command :import do |c|
2793
2960
  date_string = options[:from]
2794
2961
  if date_string =~ / (to|through|thru|(un)?til|-+) /
2795
2962
  dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
2796
- start = wwid.chronify(dates[0], guess: :begin)
2797
- finish = wwid.chronify(dates[2], guess: :end)
2963
+ start = dates[0].chronify(guess: :begin)
2964
+ finish = dates[2].chronify(guess: :end)
2798
2965
  else
2799
- start = wwid.chronify(date_string, guess: :begin)
2966
+ start = date_string.chronify(guess: :begin)
2800
2967
  finish = false
2801
2968
  end
2802
2969
  raise InvalidTimeExpression, 'Unrecognized date string' unless start
@@ -2823,9 +2990,9 @@ pre do |global, _command, _options, _args|
2823
2990
 
2824
2991
  $stdout.puts "doing v#{Doing::VERSION}" if global[:version]
2825
2992
  unless STDOUT.isatty
2826
- Doing::Color::coloring = global[:pager] ? global[:color] : false
2993
+ Doing::Color.coloring = global[:pager] ? global[:color] : false
2827
2994
  else
2828
- Doing::Color::coloring = global[:color]
2995
+ Doing::Color.coloring = global[:color]
2829
2996
  end
2830
2997
 
2831
2998
  # Return true to proceed; false to abort and not call the
@@ -2853,13 +3020,21 @@ end
2853
3020
 
2854
3021
  around do |global, command, options, arguments, code|
2855
3022
  # Doing.logger.debug('Pager:', "Global: #{global[:pager]}, Config: #{settings['paginate']}, Pager: #{Doing::Pager.paginate}")
2856
- Doing.logger.adjust_verbosity(global)
3023
+ if env_log_level.nil?
3024
+ Doing.logger.adjust_verbosity(global)
3025
+ end
2857
3026
 
2858
3027
  if global[:stdout]
2859
3028
  Doing.logger.logdev = $stdout
2860
3029
  end
2861
3030
 
2862
- wwid.default_option = global[:default]
3031
+ if global[:yes]
3032
+ Doing::Prompt.force_answer = true
3033
+ elsif global[:no]
3034
+ Doing::Prompt.force_answer = false
3035
+ else
3036
+ Doing::Prompt.default_answer = global[:default]
3037
+ end
2863
3038
 
2864
3039
  if global[:config_file] && global[:config_file] != config.config_file
2865
3040
  Doing.logger.warn(format('%sWARNING:%s %sThe use of --config_file is deprecated, please set the environment variable DOING_CONFIG instead.', colors.flamingo, colors.default, colors.boldred))