doing 2.0.25 → 2.1.0pre

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +18 -15
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +28 -0
  6. data/Gemfile.lock +8 -1
  7. data/README.md +1 -1
  8. data/Rakefile +23 -4
  9. data/bin/doing +205 -127
  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 +477 -670
  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 +599 -23
  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 +453 -173
  50. data/doc/top-level-namespace.html +1 -1
  51. data/doing.gemspec +3 -0
  52. data/doing.rdoc +40 -12
  53. data/example_plugin.rb +3 -3
  54. data/lib/completion/_doing.zsh +1 -1
  55. data/lib/completion/doing.bash +8 -8
  56. data/lib/completion/doing.fish +1 -1
  57. data/lib/doing/array.rb +36 -0
  58. data/lib/doing/colors.rb +70 -66
  59. data/lib/doing/completion.rb +6 -0
  60. data/lib/doing/configuration.rb +69 -28
  61. data/lib/doing/hash.rb +37 -0
  62. data/lib/doing/item.rb +77 -12
  63. data/lib/doing/items.rb +125 -0
  64. data/lib/doing/log_adapter.rb +55 -3
  65. data/lib/doing/note.rb +53 -1
  66. data/lib/doing/pager.rb +49 -38
  67. data/lib/doing/plugins/export/markdown_export.rb +4 -4
  68. data/lib/doing/plugins/export/template_export.rb +2 -2
  69. data/lib/doing/plugins/import/calendar_import.rb +4 -4
  70. data/lib/doing/plugins/import/doing_import.rb +5 -7
  71. data/lib/doing/plugins/import/timing_import.rb +3 -3
  72. data/lib/doing/prompt.rb +206 -0
  73. data/lib/doing/section.rb +30 -0
  74. data/lib/doing/string.rb +103 -27
  75. data/lib/doing/util.rb +14 -6
  76. data/lib/doing/version.rb +1 -1
  77. data/lib/doing/wwid.rb +306 -621
  78. data/lib/doing.rb +6 -2
  79. data/lib/examples/plugins/capture_thing_import.rb +162 -0
  80. metadata +73 -5
  81. 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
@@ -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
@@ -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
@@ -613,22 +629,29 @@ command :later do |c|
613
629
  if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
614
630
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
615
631
 
616
- input = args.empty? ? '' : args.join(' ')
632
+ input += date.strftime('%F %R | ')
633
+ input += args.empty? ? '' : args.join(' ')
617
634
  input = wwid.fork_editor(input).strip
618
635
  raise EmptyInput, 'No content' unless input && !input.empty?
619
636
 
620
- title, note = wwid.format_input(input)
621
- note.push(options[:n]) if options[:n]
637
+ d, title, note = wwid.format_input(input)
638
+ date = d.nil? ? date : d
639
+ note.add(options[:note]) if options[:note]
622
640
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
623
641
  wwid.write(wwid.doing_file)
624
642
  elsif !args.empty?
625
- title, note = wwid.format_input(args.join(' '))
626
- note.push(options[:n]) if options[:n]
643
+ d, title, note = wwid.format_input(args.join(' '))
644
+ date = d.nil? ? date : d
645
+ note.add(options[:note]) if options[:note]
627
646
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
628
647
  wwid.write(wwid.doing_file)
629
648
  elsif $stdin.stat.size.positive?
630
- title, note = wwid.format_input($stdin.read)
631
- note.push(options[:n]) if options[:n]
649
+ d, title, note = wwid.format_input($stdin.read)
650
+ unless d.nil?
651
+ Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
652
+ date = d
653
+ end
654
+ note.add(options[:note]) if options[:note]
632
655
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
633
656
  wwid.write(wwid.doing_file)
634
657
  else
@@ -659,9 +682,9 @@ command %i[done did] do |c|
659
682
  c.arg_name 'DATE_STRING'
660
683
  c.flag [:at]
661
684
 
662
- c.desc 'Backdate start date by interval [4pm|20m|2h|yesterday noon]'
685
+ c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
663
686
  c.arg_name 'DATE_STRING'
664
- c.flag %i[b back]
687
+ c.flag %i[b back started]
665
688
 
666
689
  c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM).
