doing 2.1.2pre → 2.1.6pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) 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/.yardopts +1 -1
  6. data/CHANGELOG.md +62 -14
  7. data/Gemfile.lock +25 -1
  8. data/README.md +5 -1
  9. data/Rakefile +2 -0
  10. data/bin/doing +429 -142
  11. data/docs/_config.yml +1 -0
  12. data/{doc → docs/doc}/Array.html +63 -1
  13. data/docs/doc/BooleanTermParser/Clause.html +293 -0
  14. data/docs/doc/BooleanTermParser/Operator.html +172 -0
  15. data/docs/doc/BooleanTermParser/Query.html +417 -0
  16. data/docs/doc/BooleanTermParser/QueryParser.html +135 -0
  17. data/docs/doc/BooleanTermParser/QueryTransformer.html +124 -0
  18. data/docs/doc/BooleanTermParser.html +115 -0
  19. data/docs/doc/Doing/CLIFormat.html +131 -0
  20. data/{doc → docs/doc}/Doing/Color.html +2 -2
  21. data/{doc → docs/doc}/Doing/Completion.html +1 -1
  22. data/{doc → docs/doc}/Doing/Configuration.html +163 -69
  23. data/{doc → docs/doc}/Doing/Content.html +0 -0
  24. data/{doc → docs/doc}/Doing/Errors/DoingNoTraceError.html +1 -1
  25. data/{doc → docs/doc}/Doing/Errors/DoingRuntimeError.html +1 -1
  26. data/{doc → docs/doc}/Doing/Errors/DoingStandardError.html +1 -1
  27. data/{doc → docs/doc}/Doing/Errors/EmptyInput.html +1 -1
  28. data/{doc → docs/doc}/Doing/Errors/NoResults.html +1 -1
  29. data/{doc → docs/doc}/Doing/Errors/PluginException.html +1 -1
  30. data/{doc → docs/doc}/Doing/Errors/UserCancelled.html +1 -1
  31. data/{doc → docs/doc}/Doing/Errors/WrongCommand.html +1 -1
  32. data/{doc → docs/doc}/Doing/Errors.html +1 -1
  33. data/{doc → docs/doc}/Doing/Hooks.html +1 -1
  34. data/{doc → docs/doc}/Doing/Item.html +135 -89
  35. data/{doc → docs/doc}/Doing/Items.html +36 -2
  36. data/{doc → docs/doc}/Doing/LogAdapter.html +70 -1
  37. data/{doc → docs/doc}/Doing/Note.html +5 -134
  38. data/{doc → docs/doc}/Doing/Pager.html +1 -1
  39. data/{doc → docs/doc}/Doing/Plugins.html +431 -35
  40. data/{doc → docs/doc}/Doing/Prompt.html +70 -18
  41. data/{doc → docs/doc}/Doing/Section.html +1 -1
  42. data/docs/doc/Doing/TemplateString.html +713 -0
  43. data/docs/doc/Doing/Util/Backup.html +686 -0
  44. data/{doc → docs/doc}/Doing/Util.html +16 -4
  45. data/{doc → docs/doc}/Doing/WWID.html +133 -73
  46. data/{doc → docs/doc}/Doing/WWIDFile.html +0 -0
  47. data/{doc → docs/doc}/Doing.html +4 -4
  48. data/{doc → docs/doc}/GLI/Commands/MarkdownDocumentListener.html +1 -1
  49. data/{doc → docs/doc}/GLI/Commands.html +1 -1
  50. data/{doc → docs/doc}/GLI.html +1 -1
  51. data/{doc → docs/doc}/Hash.html +1 -1
  52. data/docs/doc/PhraseParser/Operator.html +172 -0
  53. data/docs/doc/PhraseParser/PhraseClause.html +303 -0
  54. data/docs/doc/PhraseParser/Query.html +495 -0
  55. data/docs/doc/PhraseParser/QueryParser.html +136 -0
  56. data/docs/doc/PhraseParser/QueryTransformer.html +124 -0
  57. data/docs/doc/PhraseParser/TermClause.html +293 -0
  58. data/docs/doc/PhraseParser.html +115 -0
  59. data/{doc → docs/doc}/Status.html +1 -1
  60. data/{doc → docs/doc}/String.html +319 -13
  61. data/{doc → docs/doc}/Symbol.html +35 -1
  62. data/{doc → docs/doc}/Time.html +70 -2
  63. data/{doc → docs/doc}/_index.html +132 -4
  64. data/docs/doc/class_list.html +51 -0
  65. data/{doc → docs/doc}/css/common.css +0 -0
  66. data/{doc → docs/doc}/css/full_list.css +0 -0
  67. data/{doc → docs/doc}/css/style.css +0 -0
  68. data/{doc → docs/doc}/file.README.html +6 -2
  69. data/{doc → docs/doc}/file_list.html +0 -0
  70. data/{doc → docs/doc}/frames.html +0 -0
  71. data/{doc → docs/doc}/index.html +6 -2
  72. data/{doc → docs/doc}/js/app.js +0 -0
  73. data/{doc → docs/doc}/js/full_list.js +0 -0
  74. data/{doc → docs/doc}/js/jquery.js +0 -0
  75. data/{doc → docs/doc}/method_list.html +684 -196
  76. data/{doc → docs/doc}/top-level-namespace.html +2 -2
  77. data/docs/index.md +60 -0
  78. data/doing.gemspec +3 -0
  79. data/doing.rdoc +222 -74
  80. data/example_plugin.rb +3 -1
  81. data/lib/completion/_doing.zsh +53 -41
  82. data/lib/completion/doing.bash +17 -6
  83. data/lib/completion/doing.fish +321 -2
  84. data/lib/doing/array.rb +9 -0
  85. data/lib/doing/boolean_term_parser.rb +86 -0
  86. data/lib/doing/completion/fish_completion.rb +46 -3
  87. data/lib/doing/completion/zsh_completion.rb +1 -1
  88. data/lib/doing/configuration.rb +48 -21
  89. data/lib/doing/item.rb +105 -10
  90. data/lib/doing/items.rb +6 -0
  91. data/lib/doing/log_adapter.rb +28 -0
  92. data/lib/doing/note.rb +31 -30
  93. data/lib/doing/phrase_parser.rb +124 -0
  94. data/lib/doing/plugin_manager.rb +84 -21
  95. data/lib/doing/plugins/export/dayone_export.rb +209 -0
  96. data/lib/doing/plugins/export/html_export.rb +2 -2
  97. data/lib/doing/plugins/export/json_export.rb +1 -0
  98. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  99. data/lib/doing/plugins/export/template_export.rb +94 -86
  100. data/lib/doing/prompt.rb +26 -15
  101. data/lib/doing/string.rb +114 -29
  102. data/lib/doing/string_chronify.rb +5 -1
  103. data/lib/doing/symbol.rb +4 -0
  104. data/lib/doing/template_string.rb +197 -0
  105. data/lib/doing/time.rb +32 -0
  106. data/lib/doing/util.rb +6 -7
  107. data/lib/doing/util_backup.rb +287 -0
  108. data/lib/doing/version.rb +1 -1
  109. data/lib/doing/wwid.rb +105 -41
  110. data/lib/doing.rb +9 -0
  111. data/lib/examples/plugins/say_export.rb +1 -1
  112. data/lib/examples/plugins/wiki_export/wiki_export.rb +3 -3
  113. data/lib/templates/doing-dayone-entry.erb +6 -0
  114. data/lib/templates/doing-dayone.erb +5 -0
  115. metadata +136 -51
  116. data/doc/class_list.html +0 -51
data/bin/doing CHANGED
@@ -25,7 +25,7 @@ version Doing::VERSION
25
25
  hide_commands_without_desc true
26
26
  autocomplete_commands true
27
27
 
28
- REGEX_BOOL = /^(?:and|all|any|or|not|none)$/i
28
+ REGEX_BOOL = /^(?:and|all|any|or|not|none|p(?:at(?:tern)?)?)$/i
29
29
  REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i
