doing 2.0.25 → 2.1.0pre

Sign up to get free protection for your applications and to get access to all the features.
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))