667
690
  If used without the --back option, the start date will be moved back to allow
@@ -710,8 +733,6 @@ command %i[done did] do |c|
710
733
  date = options[:took] ? finish_date - took : finish_date
711
734
  elsif options[:took]
712
735
  finish_date = date + took
713
- elsif options[:back]
714
- finish_date = date
715
736
  else
716
737
  finish_date = Time.now
717
738
  end
@@ -743,16 +764,17 @@ command %i[done did] do |c|
743
764
 
744
765
  old_entry = last_entry.dup
745
766
  last_entry.note.add(note)
746
- input = [last_entry.title, last_entry.note.to_s].join("\n")
767
+ input = ["#{last_entry.date.strftime('%F %R | ')}#{last_entry.title}", last_entry.note.strip_lines.join("\n")].join("\n")
747
768
  else
748
769
  is_new = true
749
- input = [args.join(' '), note.to_s].join("\n")
770
+ input = ["#{date.strftime('%F %R | ')}#{args.join(' ')}", note.strip_lines.join("\n")].join("\n")
750
771
  end
751
772
 
752
773
  input = wwid.fork_editor(input).strip
753
774
  raise EmptyInput, 'No content' unless input && !input.empty?
754
775
 
755
- title, note = wwid.format_input(input)
776
+ d, title, note = wwid.format_input(input)
777
+ date = d.nil? ? date : d
756
778
  new_entry = Doing::Item.new(date, title, section, note)
757
779
  if new_entry.should_finish?
758
780
  if new_entry.should_time?
@@ -763,23 +785,23 @@ command %i[done did] do |c|
763
785
  end
764
786
 
765
787
  if (is_new)
766
- wwid.content[section][:items].push(new_entry)
788
+ wwid.content.push(new_entry)
767
789
  else
768
- wwid.update_item(old_entry, new_entry)
790
+ wwid.content.update_item(old_entry, new_entry)
769
791
  end
770
792
 
771
- if options[:a]
793
+ if options[:archive]
772
794
  wwid.move_item(new_entry, 'Archive', label: true)
773
795
  end
774
796
 
775
797
  wwid.write(wwid.doing_file)
776
798
  elsif args.empty? && $stdin.stat.size.zero?
777
- if options[:r]
799
+ if options[:remove]
778
800
  wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
779
801
  else
780
802
  note = options[:note] ? Doing::Note.new(options[:note]) : nil