30
30
 
31
31
  InvalidExportType = Class.new(RuntimeError)
@@ -51,11 +51,16 @@ if ENV['DOING_LOG_LEVEL'] || ENV['DOING_DEBUG'] || ENV['DOING_QUIET'] || ENV['DO
51
51
  end
52
52
  end
53
53
 
54
+ Doing.logger.benchmark(:total, :start)
55
+
54
56
  if ENV['DOING_CONFIG']
55
57
  Doing.config_with(ENV['DOING_CONFIG'], { ignore_local: true })
56
58
  end
57
59
 
60
+ Doing.logger.benchmark(:configure, :start)
58
61
  config = Doing.config
62
+ Doing.logger.benchmark(:configure, :finish)
63
+
59
64
  settings = config.settings
60
65
  wwid.config = settings
61
66
 
@@ -195,7 +200,16 @@ command %i[now next] do |c|
195
200
  end
196
201
 
197
202
  desc 'Reset the start time of an entry'
203
+ long_desc 'Update the start time of the last entry or the last entry matching a tag/search filter.
204
+ If no argument is provided, the start time will be reset to the current time.
205
+ If a date string is provided as an argument, the start time will be set to the parsed result.'
206
+ arg_name 'DATE_STRING'
198
207
  command %i[reset begin] do |c|
208
+ c.example 'doing reset', desc: 'Reset the start time of the last entry to the current time'
209
+ c.example 'doing reset --tag project1', desc: 'Reset the start time of the most recent entry tagged @project1 to the current time'
210
+ c.example 'doing reset 3pm', desc: 'Reset the start time of the last entry to 3pm of the current day'
211
+ c.example 'doing begin --tag todo --resume', desc: 'alias for reset. Updates the last @todo entry to the current time, removing @done tag.'
212
+
199
213
  c.desc 'Limit search to section'
200
214
  c.arg_name 'NAME'
201
215
  c.flag %i[s section], default_value: 'All'
@@ -203,7 +217,7 @@ command %i[reset begin] do |c|
203
217
  c.desc 'Resume entry (remove @done)'
204
218
  c.switch %i[r resume], default_value: true
205
219
 
206
- c.desc 'Reset last entry matching tag'
220
+ c.desc 'Reset last entry matching tag. Wildcards allowed (*, ?).'
207
221
  c.arg_name 'TAG'
208
222
  c.flag [:tag]
209
223
 
@@ -215,23 +229,30 @@ command %i[reset begin] do |c|
215
229
  # c.switch [:fuzzy], default_value: false, negatable: false
216
230
 
217
231
  c.desc 'Force exact search string matching (case sensitive)'
218
- c.switch %i[x exact], default_value: false, negatable: false
232
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
219
233
 
220
234
  c.desc 'Reset items that *don\'t* match search/tag filters'
221
235
  c.switch [:not], default_value: false, negatable: false
222
236
 
223
237
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
224
238
  c.arg_name 'TYPE'
225
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
239
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
226
240
 
227
241
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
228
242
  c.arg_name 'BOOLEAN'
229
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
243
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
230
244
 
231
245
  c.desc 'Select from a menu of matching entries'
232
246
  c.switch %i[i interactive], negatable: false, default_value: false
233
247
 
234
248
  c.action do |global_options, options, args|
249
+ if args.count > 0
250
+ reset_date = args.join(' ').chronify(guess: :begin)
251
+ raise InvalidArgument, 'Invalid date string' unless reset_date
252
+ else
253
+ reset_date = Time.now
254
+ end
255
+
235
256
  options[:fuzzy] = false
236
257
  if options[:section]
237
258
  options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
@@ -259,7 +280,7 @@ command %i[reset begin] do |c|
259
280
  sort: false,
260
281
  show_if_single: true)
261
282
  else
262
- last_entry = items.last
283
+ last_entry = items.reverse.last
263
284
  end
264
285
 
265
286
  unless last_entry
@@ -267,7 +288,7 @@ command %i[reset begin] do |c|
267
288
  return
268
289
  end
269
290
 
270
- wwid.reset_item(last_entry, resume: options[:resume])
291
+ wwid.reset_item(last_entry, date: reset_date, resume: options[:resume])
271
292
 
272
293
  # new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
273
294
 
@@ -301,7 +322,7 @@ command :note do |c|
301
322
  c.desc "Replace/Remove last entry's note (default append)"
302
323
  c.switch %i[r remove], negatable: false, default_value: false
303
324
 
304
- c.desc 'Add/remove note from last entry matching tag'
325
+ c.desc 'Add/remove note from last entry matching tag. Wildcards allowed (*, ?).'
305
326
  c.arg_name 'TAG'
306
327
  c.flag [:tag]
307
328
 
@@ -313,18 +334,18 @@ command :note do |c|
313
334
  # c.switch [:fuzzy], default_value: false, negatable: false
314
335
 
315
336
  c.desc 'Force exact search string matching (case sensitive)'
316
- c.switch %i[x exact], default_value: false, negatable: false
337
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
317
338
 
318
339
  c.desc 'Add note to item that *doesn\'t* match search/tag filters'
319
340
  c.switch [:not], default_value: false, negatable: false
320
341
 
321
342
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
322
343
  c.arg_name 'TYPE'
323
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
344
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
324
345
 
325
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
346
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
326
347
  c.arg_name 'BOOLEAN'
327
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
348
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
328
349
 
329
350
  c.desc 'Select item for new note from a menu of matching entries'
330
351
  c.switch %i[i interactive], negatable: false, default_value: false
@@ -394,6 +415,10 @@ command :note do |c|
394
415
  end
395
416
 
396
417
  desc 'Finish any running @meanwhile tasks and optionally create a new one'
418
+ long_desc 'The @meanwhile tag allows you to have long-running entries that encompass smaller entries.
419
+ This command makes it easy to start and stop these overarching entries. Just run `doing meanwhile Starting work on this
420
+ big project` to start a @meanwhile entry, add other entries as you work on the project, then use `doing meanwhile` by
421
+ itself to mark the entry as @done.'
397
422
  arg_name 'ENTRY'
398
423
  command :meanwhile do |c|
399
424
  c.example 'doing meanwhile "Long task that will have others after it before it\'s done"', desc: 'Add a new long-running entry, completing any current @meanwhile entry'
@@ -483,6 +508,13 @@ command :template do |c|
483
508
  c.desc 'List in single column for completion'
484
509
  c.switch %i[c column]
485
510
 
511
+ c.desc 'Save template to file instead of STDOUT'
512
+ c.switch %i[s save], default_value: false, negatable: false
513
+
514
+ c.desc 'Save template to alternate location'
515
+ c.arg_name 'DIRECTORY'
516
+ c.flag %i[p path], default_value: File.join(Doing::Util.user_home, '.config', 'doing', 'templates')
517
+
486
518
  c.action do |_global_options, options, args|
487
519
  if options[:list] || options[:column]
488
520
  if options[:column]
@@ -495,13 +527,19 @@ command :template do |c|
495
527
 
496
528
  if args.empty?
497
529
  type = Doing::Prompt.choose_from(Doing::Plugins.plugin_templates, sorted: false, prompt: 'Select template type > ')
530
+ type.sub!(/ \(.*?\)$/, '').strip!
531
+ options[:save] = Doing::Prompt.yn("Save to #{options[:path]}? (No outputs to STDOUT)", default_response: false)
498
532
  else
499
533
  type = args[0]
500
534
  end
501
535
 
502
536
  raise InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type
503
537
 
504
- $stdout.puts Doing::Plugins.template_for_trigger(type)
538
+ if options[:save]
539
+ Doing::Plugins.template_for_trigger(type, save_to: options[:path])
540
+ else
541
+ $stdout.puts Doing::Plugins.template_for_trigger(type, save_to: nil)
542
+ end
505
543
 
506
544
  # case args[0]
507
545
  # when /html|haml/i
@@ -521,8 +559,19 @@ long_desc 'List all entries and select with typeahead fuzzy matching.
521
559
 
522
560
  Multiple selections are allowed, hit tab to add the highlighted entry to the
523
561
  selection, and use ctrl-a to select all visible items. Return processes the
524
- selected entries.'
562
+ selected entries.
563
+
564
+ Search in the menu by typing:
565
+
566
+ sbtrkt fuzzy-match Items that match sbtrkt
567
+
568
+ \'wild exact-match (quoted) Items that include wild
569
+
570
+ !fire inverse-exact-match Items that do not include fire'
525
571
  command :select do |c|
572
+ c.example 'doing select', desc: 'Select from all entries. A menu of available actions will be presented after confirming the selection.'
573
+ c.example 'doing select --editor', desc: 'Select entries from a menu and batch edit them in your default editor'
574
+ c.example 'doing select --after "yesterday 12pm" --tag project1', desc: 'Display a menu of entries created after noon yesterday, add @project1 to selected entries'
526
575
  c.desc 'Select from a specific section'
527
576
  c.arg_name 'SECTION'
528
577
  c.flag %i[s section]
@@ -568,14 +617,14 @@ command :select do |c|
568
617
  c.flag [:from]
569
618
 
570
619
  c.desc 'Force exact search string matching (case sensitive)'
571
- c.switch %i[x exact], default_value: false, negatable: false
620
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
572
621
 
573
622
  c.desc 'Select items that *don\'t* match search/tag filters'
574
623
  c.switch [:not], default_value: false, negatable: false
575
624
 
576
625
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
577
626
  c.arg_name 'TYPE'
578
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
627
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
579
628
 
580
629
  c.desc 'Use --no-menu to skip the interactive menu. Use with --query to filter items and act on results automatically. Test with `--output doing` to preview matches.'
581
630
  c.switch %i[menu], negatable: true, default_value: true
@@ -680,6 +729,9 @@ command :later do |c|
680
729
  end
681
730
 
682
731
  desc 'Add a completed item with @done(date). No argument finishes last entry.'
732
+ long_desc 'Use this command to add an entry after you\'ve already finished it. It will be immediately marked as @done.
733
+ You can modify the start and end times of the entry using the --back, --took, and --at flags, making it an easy
734
+ way to add entries in post and maintain accurate (albeit manual) time tracking.'
683
735
  arg_name 'ENTRY'
684
736
  command %i[done did] do |c|
685
737
  c.example 'doing done', desc: 'Tag the last entry @done'
@@ -890,13 +942,13 @@ command :cancel do |c|
890
942
  c.arg_name 'NAME'
891
943
  c.flag %i[s section]
892
944
 
893
- c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2)'
945
+ c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2). Wildcards allowed (*, ?).'
894
946
  c.arg_name 'TAG'
895
947
  c.flag [:tag]
896
948
 
897
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
949
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
898
950
  c.arg_name 'BOOLEAN'
899
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
951
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
900
952
 
901
953
  c.desc 'Cancel the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
902
954
  c.arg_name 'QUERY'
@@ -906,14 +958,14 @@ command :cancel do |c|
906
958
  # c.switch [:fuzzy], default_value: false, negatable: false
907
959
 
908
960
  c.desc 'Force exact search string matching (case sensitive)'
909
- c.switch %i[x exact], default_value: false, negatable: false
961
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
910
962
 
911
963
  c.desc 'Finish items that *don\'t* match search/tag filters'
912
964
  c.switch [:not], default_value: false, negatable: false
913
965
 
914
966
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
915
967
  c.arg_name 'TYPE'
916
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
968
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
917
969
 
918
970
  c.desc 'Cancel last entry (or entries) not already marked @done'
919
971
  c.switch %i[u unfinished], negatable: false, default_value: false
@@ -997,7 +1049,7 @@ command :finish do |c|
997
1049
  c.flag [:at]
998
1050
 
999
1051
  c.desc 'Finish the last X entries containing TAG.
1000
- Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
1052
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
1001
1053
  c.arg_name 'TAG'
1002
1054
  c.flag [:tag]
1003
1055
 
@@ -1009,18 +1061,18 @@ command :finish do |c|
1009
1061
  # c.switch [:fuzzy], default_value: false, negatable: false
1010
1062
 
1011
1063
  c.desc 'Force exact search string matching (case sensitive)'
1012
- c.switch %i[x exact], default_value: false, negatable: false
1064
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1013
1065
 
1014
1066
  c.desc 'Finish items that *don\'t* match search/tag filters'
1015
1067
  c.switch [:not], default_value: false, negatable: false
1016
1068
 
1017
1069
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1018
1070
  c.arg_name 'TYPE'
1019
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1071
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1020
1072
 
1021
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1073
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
1022
1074
  c.arg_name 'BOOLEAN'
1023
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1075
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1024
1076
 
1025
1077
  c.desc 'Remove done tag'
1026
1078
  c.switch %i[r remove], negatable: false, default_value: false
@@ -1119,7 +1171,14 @@ command :finish do |c|
1119
1171
  end
1120
1172
 
1121
1173
  desc 'Repeat last entry as new entry'
1174
+ long_desc 'This command is designed to allow multiple time intervals to be created for an entry by duplicating it with a new start (and end, eventually) time.'
1122
1175
  command %i[again resume] do |c|
1176
+ c.example 'doing resume', desc: 'Duplicate the most recent entry with a new start time, removing any @done tag'
1177
+ c.example 'doing again', desc: 'again is an alias for resume'
1178
+ c.example 'doing resume --editor', desc: 'Repeat the last entry, opening the new entry in the default editor'
1179
+ c.example 'doing resume --tag project1 --in Projects', desc: 'Repeat the last entry tagged @project1, creating the new entry in the Projects section'
1180
+ c.example 'doing resume --interactive', desc: 'Select the entry to repeat from a menu'
1181
+
1123
1182
  c.desc 'Get last entry from a specific section'
1124
1183
  c.arg_name 'NAME'
1125
1184
  c.flag %i[s section], default_value: 'All'
@@ -1128,7 +1187,7 @@ command %i[again resume] do |c|
1128
1187
  c.arg_name 'SECTION_NAME'
1129
1188
  c.flag [:in]
1130
1189
 
1131
- c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma.'
1190
+ c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?).'
1132
1191
  c.arg_name 'TAG'
1133
1192
  c.flag [:tag]
1134
1193
 
@@ -1141,18 +1200,18 @@ command %i[again resume] do |c|
1141
1200
  # c.switch [:fuzzy], default_value: false, negatable: false
1142
1201
 
1143
1202
  c.desc 'Force exact search string matching (case sensitive)'
1144
- c.switch %i[x exact], default_value: false, negatable: false
1203
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1145
1204
 
1146
1205
  c.desc 'Resume items that *don\'t* match search/tag filters'
1147
1206
  c.switch [:not], default_value: false, negatable: false
1148
1207
 
1149
1208
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1150
1209
  c.arg_name 'TYPE'
1151
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1210
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1152
1211
 
1153
- c.desc 'Boolean used to combine multiple tags'
1212
+ c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans.'
1154
1213
  c.arg_name 'BOOLEAN'
1155
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1214
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1156
1215
 
1157
1216
  c.desc "Edit duplicated entry with #{Doing::Util.default_editor} before adding"
1158
1217
  c.switch %i[e editor], negatable: false, default_value: false
@@ -1186,6 +1245,77 @@ command %i[again resume] do |c|
1186
1245
  end
1187
1246
  end
1188
1247
 