781
803
  opt = {
782
- archive: options[:a],
804
+ archive: options[:archive],
783
805
  back: finish_date,
784
806
  count: 1,
785
807
  date: options[:date],
@@ -793,9 +815,10 @@ command %i[done did] do |c|
793
815
  end
794
816
  elsif !args.empty?
795
817
  note = Doing::Note.new(options[:note])
796
- title, new_note = wwid.format_input([args.join(' '), note.to_s].join("\n"))
818
+ d, title, new_note = wwid.format_input([args.join(' '), note.strip_lines.join("\n")].join("\n"))
819
+ date = d.nil? ? date : d
797
820
  title.chomp!
798
- section = 'Archive' if options[:a]
821
+ section = 'Archive' if options[:archive]
799
822
  new_entry = Doing::Item.new(date, title, section, new_note)
800
823
  if new_entry.should_finish?
801
824
  if new_entry.should_time?
@@ -804,13 +827,17 @@ command %i[done did] do |c|
804
827
  new_entry.tag('done')
805
828
  end
806
829
  end
807
- wwid.content[section][:items].push(new_entry)
830
+ wwid.content.push(new_entry)
808
831
  wwid.write(wwid.doing_file)
809
832
  Doing.logger.info('Entry Added:', new_entry.title)
810
833
  elsif $stdin.stat.size.positive?
811
- title, note = wwid.format_input($stdin.read)
834
+ d, title, note = wwid.format_input($stdin.read.strip)
835
+ unless d.nil?
836
+ Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
837
+ date = d
838
+ end
812
839
  note.add(options[:note]) if options[:note]
813
- section = options[:a] ? 'Archive' : section
840
+ section = options[:archive] ? 'Archive' : section
814
841
  new_entry = Doing::Item.new(date, title, section, note)
815
842
 
816
843
  if new_entry.should_finish?
@@ -821,7 +848,7 @@ command %i[done did] do |c|
821
848
  end
822
849
  end
823
850
 
824
- wwid.content[section][:items].push(new_entry)
851
+ wwid.content.push(new_entry)
825
852
  wwid.write(wwid.doing_file)
826
853
  Doing.logger.info('Entry Added:', new_entry.title)
827
854
  else
@@ -907,7 +934,7 @@ command :cancel do |c|
907
934
  end
908
935
 
909
936
  opts = {
910
- archive: options[:a],
937
+ archive: options[:archive],
911
938
  case: options[:case].normalize_case,
912
939
  count: count,
913
940
  date: false,
@@ -1279,15 +1306,15 @@ command :tag do |c|
1279
1306
  end
1280
1307
 
1281
1308
 
1282
- question = if options[:a]
1309
+ question = if options[:aarchive]
1283
1310
  "Are you sure you want to autotag all records#{section_q}"
1284
- elsif options[:r]
1311
+ elsif options[:remove]
1285
1312
  "Are you sure you want to remove #{tags.join(' and ')} from all records#{section_q}"
1286
1313
  else
1287
1314
  "Are you sure you want to add #{tags.join(' and ')} to all records#{section_q}"
1288
1315
  end
1289
1316
 
1290
- res = wwid.yn(question, default_response: false)
1317
+ res = Doing::Prompt.yn(question, default_response: false)
1291
1318
 
1292
1319
  raise UserCancelled unless res
1293
1320
  end
@@ -1408,7 +1435,7 @@ command [:mark, :flag] do |c|
1408
1435
  "Are you sure you want to flag all records#{section_q}"
1409
1436
  end
1410
1437
 
1411
- res = wwid.yn(question, default_response: false)
1438
+ res = Doing::Prompt.yn(question, default_response: false)
1412
1439
 
1413
1440
  exit_now! 'Cancelled' unless res
1414
1441
  end
@@ -1598,7 +1625,6 @@ command :show do |c|
1598
1625
  end
1599
1626
 
1600
1627
  opt = options.dup
1601
-
1602
1628
  opt[:sort_tags] = options[:tag_sort] =~ /^n/i
1603
1629
  opt[:count] = options[:count].to_i
1604
1630
  opt[:date_filter] = dates
@@ -1729,7 +1755,7 @@ command :recent do |c|
1729
1755
  c.switch %i[i interactive], negatable: false, default_value: false
1730
1756
 
1731
1757
  c.action do |global_options, options, args|
1732
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
1758
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
1733
1759
 
1734
1760
  unless global_options[:version]
1735
1761
  if settings['templates']['recent'].key?('count')
@@ -1744,7 +1770,7 @@ command :recent do |c|
1744
1770
  count = args.empty? ? config_count : args[0].to_i
1745
1771
  end
1746
1772
 
1747
- options[:t] = true if options[:totals]
1773
+ options[:times] = true if options[:totals]
1748
1774
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1749
1775
 
1750
1776
  template = settings['templates']['recent'].deep_merge(settings['templates']['default'])
@@ -1753,7 +1779,7 @@ command :recent do |c|
1753
1779
  opts = {
1754
1780
  sort_tags: options[:sort_tags],
1755
1781
  tags_color: tags_color,
1756
- times: options[:t],
1782
+ times: options[:times],
1757
1783
  totals: options[:totals],
1758
1784
  interactive: options[:interactive]
1759
1785
  }
@@ -1802,7 +1828,7 @@ command :today do |c|
1802
1828
  c.action do |_global_options, options, _args|
1803
1829
  raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1804
1830
 
1805
- options[:t] = true if options[:totals]
1831
+ options[:times] = true if options[:totals]
1806
1832
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1807
1833
  opt = {
1808
1834
  after: options[:after],
@@ -1863,14 +1889,14 @@ command :on do |c|
1863
1889
 
1864
1890
  raise InvalidTimeExpression, 'Unrecognized date string' unless start
1865
1891
 
1866
- message = "Date interpreted as #{start}"
1892
+ message = "date interpreted as #{start}"
1867
1893
  message += " to #{finish}" if finish
1868
- Doing.logger.debug(message)
1894
+ Doing.logger.debug('Interpreter:', message)
1869
1895
 
1870
- options[:t] = true if options[:totals]
1896
+ options[:times] = true if options[:totals]
1871
1897
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1872
1898
 
1873
- Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1899
+ Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
1874
1900
  { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1875
1901
  end
1876
1902
  end
@@ -1918,12 +1944,12 @@ command :since do |c|
1918
1944
 
1919
1945
  raise InvalidTimeExpression, 'Unrecognized date string' unless start
1920
1946
 
1921
- Doing.logger.debug("Date interpreted as #{start} through the current time")
1947
+ Doing.logger.debug('Interpreter:', "date interpreted as #{start} through the current time")
1922
1948
 
1923
- options[:t] = true if options[:totals]
1949
+ options[:times] = true if options[:totals]
1924
1950
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1925
1951
 
1926
- Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1952
+ Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
1927
1953
  { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1928
1954
  end
1929
1955
  end
@@ -2053,9 +2079,9 @@ command :last do |c|
2053
2079
  end
2054
2080
 
2055
2081
  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] })
2082
+ 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
2083
  else
2058
- Doing::Pager::page wwid.last(times: true, section: options[:s],
2084
+ Doing::Pager::page wwid.last(times: true, section: options[:section],
2059
2085
  options: { search: search, fuzzy: options[:fuzzy], case: options[:case], negate: options[:not], tag: tags, tag_bool: options[:bool] }).strip
2060
2086
  end
2061
2087
  end
@@ -2067,8 +2093,8 @@ command :sections do |c|
2067
2093
  c.switch %i[c column], negatable: false, default_value: false
2068
2094
 
2069
2095
  c.action do |_global_options, options, _args|
2070
- joiner = options[:c] ? "\n" : "\t"
2071
- print wwid.sections.join(joiner)
2096
+ joiner = options[:column] ? "\n" : "\t"
2097
+ print wwid.content.section_titles.join(joiner)
2072
2098
  end
2073
2099
  end
2074
2100
 
@@ -2089,7 +2115,7 @@ command :add_section do |c|
2089
2115
  c.action do |_global_options, _options, args|
2090
2116
  raise InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
2091
2117
 
2092
- wwid.add_section(args.join(' ').cap_first)
2118
+ wwid.content.add_section(args.join(' ').cap_first, log: true)
2093
2119
  wwid.write(wwid.doing_file)
2094
2120
  end
2095
2121
  end
@@ -2247,6 +2273,7 @@ command :view do |c|
2247
2273
  rescue WrongCommand => exception
2248
2274
  cmd = commands[:show]
2249
2275
  options[:sort] = 'asc'
2276
+ options[:tag_order] = 'asc'
2250
2277
  action = cmd.send(:get_action, nil)
2251
2278
  return action.call(global_options, options, args)
2252
2279
  end
@@ -2289,12 +2316,9 @@ command :view do |c|
2289
2316
  # If the -o/--output flag was specified, override any default in the view template
2290
2317
  options[:output] ||= view.key?('output_format') ? view['output_format'] : 'template'
2291
2318
 
2292
- count = if options[:c]
2293
- options[:c]
2294
- else
2295
- view.key?('count') ? view['count'] : 10
2296
- end
2297
- section = if options[:s]
2319
+ count = options[:count] ? options[:count] : view.key?('count') ? view['count'] : 10
2320
+
2321
+ section = if options[:section]
2298
2322
  section
2299
2323
  else
2300
2324
  view.key?('section') ? view['section'] : settings['current_section']
@@ -2312,7 +2336,7 @@ command :view do |c|
2312
2336
  view.key?('tag_order') ? view['tag_order'].normalize_order : 'asc'
2313
2337
  end
2314
2338
 
2315
- options[:t] = true if totals
2339
+ options[:times] = true if totals
2316
2340
  output_format = options[:output]&.downcase || 'template'
2317
2341
 
2318
2342
  options[:sort_tags] = if options[:tag_sort]
@@ -2386,7 +2410,7 @@ command :views do |c|
2386
2410
  c.switch %i[c column], default_value: false
2387
2411
 
2388
2412
  c.action do |_global_options, options, _args|
2389
- joiner = options[:c] ? "\n" : "\t"
2413
+ joiner = options[:column] ? "\n" : "\t"
2390
2414
  print wwid.views.join(joiner)
2391
2415
  end
2392
2416
  end
@@ -2552,6 +2576,10 @@ end
2552
2576
  desc 'Open the "doing" file in an editor'
2553
2577
  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
2578
  command :open do |c|
2579
+ c.desc 'Open with editor command (e.g. vim, mate)'
2580
+ c.arg_name 'COMMAND'
2581
+ c.flag %i[e editor]
2582
+
2555
2583
  if `uname` =~ /Darwin/
2556
2584
  c.desc 'Open with app name'
2557
2585
  c.arg_name 'APP_NAME'
@@ -2567,11 +2595,17 @@ command :open do |c|
2567
2595
  params.delete_if do |k, v|
2568
2596
  k.instance_of?(String) || v.nil? || v == false
2569
2597
  end
2570
- if `uname` =~ /Darwin/
2598
+
2599
+ if options[:editor]
2600
+ raise MissingEditor, "Editor #{options[:editor]} not found" unless Doing::Util.exec_available(options[:editor])
2601
+
2602
+ editor = TTY::Which.which(options[:editor])
2603
+ system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
2604
+ elsif `uname` =~ /Darwin/
2571
2605
  if options[:app]
2572
- system %(open -a "#{options[:a]}" "#{File.expand_path(wwid.doing_file)}")
2606
+ system %(open -a "#{options[:app]}" "#{File.expand_path(wwid.doing_file)}")
2573
2607
  elsif options[:bundle_id]
2574
- system %(open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}")
2608
+ system %(open -b "#{options[:bundle_id]}" "#{File.expand_path(wwid.doing_file)}")
2575
2609
  elsif Doing::Util.find_default_editor('doing_file')
2576
2610
  editor = Doing::Util.find_default_editor('doing_file')
2577
2611
  if Doing::Util.exec_available(editor)
@@ -2615,6 +2649,16 @@ command :config do |c|
2615
2649
  c.desc 'DEPRECATED'
2616
2650
  c.switch %i[u update]
2617
2651
 
2652
+ c.desc 'List configuration paths, including .doingrc files in the current and parent directories'
2653
+ c.long_desc 'Config files are listed in order of precedence (if there are multiple configs detected).
2654
+ Values defined in the top item in the list will override values in configutations below it.'
2655
+ c.command :list do |list|
2656
+ list.action do |global, options, args|
2657
+ puts config.additional_configs.join("\n")
2658
+ puts config.config_file
2659
+ end
2660
+ end
2661
+
2618
2662
  c.desc 'Open config file in editor'
2619
2663
  c.command :edit do |edit|
2620
2664
  edit.example 'doing config edit', desc: 'Open a config file in the default editor'
@@ -2627,14 +2671,14 @@ command :config do |c|
2627
2671
  if `uname` =~ /Darwin/
2628
2672
  edit.desc 'Application to use'
2629
2673
  edit.arg_name 'APP_NAME'
2630
- edit.flag [:a]
2674
+ edit.flag %i[a app]
2631
2675
 
2632
2676
  edit.desc 'Application bundle id to use'
2633
2677
  edit.arg_name 'BUNDLE_ID'
2634
- edit.flag [:b]
2678
+ edit.flag %i[b bundle_id]
2635
2679
 
2636
2680
  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'})"
2637
- edit.switch [:x]
2681
+ edit.switch %i[x default]
2638
2682
  end
2639
2683
 
2640
2684
  edit.action do |global, options, args|
@@ -2656,7 +2700,7 @@ command :config do |c|
2656
2700
  config_file = config.choose_config
2657
2701
 
2658
2702
  if `uname` =~ /Darwin/
2659
- if options[:x]
2703
+ if options[:default]
2660
2704
  editor = Doing::Util.find_default_editor('config')
2661
2705
  if editor
2662
2706
  if Doing::Util.exec_available(editor)
@@ -2667,14 +2711,14 @@ command :config do |c|
2667
2711
  else
2668
2712
  raise InvalidArgument, 'No viable editor found in config or environment.'
2669
2713
  end
2670
- elsif options[:a] || options[:b]
2671
- if options[:a]
2672
- `open -a "#{options[:a]}" "#{config_file}"`
2673
- elsif options[:b]
2674
- `open -b #{options[:b]} "#{config_file}"`
2714
+ elsif options[:app] || options[:bundle_id]
2715
+ if options[:app]
2716
+ `open -a "#{options[:app]}" "#{config_file}"`
2717
+ elsif options[:bundle_id]
2718
+ `open -b #{options[:bundle_id]} "#{config_file}"`
2675
2719
  end
2676
2720
  else
2677
- editor = options[:e] || Doing::Util.find_default_editor('config')
2721
+ editor = options[:editor] || Doing::Util.find_default_editor('config')
2678
2722
 
2679
2723
  raise MissingEditor, 'No viable editor defined in config or environment' unless editor
2680
2724
 
@@ -2685,7 +2729,7 @@ command :config do |c|
2685
2729
  end
2686
2730
  end
2687
2731
  else
2688
- editor = options[:e] || Doing::Util.default_editor
2732
+ editor = options[:editor] || Doing::Util.default_editor
2689
2733
  raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
2690
2734
 
2691
2735
  system %(#{editor} "#{config_file}")
@@ -2725,16 +2769,33 @@ command :config do |c|
2725
2769
 
2726
2770
  keypath = args.join('.')
2727
2771
  cfg = config.value_for_key(keypath)
2772
+ real_path = config.resolve_key_path(keypath)
2728
2773
 
2729
2774
  if cfg
2730
- $stdout.puts case options[:output]
2731
- when /^j/
2732
- JSON.pretty_generate(cfg)
2733
- when /^r/
2734
- cfg.map {|k, v| v.to_s }
2735
- else
2736
- YAML.dump(cfg)
2737
- end
2775
+ val = cfg.map {|k, v| v }[0]
2776
+ if real_path.count.positive?
2777
+ nested_cfg = {}
2778
+ nested_cfg.deep_set(real_path, val)
2779
+ else
2780
+ nested_cfg = val
2781
+ end
2782
+
2783
+ if options[:output] =~ /^r/
2784
+ if val.is_a?(Hash)
2785
+ $stdout.puts YAML.dump(val)
2786
+ elsif val.is_a?(Array)
2787
+ $stdout.puts val.join(', ')
2788
+ else
2789
+ $stdout.puts val.to_s
2790
+ end
2791
+ else
2792
+ $stdout.puts case options[:output]
2793
+ when /^j/
2794
+ JSON.pretty_generate(val)
2795
+ else
2796
+ YAML.dump(nested_cfg)
2797
+ end
2798
+ end
2738
2799
  else
2739
2800
  Doing.logger.log_now(:error, 'Config:', "Key #{keypath} not found")
2740
2801
  end
@@ -2749,46 +2810,55 @@ command :config do |c|
2749
2810
  set.example 'doing config set timer_format human', desc: 'Set the value of timer_format to "human"'
2750
2811
  set.example 'doing config set plug.plugpath ~/my_plugins', desc: 'Key path is fuzzy matched: set the value of plugins->plugin_path'
2751
2812
 
2813
+ set.desc 'Delete specified key'
2814
+ set.switch %i[r remove], default_value: false, negatable: false
2815
+
2752
2816
  set.action do |_global, options, args|
2753
- if args.count < 2
2817
+ if args.count < 2 && !options[:remove]
2754
2818
  raise InvalidArgument, 'config set requires at least two arguments, key path and value'
2755
2819
 
2756
2820
  end
2757
2821
 
2758
- value = args.pop
2822
+ value = options[:remove] ? nil : args.pop
2759
2823
  keypath = args.join('.')
2760
2824
  old_value = config.value_for_key(keypath).map { |k, v| v.to_s }
2761
2825
  real_path = config.resolve_key_path(keypath)
2762
2826
  raise InvalidArgument, 'Invalid key path' if real_path.empty?
2763
2827
 
2764
- key = real_path.last
2765
- real_path[0...-1].inject(config.settings, :fetch)[key] = value
2766
-
2767
2828
  config_file = config.choose_config
2829
+ cfg = YAML.safe_load_file(config_file) || {}
2768
2830
 
2769
- prev_level = Doing.logger.level
2770
- Doing.logger.adjust_verbosity({ level: :info })
2771
- Doing.logger.log_now(:info, 'Config:', "Updating #{config_file}")
2772
- Doing.logger.log_now(:info, 'Config:', "#{real_path.join('->')} => #{value}")
2773
- res = wwid.yn('Update selected config', default_response: true)
2831
+ $stderr.puts "Updating #{config_file}".yellow
2832
+
2833
+ if options[:remove]
2834
+ cfg.deep_set(real_path, nil)
2835
+ $stderr.puts "#{'Deleting key:'.yellow} #{real_path.join('->').boldwhite}"
2836
+ else
2837
+ old_value = cfg.dig(*real_path) || 'empty'
2838
+ cfg.deep_set(real_path, value.set_type)
2839
+ $stderr.puts "#{'Key path:'.yellow} #{real_path.join('->').boldwhite}"
2840
+ $stderr.puts "#{'Previous:'.yellow} #{old_value.to_s.boldwhite}"
2841
+ $stderr.puts "#{' New:'.yellow} #{value.set_type.to_s.boldwhite}"
2842
+ end
2843
+
2844
+ res = Doing::Prompt.yn('Update selected config', default_response: true)
2774
2845
 
2775
2846
  raise UserCancelled, 'Cancelled' unless res
2776
2847
 
2777
- Doing.logger.adjust_verbosity({ level: prev_level })
2778
- Doing::Util.write_to_file(config_file, YAML.dump(config.settings), backup: true)
2848
+ Doing::Util.write_to_file(config_file, YAML.dump(cfg), backup: true)
2779
2849
  Doing.logger.warn('Config:', "#{config_file} updated")
2780
2850
  end
2781
2851
  end
2782
2852
  end
2783
2853
 
2784
- desc 'Undo the last change to the doing_file'
2854
+ desc 'Undo the last change to the Doing file'
2785
2855
  command :undo do |c|
2786
2856
  c.desc 'Specify alternate doing file'
2787
2857
  c.arg_name 'PATH'
2788
2858
  c.flag %i[f file], default_value: wwid.doing_file
2789
2859
 
2790
2860
  c.action do |_global_options, options, _args|
2791
- file = options[:f] || wwid.doing_file
2861
+ file = options[:file] || wwid.doing_file
2792
2862
  wwid.restore_backup(file)
2793
2863
  end
2794
2864
  end
@@ -2895,9 +2965,9 @@ pre do |global, _command, _options, _args|
2895
2965
 
2896
2966
  $stdout.puts "doing v#{Doing::VERSION}" if global[:version]
2897
2967
  unless STDOUT.isatty
2898
- Doing::Color::coloring = global[:pager] ? global[:color] : false
2968
+ Doing::Color.coloring = global[:pager] ? global[:color] : false
2899
2969
  else
2900
- Doing::Color::coloring = global[:color]
2970
+ Doing::Color.coloring = global[:color]
2901
2971
  end
2902
2972
 
2903
2973
  # Return true to proceed; false to abort and not call the
@@ -2925,13 +2995,21 @@ end
2925
2995
 
2926
2996
  around do |global, command, options, arguments, code|
2927
2997
  # Doing.logger.debug('Pager:', "Global: #{global[:pager]}, Config: #{settings['paginate']}, Pager: #{Doing::Pager.paginate}")
2928
- Doing.logger.adjust_verbosity(global)
2998
+ if env_log_level.nil?
2999
+ Doing.logger.adjust_verbosity(global)
3000
+ end
2929
3001
 
2930
3002
  if global[:stdout]
2931
3003
  Doing.logger.logdev = $stdout
2932
3004
  end
2933
3005
 
2934
- wwid.default_option = global[:default]
3006
+ if global[:yes]
3007
+ Doing::Prompt.force_answer = true
3008
+ elsif global[:no]
3009
+ Doing::Prompt.force_answer = false
3010
+ else
3011
+ Doing::Prompt.default_answer = global[:default]
3012
+ end
2935
3013
 
2936
3014
  if global[:config_file] && global[:config_file] != config.config_file
2937
3015
  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))