1248
+ desc 'List all tags in the current Doing file'
1249
+ command :tags do |c|
1250
+ c.desc 'Section'
1251
+ c.arg_name 'SECTION_NAME'
1252
+ c.flag %i[s section], default_value: 'All'
1253
+
1254
+ c.desc 'Show count of occurrences'
1255
+ c.switch %i[c counts]
1256
+
1257
+ c.desc 'Sort by name or count'
1258
+ c.arg_name 'SORT_ORDER'
1259
+ c.flag %i[sort], default_value: 'name', must_match: /^(?:n(?:ame)?|c(?:ount)?)$/
1260
+
1261
+ c.desc 'Sort order (asc/desc)'
1262
+ c.arg_name 'ORDER'
1263
+ c.flag %i[o order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
1264
+
1265
+ c.desc 'Get tags for entries matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?).'
1266
+ c.arg_name 'TAG'
1267
+ c.flag [:tag]
1268
+
1269
+ c.desc 'Get tags for items matching search. Surround with
1270
+ slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
1271
+ c.arg_name 'QUERY'
1272
+ c.flag [:search]
1273
+
1274
+ # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
1275
+ # c.switch [:fuzzy], default_value: false, negatable: false
1276
+
1277
+ c.desc 'Force exact search string matching (case sensitive)'
1278
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1279
+
1280
+ c.desc 'Get tags from items that *don\'t* match search/tag filters'
1281
+ c.switch [:not], default_value: false, negatable: false
1282
+
1283
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1284
+ c.arg_name 'TYPE'
1285
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1286
+
1287
+ c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans.'
1288
+ c.arg_name 'BOOLEAN'
1289
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1290
+
1291
+ c.desc 'Select items to scan from a menu of matching entries'
1292
+ c.switch %i[i interactive], negatable: false, default_value: false
1293
+
1294
+ c.action do |_global, options, args|
1295
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
1296
+
1297
+ items = wwid.filter_items([], opt: options)
1298
+
1299
+ # items = wwid.content.in_section(section)
1300
+ tags = wwid.all_tags(items, counts: true)
1301
+
1302
+ if options[:sort] =~ /^n/i
1303
+ tags = tags.sort_by { |tag, count| tag }
1304
+ else
1305
+ tags = tags.sort_by { |tag, count| count }
1306
+ end
1307
+
1308
+ tags.reverse! if options[:order].normalize_order == 'desc'
1309
+
1310
+ if options[:counts]
1311
+ tags.each { |t, c| puts "#{t} (#{c})" }
1312
+ else
1313
+ tags.each { |t, c| puts "#{t}" }
1314
+ end
1315
+ end
1316
+ end
1317
+
1318
+
1189
1319
  desc 'Add tag(s) to last entry'
1190
1320
  long_desc 'Add (or remove) tags from the last entry, or from multiple entries
1191
1321
  (with `--count`), entries matching a search (with `--search`), or entries
@@ -1239,7 +1369,7 @@ command :tag do |c|
1239
1369
  c.switch %i[a autotag], negatable: false, default_value: false
1240
1370
 
1241
1371
  c.desc 'Tag the last X entries containing TAG.
1242
- Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
1372
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
1243
1373
  c.arg_name 'TAG'
1244
1374
  c.flag [:tag]
1245
1375
 
@@ -1251,18 +1381,18 @@ command :tag do |c|
1251
1381
  # c.switch [:fuzzy], default_value: false, negatable: false
1252
1382
 
1253
1383
  c.desc 'Force exact search string matching (case sensitive)'
1254
- c.switch %i[x exact], default_value: false, negatable: false
1384
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1255
1385
 
1256
1386
  c.desc 'Tag items that *don\'t* match search/tag filters'
1257
1387
  c.switch [:not], default_value: false, negatable: false
1258
1388
 
1259
1389
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1260
1390
  c.arg_name 'TYPE'
1261
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1391
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1262
1392
 
1263
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1393
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
1264
1394
  c.arg_name 'BOOLEAN'
1265
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1395
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1266
1396
 
1267
1397
  c.desc 'Select item(s) to tag from a menu of matching entries'
1268
1398
  c.switch %i[i interactive], negatable: false, default_value: false
@@ -1314,44 +1444,49 @@ command :tag do |c|
1314
1444
  options[:search] = search
1315
1445
  end
1316
1446
 
1447
+ options[:count] = count
1448
+ options[:section] = section
1449
+ options[:tag] = search_tags
1450
+ options[:tags] = tags
1451
+ options[:tag_bool] = options[:bool].normalize_bool
1452
+
1317
1453
  if count.zero? && !options[:force]
1318
- if options[:search]
1319
- section_q = ' matching your search terms'
1320
- elsif options[:tag]
1321
- section_q = ' matching your tag search'
1322
- elsif section == 'All'
1323
- section_q = ''
1324
- else
1325
- section_q = " in section #{section}"
1326
- end
1454
+ matches = wwid.filter_items([], opt: options).count
1455
+
1456
+ if matches > 5
1457
+ if options[:search]
1458
+ section_q = ' matching your search terms'
1459
+ elsif options[:tag]
1460
+ section_q = ' matching your tag search'
1461
+ elsif section == 'All'
1462
+ section_q = ''
1463
+ else
1464
+ section_q = " in section #{section}"
1465
+ end
1327
1466
 
1328
1467
 
1329
- question = if options[:aarchive]
1330
- "Are you sure you want to autotag all records#{section_q}"
1331
- elsif options[:remove]
1332
- "Are you sure you want to remove #{tags.join(' and ')} from all records#{section_q}"
1333
- else
1334
- "Are you sure you want to add #{tags.join(' and ')} to all records#{section_q}"
1335
- end
1468
+ question = if options[:autotag]
1469
+ "Are you sure you want to autotag #{matches} records#{section_q}"
1470
+ elsif options[:remove]
1471
+ "Are you sure you want to remove #{tags.join(' and ')} from #{matches} records#{section_q}"
1472
+ else
1473
+ "Are you sure you want to add #{tags.join(' and ')} to #{matches} records#{section_q}"
1474
+ end
1336
1475
 
1337
- res = Doing::Prompt.yn(question, default_response: false)
1476
+ res = Doing::Prompt.yn(question, default_response: false)
1338
1477
 
1339
- raise UserCancelled unless res
1478
+ raise UserCancelled unless res
1479
+ end
1340
1480
  end
1341
1481
 
1342
- options[:count] = count
1343
- options[:section] = section
1344
- options[:tag] = search_tags
1345
- options[:tags] = tags
1346
- options[:tag_bool] = options[:bool].normalize_bool
1347
-
1348
1482
  wwid.tag_last(options)
1349
1483
  end
1350
1484
  end
1351
1485
 
1352
1486
  desc 'Mark last entry as flagged'
1353
- command [:mark, :flag] do |c|
1487
+ command %i[mark flag] do |c|
1354
1488
  c.example 'doing flag', desc: 'Add @flagged to the last entry created'
1489
+ c.example 'doing mark', desc: 'mark is an alias for flag'
1355
1490
  c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
1356
1491
  c.example 'doing flag --interactive --search "/(develop|cod)ing/"', desc: 'Find entries matching regular expression and create a menu allowing multiple selections, selected items will be @flagged'
1357
1492
 
@@ -1376,7 +1511,7 @@ command [:mark, :flag] do |c|
1376
1511
  c.switch %i[u unfinished], negatable: false, default_value: false
1377
1512
 
1378
1513
  c.desc 'Flag the last entry containing TAG.
1379
- Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
1514
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
1380
1515
  c.arg_name 'TAG'
1381
1516
  c.flag [:tag]
1382
1517
 
@@ -1388,18 +1523,18 @@ command [:mark, :flag] do |c|
1388
1523
  # c.switch [:fuzzy], default_value: false, negatable: false
1389
1524
 
1390
1525
  c.desc 'Force exact search string matching (case sensitive)'
1391
- c.switch %i[x exact], default_value: false, negatable: false
1526
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1392
1527
 
1393
1528
  c.desc 'Flag items that *don\'t* match search/tag/date filters'
1394
1529
  c.switch [:not], default_value: false, negatable: false
1395
1530
 
1396
1531
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1397
1532
  c.arg_name 'TYPE'
1398
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1533
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1399
1534
 
1400
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1535
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
1401
1536
  c.arg_name 'BOOLEAN'
1402
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1537
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1403
1538
 
1404
1539
  c.desc 'Select item(s) to flag from a menu of matching entries'
1405
1540
  c.switch %i[i interactive], negatable: false, default_value: false
@@ -1473,7 +1608,11 @@ end
1473
1608
  desc 'List all entries'
1474
1609
  long_desc %(
1475
1610
  The argument can be a section name, @tag(s) or both.
1476
- "pick" or "choose" as an argument will offer a section menu.
1611
+ "pick" or "choose" as an argument will offer a section menu. Run with `--menu` to get a menu of available tags.
1612
+
1613
+ Show tags by passing @tagname arguments. Multiple tags can be combined, and you can specify the boolean used to
1614
+ combine them with `--bool (AND|OR|NOT)`. You can also use @+tagname to require a tag to match, or @-tagname to ignore
1615
+ entries containing tagname. +/- operators require `--bool PATTERN` (which is the default).
1477
1616
  )
1478
1617
  arg_name '[SECTION|@TAGS]'
1479
1618
  command :show do |c|
@@ -1485,17 +1624,17 @@ command :show do |c|
1485
1624
  c.example 'doing show Ideas @doing --from "mon to fri"', desc: 'Show entries tagged @doing from the Ideas section added between monday and friday of the current week.'
1486
1625
  c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
1487
1626
 
1488
- c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
1627
+ c.desc 'Tag filter, combine multiple tags with a comma. Use `--tag pick` for a menu of available tags. Wildcards allowed (*, ?). Added for compatibility with other commands.'
1489
1628
  c.arg_name 'TAG'
1490
1629
  c.flag [:tag]
1491
1630
 
1492
- c.desc 'Tag boolean (AND,OR,NOT)'
1631
+ c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
1493
1632
  c.arg_name 'BOOLEAN'
1494
- c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
1633
+ c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1495
1634
 
1496
1635
  c.desc 'Max count to show'
1497
1636
  c.arg_name 'MAX'
1498
- c.flag %i[c count], default_value: 0
1637
+ c.flag %i[c count], default_value: 0, must_match: /^\d+$/, type: Integer
1499
1638
 
1500
1639
  c.desc 'Age (oldest|newest)'
1501
1640
  c.arg_name 'AGE'
@@ -1529,14 +1668,14 @@ command :show do |c|
1529
1668
  # c.switch [:fuzzy], default_value: false, negatable: false
1530
1669
 
1531
1670
  c.desc 'Force exact search string matching (case sensitive)'
1532
- c.switch %i[x exact], default_value: false, negatable: false
1671
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1533
1672
 
1534
1673
  c.desc 'Show items that *don\'t* match search/tag/date filters'
1535
1674
  c.switch [:not], default_value: false, negatable: false
1536
1675
 
1537
1676
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1538
1677
  c.arg_name 'TYPE'
1539
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1678
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1540
1679
 
1541
1680
  c.desc 'Sort order (asc/desc)'
1542
1681
  c.arg_name 'ORDER'
@@ -1564,6 +1703,9 @@ command :show do |c|
1564
1703
  c.desc 'Only show items with recorded time intervals'
1565
1704
  c.switch [:only_timed], default_value: false, negatable: false
1566
1705
 
1706
+ c.desc 'Select section or tag to display from a menu'
1707
+ c.switch %i[m menu], negatable: false, default_value: false
1708
+
1567
1709
  c.desc 'Select from a menu of matching entries to perform additional operations'
1568
1710
  c.switch %i[i interactive], negatable: false, default_value: false
1569
1711
 
@@ -1576,15 +1718,17 @@ command :show do |c|
1576
1718
 
1577
1719
  tag_filter = false
1578
1720
  tags = []
1721
+
1579
1722
  if args.length.positive?
1580
1723
  case args[0]
1581
1724
  when /^all$/i
1582
1725
  section = 'All'
1583
1726
  args.shift
1584
1727
  when /^(choose|pick)$/i
1585
- section = wwid.choose_section
1728
+ section = wwid.choose_section(include_all: true)
1729
+
1586
1730
  args.shift
1587
- when /^@/
1731
+ when /^[@+-]/
1588
1732
  section = 'All'
1589
1733
  else
1590
1734
  begin
@@ -1607,18 +1751,12 @@ command :show do |c|
1607
1751
  end
1608
1752
  end
1609
1753
  else
1610
- section = settings['current_section']
1754
+ section = options[:menu] ? wwid.choose_section(include_all: true) : settings['current_section']
1755
+ section ||= 'All'
1611
1756
  end
1612
1757
 
1613
1758
  tags.concat(options[:tag].to_tags) if options[:tag]
1614
1759
 
1615
- unless tags.empty?
1616
- tag_filter = {
1617
- 'tags' => tags,
1618
- 'bool' => options[:bool].normalize_bool
1619
- }
1620
- end
1621
-
1622
1760
  options[:times] = true if options[:totals]
1623
1761
 
1624
1762
  template = settings['templates']['default'].deep_merge({
@@ -1636,27 +1774,55 @@ command :show do |c|
1636
1774
  options[:search] = search
1637
1775
  end
1638
1776
 
1777
+ options[:section] = section
1778
+
1779
+ unless tags.empty?
1780
+ tag_filter = {
1781
+ 'tags' => tags,
1782
+ 'bool' => options[:bool].normalize_bool
1783
+ }
1784
+ end
1785
+
1786
+ options[:tag_filter] = tag_filter
1787
+ options[:tag] = nil
1788
+
1789
+ items = wwid.filter_items([], opt: options)
1790
+
1791
+ if options[:menu]
1792
+ tag = wwid.choose_tag(section, items: items, include_all: true)
1793
+ raise UserCancelled unless tag
1794
+
1795
+ # options[:bool] = :and unless tags.empty?
1796
+
1797
+ tags = tag.split(/ +/).map { |t| t.strip.sub(/^@?/, '') } if tag =~ /^@/
1798
+ unless tags.empty?
1799
+ tag_filter = {
1800
+ 'tags' => tags,
1801
+ 'bool' => options[:bool].normalize_bool
1802
+ }
1803
+ options[:tag_filter] = tag_filter
1804
+ end
1805
+ end
1806
+
1639
1807
  opt = options.dup
1640
1808
  opt[:sort_tags] = options[:tag_sort] =~ /^n/i
1641
1809
  opt[:count] = options[:count].to_i
1642
1810
  opt[:highlight] = true
1643
1811
  opt[:order] = options[:sort].normalize_order
1644
- opt[:section] = section
1645
1812
  opt[:tag] = nil
1646
- opt[:tag_filter] = tag_filter
1647
1813
  opt[:tag_order] = options[:tag_order].normalize_order
1648
1814
  opt[:tags_color] = template['tags_color']
1649
1815
 
1650
- Doing::Pager.page wwid.list_section(opt)
1816
+ Doing::Pager.page wwid.list_section(opt, items: items)
1651
1817
  end
1652
1818
  end
1653
1819
 
1654
1820
  desc 'Search for entries'
1655
- long_desc <<~'EODESC'
1656
- Search all sections (or limit to a single section) for entries matching text or regular expression. Normal strings are fuzzy matched.
1821
+ long_desc %(
1822
+ Search all sections (or limit to a single section) for entries matching text or regular expression. Normal strings are fuzzy matched.
1657
1823
 
1658
- To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
1659
- EODESC
1824
+ To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
1825
+ )
1660
1826
 
1661
1827
  arg_name 'SEARCH_PATTERN'
1662
1828
  command %i[grep search] do |c|
@@ -1714,14 +1880,14 @@ command %i[grep search] do |c|
1714
1880
  # c.switch [:fuzzy], default_value: false, negatable: false
1715
1881
 
1716
1882
  c.desc 'Force exact string matching (case sensitive)'
1717
- c.switch %i[x exact], default_value: false, negatable: false
1883
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1718
1884
 
1719
1885
  c.desc 'Show items that *don\'t* match search string'
1720
1886
  c.switch [:not], default_value: false, negatable: false
1721
1887
 
1722
1888
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1723
1889
  c.arg_name 'TYPE'
1724
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1890
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1725
1891
 
1726
1892
  c.desc 'Display an interactive menu of results to perform further operations'
1727
1893
  c.switch %i[i interactive], default_value: false, negatable: false
@@ -1820,6 +1986,8 @@ command :recent do |c|
1820
1986
  end
1821
1987
 
1822
1988
  desc 'List entries from today'
1989
+ long_desc 'List entries from the current day. Use --before, --after, and
1990
+ --from to specify time ranges.'
1823
1991
  command :today do |c|
1824
1992
  c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day'
1825
1993
  c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
@@ -1994,6 +2162,8 @@ command :since do |c|
1994
2162
  end
1995
2163
 
1996
2164
  desc 'List entries from yesterday'
2165
+ long_desc 'Show only entries with start times within the previous 24 hour period. Use --before, --after, and --from to limit to
2166
+ time spans within the day.'
1997
2167
  command :yesterday do |c|
1998
2168
  c.example 'doing yesterday', desc: 'List all entries from the previous day'
1999
2169
  c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm'
@@ -2065,6 +2235,8 @@ command :yesterday do |c|
2065
2235
  end
2066
2236
 
2067
2237
  desc 'Show the last entry, optionally edit'
2238
+ long_desc 'Shows the last entry. Using --search and --tag filters, you can view/edit the last entry matching a filter,
2239
+ allowing `doing last` to target historical entries.'
2068
2240
  command :last do |c|
2069
2241
  c.example 'doing last', desc: 'Show the most recent entry in all sections'
2070
2242
  c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section'
@@ -2081,13 +2253,13 @@ command :last do |c|
2081
2253
  c.desc "Edit entry with #{Doing::Util.default_editor}"
2082
2254
  c.switch %i[e editor], negatable: false, default_value: false
2083
2255
 
2084
- c.desc 'Tag filter, combine multiple tags with a comma.'
2256
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
2085
2257
  c.arg_name 'TAG'
2086
2258
  c.flag [:tag]
2087
2259
 
2088
- c.desc 'Tag boolean'
2260
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
2089
2261
  c.arg_name 'BOOLEAN'
2090
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
2262
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2091
2263
 
2092
2264
  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2093
2265
  c.arg_name 'QUERY'
@@ -2100,14 +2272,14 @@ command :last do |c|
2100
2272
  # c.switch [:fuzzy], default_value: false, negatable: false
2101
2273
 
2102
2274
  c.desc 'Force exact search string matching (case sensitive)'
2103
- c.switch %i[x exact], default_value: false, negatable: false
2275
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
2104
2276
 
2105
2277
  c.desc 'Show items that *don\'t* match search string or tag filter'
2106
2278
  c.switch [:not], default_value: false, negatable: false
2107
2279
 
2108
2280
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
2109
2281
  c.arg_name 'TYPE'
2110
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2282
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2111
2283
 
2112
2284
  c.action do |global_options, options, _args|
2113
2285
  options[:fuzzy] = false
@@ -2117,15 +2289,7 @@ command :last do |c|
2117
2289
  tags = []
2118
2290
  else
2119
2291
  tags = options[:tag].to_tags
2120
- options[:bool] = case options[:bool]
2121
- when /(any|or)/i
2122
- :or
2123
- when /(not|none)/i
2124
- :not
2125
- else
2126
- :and
2127
- end
2128
-
2292
+ options[:bool] = options[:bool].normalize_bool
2129
2293
  end
2130
2294
 
2131
2295
  options[:case] = options[:case].normalize_case
@@ -2230,6 +2394,8 @@ command :plugins do |c|
2230
2394
  end
2231
2395
 
2232
2396
  desc 'Generate shell completion scripts'
2397
+ long_desc 'Generates the necessary scripts to add command line completion to various shells, so typing \'doing\' and hitting
2398
+ tab will offer completions of subcommands and their options.'
2233
2399
  command :completion do |c|
2234
2400
  c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
2235
2401
  c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
@@ -2252,7 +2418,8 @@ command :completion do |c|
2252
2418
  end
2253
2419
 
2254
2420
  desc 'Display a user-created view'
2255
- long_desc 'Command line options override view configuration'
2421
+ long_desc 'Views are defined in your configuration (use `doing config` to edit).
2422
+ Command line options override view configuration.'
2256
2423
  arg_name 'VIEW_NAME'
2257
2424
  command :view do |c|
2258
2425
  c.example 'doing view color', desc: 'Display entries according to config for view "color"'
@@ -2282,13 +2449,13 @@ command :view do |c|
2282
2449
  c.desc 'Include colors in output'
2283
2450
  c.switch [:color], default_value: true, negatable: true
2284
2451
 
2285
- c.desc 'Tag filter, combine multiple tags with a comma.'
2452
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
2286
2453
  c.arg_name 'TAG'
2287
2454
  c.flag [:tag]
2288
2455
 
2289
- c.desc 'Tag boolean (AND,OR,NOT)'
2456
+ c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
2290
2457
  c.arg_name 'BOOLEAN'
2291
- c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
2458
+ c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2292
2459
 
2293
2460
  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2294
2461
  c.arg_name 'QUERY'
@@ -2298,14 +2465,14 @@ command :view do |c|
2298
2465
  # c.switch [:fuzzy], default_value: false, negatable: false
2299
2466
 
2300
2467
  c.desc 'Force exact search string matching (case sensitive)'
2301
- c.switch %i[x exact], default_value: false, negatable: false
2468
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
2302
2469
 
2303
2470
  c.desc 'Show items that *don\'t* match search string'
2304
2471
  c.switch [:not], default_value: false, negatable: false
2305
2472
 
2306
2473
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
2307
2474
  c.arg_name 'TYPE'
2308
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2475
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2309
2476
 
2310
2477
  c.desc 'Sort tags by (name|time)'
2311
2478
  c.arg_name 'KEY'
@@ -2367,6 +2534,7 @@ command :view do |c|
2367
2534
  end
2368
2535
 
2369
2536
  view = wwid.get_view(title)
2537
+
2370
2538
  if view
2371
2539
  page_title = view.key?('title') ? view['title'] : title.cap_first
2372
2540
  only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed]
@@ -2382,16 +2550,22 @@ command :view do |c|
2382
2550
  tag_filter = false
2383
2551
  if options[:tag]
2384
2552
  tag_filter = { 'tags' => [], 'bool' => 'OR' }
2385
- tag_filter['tags'] = options[:tag].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2386
- tag_filter['bool'] = options[:bool].normalize_bool
2553
+ bool = options[:bool].normalize_bool
2554
+ tag_filter['bool'] = bool
2555
+ tag_filter['tags'] = if bool == :pattern
2556
+ options[:tag]
2557
+ else
2558
+ options[:tag].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2559
+ end
2387
2560
  elsif view.key?('tags') && !(view['tags'].nil? || view['tags'].empty?)
2388
2561
  tag_filter = { 'tags' => [], 'bool' => 'OR' }
2562
+ bool = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :pattern
2563
+ tag_filter['bool'] = bool
2389
2564
  tag_filter['tags'] = if view['tags'].instance_of?(Array)
2390
- view['tags'].map(&:strip)
2565
+ bool == :pattern ? view['tags'].join(' ').strip : view['tags'].map(&:strip)
2391
2566
  else
2392
- view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2567
+ bool == :pattern ? view['tags'].strip : view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2393
2568
  end
2394
- tag_filter['bool'] = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :or
2395
2569
  end
2396
2570
 
2397
2571
  # If the -o/--output flag was specified, override any default in the view template
@@ -2440,6 +2614,7 @@ command :view do |c|
2440
2614
  end
2441
2615
 
2442
2616
  opts = options.dup
2617
+ opts[:view_template] = title
2443
2618
  opts[:count] = count
2444
2619
  opts[:format] = date_format
2445
2620
  opts[:highlight] = options[:color]
@@ -2500,13 +2675,13 @@ command %i[archive move] do |c|
2500
2675
  c.desc 'Label moved items with @from(SECTION_NAME)'
2501
2676
  c.switch [:label], default_value: true, negatable: true
2502
2677
 
2503
- c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
2678
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands.'
2504
2679
  c.arg_name 'TAG'
2505
2680
  c.flag [:tag]
2506
2681
 
2507
- c.desc 'Tag boolean (AND|OR|NOT)'
2682
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
2508
2683
  c.arg_name 'BOOLEAN'
2509
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
2684
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2510
2685
 
2511
2686
  c.desc 'Search filter'
2512
2687
  c.arg_name 'QUERY'
@@ -2516,14 +2691,14 @@ command %i[archive move] do |c|
2516
2691
  # c.switch [:fuzzy], default_value: false, negatable: false
2517
2692
 
2518
2693
  c.desc 'Force exact search string matching (case sensitive)'
2519
- c.switch %i[x exact], default_value: false, negatable: false
2694
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
2520
2695
 
2521
2696
  c.desc 'Show items that *don\'t* match search string'
2522
2697
  c.switch [:not], default_value: false, negatable: false
2523
2698
 
2524
2699
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
2525
2700
  c.arg_name 'TYPE'
2526
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2701
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2527
2702
 
2528
2703
  c.desc 'Archive entries older than date
2529
2704
  (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
@@ -2569,6 +2744,9 @@ command %i[archive move] do |c|
2569
2744
  end
2570
2745
 
2571
2746
  desc 'Move entries to archive file'
2747
+ long_desc 'As your doing file grows, commands can get slow. Given that your historical data (and your archive section)
2748
+ probably aren\'t providing any useful insights a year later, use this command to "rotate" old entries out to an archive
2749
+ file. You\'ll still have access to all historical data, but it won\'t be slowing down daily operation.'
2572
2750
  command :rotate do |c|
2573
2751
  c.example 'doing rotate', desc: 'Move all entries in doing file to a dated secondary file'
2574
2752
  c.example 'doing rotate --section Archive --keep 10', desc: 'Move entries in the Archive section to a secondary file, keeping the most recent 10 entries'
@@ -2582,13 +2760,13 @@ command :rotate do |c|
2582
2760
  c.arg_name 'SECTION_NAME'
2583
2761
  c.flag %i[s section], default_value: 'All'
2584
2762
 
2585
- c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
2763
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands.'
2586
2764
  c.arg_name 'TAG'
2587
2765
  c.flag [:tag]
2588
2766
 
2589
- c.desc 'Tag boolean (AND|OR|NOT)'
2767
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
2590
2768
  c.arg_name 'BOOLEAN'
2591
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
2769
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2592
2770
 
2593
2771
  c.desc 'Search filter'
2594
2772
  c.arg_name 'QUERY'
@@ -2598,14 +2776,14 @@ command :rotate do |c|
2598
2776
  # c.switch [:fuzzy], default_value: false, negatable: false
2599
2777
 
2600
2778
  c.desc 'Force exact search string matching (case sensitive)'
2601
- c.switch %i[x exact], default_value: false, negatable: false
2779
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
2602
2780
 
2603
2781
  c.desc 'Rotate items that *don\'t* match search string or tag filter'
2604
2782
  c.switch [:not], default_value: false, negatable: false
2605
2783
 
2606
2784
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
2607
2785
  c.arg_name 'TYPE'
2608
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2786
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2609
2787
 
2610
2788
  c.desc 'Rotate entries older than date
2611
2789
  (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
@@ -2635,8 +2813,10 @@ command :rotate do |c|
2635
2813
  end
2636
2814
 
2637
2815
  desc 'Open the "doing" file in an editor'
2638
- long_desc "`doing open` defaults to using the editor_app setting in #{config.config_file} (#{settings.key?('editor_app') ? settings['editor_app'] : 'not set'})."
2816
+ long_desc "`doing open` defaults to using the editors->doing_file setting
2817
+ in #{config.config_file} (#{Doing::Util.find_default_editor('doing_file')})."
2639
2818
  command :open do |c|
2819
+ c.example 'doing open', desc: 'Open the doing file in the default editor'
2640
2820
  c.desc 'Open with editor command (e.g. vim, mate)'
2641
2821
  c.arg_name 'COMMAND'
2642
2822
  c.flag %i[e editor]
@@ -2810,7 +2990,7 @@ command :config do |c|
2810
2990
  c.command :undo do |undo|
2811
2991
  undo.action do |_global, options, args|
2812
2992
  config_file = config.choose_config
2813
- wwid.restore_backup(config_file)
2993
+ Doing::Util::Backup.restore_last_backup(config_file, count: 1)
2814
2994
  end
2815
2995
  end
2816
2996
 
@@ -2885,6 +3065,7 @@ command :config do |c|
2885
3065
  real_path = config.resolve_key_path(keypath, create: true)
2886
3066
 
2887
3067
  old_value = settings.dig(*real_path) || nil
3068
+ old_type = old_value&.class.to_s || nil
2888
3069
 
2889
3070
  if old_value.is_a?(Hash) && !options[:remove]
2890
3071
  Doing.logger.log_now(:warn, 'Config:', "Config key must point to a single value, #{real_path.join('->').boldwhite} is a mapping")
@@ -2906,13 +3087,11 @@ command :config do |c|
2906
3087
  cfg.deep_set(real_path, nil)
2907
3088
  $stderr.puts "#{'Deleting key:'.yellow} #{real_path.join('->').boldwhite}"
2908
3089
  else
2909
-
2910
- p real_path
2911
- cfg.deep_set(real_path, value.set_type)
3090
+ cfg.deep_set(real_path, value.set_type(old_type))
2912
3091
 
2913
3092
  $stderr.puts "#{'Key path:'.yellow} #{real_path.join('->').boldwhite}"
2914
- $stderr.puts "#{'Previous:'.yellow} #{(old_value ? old_value .to_s : 'empty').boldwhite}"
2915
- $stderr.puts "#{' New:'.yellow} #{value.set_type.to_s.boldwhite}"
3093
+ $stderr.puts "#{'Previous:'.yellow} #{(old_value ? old_value.to_s : 'empty').boldwhite}"
3094
+ $stderr.puts "#{' New:'.yellow} #{value.set_type(old_type).to_s.boldwhite}"
2916
3095
  end
2917
3096
 
2918
3097
  res = Doing::Prompt.yn('Update selected config', default_response: true)
@@ -2925,22 +3104,126 @@ command :config do |c|
2925
3104
  end
2926
3105
  end
2927
3106
 
2928
- desc 'Undo the last change to the Doing file'
3107
+ desc 'Undo the last X changes to the Doing file'
3108
+ long_desc 'Reverts the last X commands that altered the doing file.
3109
+ All changes performed by a single command are undone at once.
3110
+
3111
+ Specify a number to jump back multiple revisions, or use --select for an interactive menu.'
3112
+ arg_name 'COUNT'
2929
3113
  command :undo do |c|
3114
+ c.example 'doing undo', desc: 'Undo the most recent change to the doing file'
3115
+ c.example 'doing undo 5', desc: 'Undo the last 5 changes to the doing file'
3116
+ c.example 'doing undo --interactive', desc: 'Select from a menu of available revisions'
3117
+ c.example 'doing undo --redo', desc: 'Undo the last undo command'
3118
+
2930
3119
  c.desc 'Specify alternate doing file'
2931
3120
  c.arg_name 'PATH'
2932
3121
  c.flag %i[f file], default_value: wwid.doing_file
2933
3122
 
2934
- c.action do |_global_options, options, _args|
3123
+ c.desc 'Select from recent backups'
3124
+ c.switch %i[i interactive], negatable: false
3125
+
3126
+ c.desc 'Remove old backups, retaining X files'
3127
+ c.arg_name 'COUNT'
3128
+ c.flag %i[p prune], type: Integer
3129
+
3130
+ c.desc 'Redo last undo. Note: you cannot undo a redo.'
3131
+ c.switch %i[r redo]
3132
+
3133
+ c.action do |_global_options, options, args|
3134
+ file = options[:file] || wwid.doing_file
3135
+ count = args.empty? ? 1 : args[0].to_i
3136
+ raise InvalidArgument, "Invalid count specified for undo" unless count&.positive?
3137
+
3138
+ if options[:prune]
3139
+ Doing::Util::Backup.prune_backups(file, options[:prune])
3140
+ elsif options[:redo]
3141
+ if options[:interactive]
3142
+ Doing::Util::Backup.select_redo(file)
3143
+ else
3144
+ Doing::Util::Backup.redo_backup(file, count: count)
3145
+ end
3146
+ else
3147
+ if options[:interactive]
3148
+ Doing::Util::Backup.select_backup(file)
3149
+ else
3150
+ Doing::Util::Backup.restore_last_backup(file, count: count)
3151
+ end
3152
+ end
3153
+ end
3154
+ end
3155
+
3156
+ long_desc 'Shortcut for `doing undo -r`, reverses the last undo command. You cannot undo a redo.'
3157
+ arg_name 'COUNT'
3158
+ command :redo do |c|
3159
+ c.desc 'Specify alternate doing file'
3160
+ c.arg_name 'PATH'
3161
+ c.flag %i[f file], default_value: wwid.doing_file
3162
+
3163
+ c.desc 'Select from an interactive menu'
3164
+ c.switch %i[i interactive]
3165
+
3166
+ c.action do |_global, options, args|
2935
3167
  file = options[:file] || wwid.doing_file
2936
- wwid.restore_backup(file)
3168
+ count = args.empty? ? 1 : args[0].to_i
3169
+ raise InvalidArgument, "Invalid count specified for redo" unless count&.positive?
3170
+ if options[:interactive]
3171
+ Doing::Util::Backup.select_redo(file)
3172
+ else
3173
+ Doing::Util::Backup.redo_backup(file, count: count)
3174
+ end
2937
3175
  end
2938
3176
  end
2939
3177
 
3178
+ desc 'List recent changes in Doing'
3179
+ long_desc 'Display a formatted list of changes in recent versions, latest at the top'
3180
+ command %i[changelog changes] do |c|
3181
+ c.action do |_global_options, options, args|
3182
+ changelog = File.expand_path(File.join(File.dirname(__FILE__), '..', 'CHANGELOG.md'))
3183
+ if File.exist?(changelog)
3184
+ parsed = TTY::Markdown.parse(IO.read(changelog), width: 80, symbols: {override: {bullet: "•"}})
3185
+ Doing::Pager.paginate = true
3186
+ Doing::Pager.page parsed
3187
+ else
3188
+ raise "Error locating changelog"
3189
+ end
3190
+ end
3191
+ end
3192
+
3193
+ arg_name 'OPTION'
3194
+ command :commands_accepting do |c|
3195
+ c.desc 'Output in single column for completion'
3196
+ c.switch %i[c column]
3197
+
3198
+ c.action do |g, o, a|
3199
+ a.each do |option|
3200
+ cmds = []
3201
+ commands.each do |cmd, v|
3202
+ v.flags.merge(v.switches).each do |n, flag|
3203
+ if flag.name == option.to_sym || flag.aliases&.include?(option.to_sym)
3204
+ cmds.push(cmd)
3205
+ end
3206
+ end
3207
+ end
3208
+
3209
+ if o[:column]
3210
+ puts cmds
3211
+ else
3212
+ puts "Commands accepting --#{option}: #{cmds.join(', ')}"
3213
+ end
3214
+ end
3215
+ end
3216
+ end
3217
+
3218
+
2940
3219
  desc 'Import entries from an external source'
2941
3220
  long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
2942
3221
  arg_name 'PATH'
2943
3222
  command :import do |c|
3223
+ c.example 'doing import --type timing "~/Desktop/All Activities.json"', desc: 'Import a Timing.app JSON report'
3224
+ c.example 'doing import --type doing --tag imported --no-autotag ~/doing_backup.md', desc: 'Import an Doing archive, tag all entries with @imported, skip autotagging'
3225
+ c.example 'doing import --type doing --from "10/1 to 10/15" ~/doing_backup.md', desc: 'Import a Doing archive, only importing entries between two dates'
3226
+
2944
3227
  c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
2945
3228
  c.arg_name 'TYPE'
2946
3229
  c.flag :type, default_value: 'doing'
@@ -2953,14 +3236,14 @@ command :import do |c|
2953
3236
  # c.switch [:fuzzy], default_value: false, negatable: false
2954
3237
 
2955
3238
  c.desc 'Force exact search string matching (case sensitive)'
2956
- c.switch %i[x exact], default_value: false, negatable: false
3239
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
2957
3240
 
2958
3241
  c.desc 'Import items that *don\'t* match search/tag/date filters'
2959
3242
  c.switch [:not], default_value: false, negatable: false
2960
3243
 
2961
3244
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
2962
3245
  c.arg_name 'TYPE'
2963
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
3246
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2964
3247
 
2965
3248
  c.desc 'Only import items with recorded time intervals'
2966
3249
  c.switch [:only_timed], default_value: false, negatable: false
@@ -3035,7 +3318,6 @@ end
3035
3318
 
3036
3319
  pre do |global, _command, _options, _args|
3037
3320
  # global[:pager] ||= settings['paginate']
3038
-
3039
3321
  Doing::Pager.paginate = global[:pager]
3040
3322
 
3041
3323
  $stdout.puts "doing v#{Doing::VERSION}" if global[:version]
@@ -3066,6 +3348,8 @@ post do |global, _command, _options, _args|
3066
3348
  # Use skips_post before a command to skip this
3067
3349
  # block on that command only
3068
3350
  Doing.logger.output_results
3351
+ Doing.logger.benchmark(:total, :finish)
3352
+ Doing.logger.log_benchmarks
3069
3353
  end
3070
3354
 
3071
3355
  around do |global, command, options, arguments, code|
@@ -3080,10 +3364,13 @@ around do |global, command, options, arguments, code|
3080
3364
 
3081
3365
  if global[:yes]
3082
3366
  Doing::Prompt.force_answer = true
3367
+ Doing.config.force_answer = true
3083
3368
  elsif global[:no]
3084
3369
  Doing::Prompt.force_answer = false
3370
+ Doing.config.force_answer = false
3085
3371
  else
3086
3372
  Doing::Prompt.default_answer = global[:default]
3373
+ Doing.config.force_answer = global[:default] ? true : false
3087
3374
  end
3088
3375
 
3089
3376
  if global[:config_file] && global[:config_file] != config.config_file
@@ -3096,13 +3383,13 @@ around do |global, command, options, arguments, code|
3096
3383
  config.config_file = cf
3097
3384
  settings = config.configure({ ignore_local: true })
3098
3385
  end
3099
-
3386
+ Doing.logger.benchmark(:init, :start)
3100
3387
  if global[:doing_file]
3101
3388
  wwid.init_doing_file(global[:doing_file])
3102
3389
  else
3103
3390
  wwid.init_doing_file
3104
3391
  end
3105
-
3392
+ Doing.logger.benchmark(:init, :finish)
3106
3393
  wwid.auto_tag = !global[:noauto]
3107
3394
 
3108
3395
  settings[:include_notes] = false unless global[:notes]