doing 2.1.1pre → 2.1.5pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +19 -15
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +58 -8
  6. data/Gemfile.lock +25 -1
  7. data/README.md +1 -1
  8. data/Rakefile +2 -0
  9. data/bin/doing +447 -149
  10. data/doc/Array.html +63 -1
  11. data/doc/BooleanTermParser/Clause.html +293 -0
  12. data/doc/BooleanTermParser/Operator.html +172 -0
  13. data/doc/BooleanTermParser/Query.html +417 -0
  14. data/doc/BooleanTermParser/QueryParser.html +135 -0
  15. data/doc/BooleanTermParser/QueryTransformer.html +124 -0
  16. data/doc/BooleanTermParser.html +115 -0
  17. data/doc/Doing/CLIFormat.html +131 -0
  18. data/doc/Doing/Color.html +2 -2
  19. data/doc/Doing/Completion.html +1 -1
  20. data/doc/Doing/Configuration.html +168 -73
  21. data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  22. data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  23. data/doc/Doing/Errors/DoingStandardError.html +1 -1
  24. data/doc/Doing/Errors/EmptyInput.html +1 -1
  25. data/doc/Doing/Errors/NoResults.html +1 -1
  26. data/doc/Doing/Errors/PluginException.html +1 -1
  27. data/doc/Doing/Errors/UserCancelled.html +1 -1
  28. data/doc/Doing/Errors/WrongCommand.html +1 -1
  29. data/doc/Doing/Errors.html +1 -1
  30. data/doc/Doing/Hooks.html +1 -1
  31. data/doc/Doing/Item.html +177 -86
  32. data/doc/Doing/Items.html +36 -2
  33. data/doc/Doing/LogAdapter.html +70 -1
  34. data/doc/Doing/Note.html +5 -134
  35. data/doc/Doing/Pager.html +1 -1
  36. data/doc/Doing/Plugins.html +380 -40
  37. data/doc/Doing/Prompt.html +70 -18
  38. data/doc/Doing/Section.html +1 -1
  39. data/doc/Doing/TemplateString.html +713 -0
  40. data/doc/Doing/Util/Backup.html +686 -0
  41. data/doc/Doing/Util.html +16 -4
  42. data/doc/Doing/WWID.html +133 -73
  43. data/doc/Doing.html +4 -4
  44. data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  45. data/doc/GLI/Commands.html +1 -1
  46. data/doc/GLI.html +1 -1
  47. data/doc/Hash.html +1 -1
  48. data/doc/PhraseParser/Operator.html +172 -0
  49. data/doc/PhraseParser/PhraseClause.html +303 -0
  50. data/doc/PhraseParser/Query.html +495 -0
  51. data/doc/PhraseParser/QueryParser.html +136 -0
  52. data/doc/PhraseParser/QueryTransformer.html +124 -0
  53. data/doc/PhraseParser/TermClause.html +293 -0
  54. data/doc/PhraseParser.html +115 -0
  55. data/doc/Status.html +1 -1
  56. data/doc/String.html +319 -13
  57. data/doc/Symbol.html +35 -1
  58. data/doc/Time.html +70 -2
  59. data/doc/_index.html +132 -4
  60. data/doc/class_list.html +1 -1
  61. data/doc/file.README.html +2 -2
  62. data/doc/index.html +2 -2
  63. data/doc/method_list.html +648 -160
  64. data/doc/top-level-namespace.html +2 -2
  65. data/doing.gemspec +3 -0
  66. data/doing.rdoc +263 -82
  67. data/lib/completion/doing.bash +18 -18
  68. data/lib/doing/array.rb +9 -0
  69. data/lib/doing/boolean_term_parser.rb +86 -0
  70. data/lib/doing/configuration.rb +63 -24
  71. data/lib/doing/item.rb +112 -10
  72. data/lib/doing/items.rb +6 -0
  73. data/lib/doing/log_adapter.rb +28 -0
  74. data/lib/doing/note.rb +31 -30
  75. data/lib/doing/phrase_parser.rb +124 -0
  76. data/lib/doing/plugin_manager.rb +57 -13
  77. data/lib/doing/plugins/export/dayone_export.rb +209 -0
  78. data/lib/doing/plugins/export/template_export.rb +113 -81
  79. data/lib/doing/prompt.rb +26 -13
  80. data/lib/doing/string.rb +114 -29
  81. data/lib/doing/string_chronify.rb +5 -1
  82. data/lib/doing/symbol.rb +4 -0
  83. data/lib/doing/template_string.rb +197 -0
  84. data/lib/doing/time.rb +32 -0
  85. data/lib/doing/util.rb +6 -7
  86. data/lib/doing/util_backup.rb +287 -0
  87. data/lib/doing/version.rb +1 -1
  88. data/lib/doing/wwid.rb +152 -55
  89. data/lib/doing.rb +9 -0
  90. data/lib/templates/doing-dayone-entry.erb +6 -0
  91. data/lib/templates/doing-dayone.erb +5 -0
  92. metadata +85 -2
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'
@@ -521,8 +546,19 @@ long_desc 'List all entries and select with typeahead fuzzy matching.
521
546
 
522
547
  Multiple selections are allowed, hit tab to add the highlighted entry to the
523
548
  selection, and use ctrl-a to select all visible items. Return processes the
524
- selected entries.'
549
+ selected entries.
550
+
551
+ Search in the menu by typing:
552
+
553
+ sbtrkt fuzzy-match Items that match sbtrkt
554
+
555
+ \'wild exact-match (quoted) Items that include wild
556
+
557
+ !fire inverse-exact-match Items that do not include fire'
525
558
  command :select do |c|
559
+ c.example 'doing select', desc: 'Select from all entries. A menu of available actions will be presented after confirming the selection.'
560
+ c.example 'doing select --editor', desc: 'Select entries from a menu and batch edit them in your default editor'
561
+ 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
562
  c.desc 'Select from a specific section'
527
563
  c.arg_name 'SECTION'
528
564
  c.flag %i[s section]
@@ -568,14 +604,14 @@ command :select do |c|
568
604
  c.flag [:from]
569
605
 
570
606
  c.desc 'Force exact search string matching (case sensitive)'
571
- c.switch %i[x exact], default_value: false, negatable: false
607
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
572
608
 
573
609
  c.desc 'Select items that *don\'t* match search/tag filters'
574
610
  c.switch [:not], default_value: false, negatable: false
575
611
 
576
612
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
577
613
  c.arg_name 'TYPE'
578
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
614
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
579
615
 
580
616
  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
617
  c.switch %i[menu], negatable: true, default_value: true
@@ -680,6 +716,9 @@ command :later do |c|
680
716
  end
681
717
 
682
718
  desc 'Add a completed item with @done(date). No argument finishes last entry.'
719
+ desc 'Use this command to add an entry after you\'ve already finished it. It will be immediately marked as @done.
720
+ You can modify the start and end times of the entry using the --back, --took, and --at flags, making it an easy
721
+ way to add entries in post and maintain accurate (albeit manual) time tracking.'
683
722
  arg_name 'ENTRY'
684
723
  command %i[done did] do |c|
685
724
  c.example 'doing done', desc: 'Tag the last entry @done'
@@ -890,13 +929,13 @@ command :cancel do |c|
890
929
  c.arg_name 'NAME'
891
930
  c.flag %i[s section]
892
931
 
893
- c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2)'
932
+ c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2). Wildcards allowed (*, ?).'
894
933
  c.arg_name 'TAG'
895
934
  c.flag [:tag]
896
935
 
897
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
936
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
898
937
  c.arg_name 'BOOLEAN'
899
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
938
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
900
939
 
901
940
  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
941
  c.arg_name 'QUERY'
@@ -906,14 +945,14 @@ command :cancel do |c|
906
945
  # c.switch [:fuzzy], default_value: false, negatable: false
907
946
 
908
947
  c.desc 'Force exact search string matching (case sensitive)'
909
- c.switch %i[x exact], default_value: false, negatable: false
948
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
910
949
 
911
950
  c.desc 'Finish items that *don\'t* match search/tag filters'
912
951
  c.switch [:not], default_value: false, negatable: false
913
952
 
914
953
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
915
954
  c.arg_name 'TYPE'
916
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
955
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
917
956
 
918
957
  c.desc 'Cancel last entry (or entries) not already marked @done'
919
958
  c.switch %i[u unfinished], negatable: false, default_value: false
@@ -997,7 +1036,7 @@ command :finish do |c|
997
1036
  c.flag [:at]
998
1037
 
999
1038
  c.desc 'Finish the last X entries containing TAG.
1000
- Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
1039
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
1001
1040
  c.arg_name 'TAG'
1002
1041
  c.flag [:tag]
1003
1042
 
@@ -1009,18 +1048,18 @@ command :finish do |c|
1009
1048
  # c.switch [:fuzzy], default_value: false, negatable: false
1010
1049
 
1011
1050
  c.desc 'Force exact search string matching (case sensitive)'
1012
- c.switch %i[x exact], default_value: false, negatable: false
1051
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1013
1052
 
1014
1053
  c.desc 'Finish items that *don\'t* match search/tag filters'
1015
1054
  c.switch [:not], default_value: false, negatable: false
1016
1055
 
1017
1056
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1018
1057
  c.arg_name 'TYPE'
1019
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1058
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1020
1059
 
1021
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1060
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
1022
1061
  c.arg_name 'BOOLEAN'
1023
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1062
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1024
1063
 
1025
1064
  c.desc 'Remove done tag'
1026
1065
  c.switch %i[r remove], negatable: false, default_value: false
@@ -1119,7 +1158,14 @@ command :finish do |c|
1119
1158
  end
1120
1159
 
1121
1160
  desc 'Repeat last entry as new entry'
1161
+ 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
1162
  command %i[again resume] do |c|
1163
+ c.example 'doing resume', desc: 'Duplicate the most recent entry with a new start time, removing any @done tag'
1164
+ c.example 'doing again', desc: 'again is an alias for resume'
1165
+ c.example 'doing resume --editor', desc: 'Repeat the last entry, opening the new entry in the default editor'
1166
+ c.example 'doing resume --tag project1 --in Projects', desc: 'Repeat the last entry tagged @project1, creating the new entry in the Projects section'
1167
+ c.example 'doing resume --interactive', desc: 'Select the entry to repeat from a menu'
1168
+
1123
1169
  c.desc 'Get last entry from a specific section'
1124
1170
  c.arg_name 'NAME'
1125
1171
  c.flag %i[s section], default_value: 'All'
@@ -1128,7 +1174,7 @@ command %i[again resume] do |c|
1128
1174
  c.arg_name 'SECTION_NAME'
1129
1175
  c.flag [:in]
1130
1176
 
1131
- c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma.'
1177
+ c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?).'
1132
1178
  c.arg_name 'TAG'
1133
1179
  c.flag [:tag]
1134
1180
 
@@ -1141,18 +1187,18 @@ command %i[again resume] do |c|
1141
1187
  # c.switch [:fuzzy], default_value: false, negatable: false
1142
1188
 
1143
1189
  c.desc 'Force exact search string matching (case sensitive)'
1144
- c.switch %i[x exact], default_value: false, negatable: false
1190
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1145
1191
 
1146
1192
  c.desc 'Resume items that *don\'t* match search/tag filters'
1147
1193
  c.switch [:not], default_value: false, negatable: false
1148
1194
 
1149
1195
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1150
1196
  c.arg_name 'TYPE'
1151
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1197
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1152
1198
 
1153
- c.desc 'Boolean used to combine multiple tags'
1199
+ c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans.'
1154
1200
  c.arg_name 'BOOLEAN'
1155
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1201
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1156
1202
 
1157
1203
  c.desc "Edit duplicated entry with #{Doing::Util.default_editor} before adding"
1158
1204
  c.switch %i[e editor], negatable: false, default_value: false
@@ -1186,6 +1232,77 @@ command %i[again resume] do |c|
1186
1232
  end
1187
1233
  end
1188
1234
 
1235
+ desc 'List all tags in the current Doing file'
1236
+ command :tags do |c|
1237
+ c.desc 'Section'
1238
+ c.arg_name 'SECTION_NAME'
1239
+ c.flag %i[s section], default_value: 'All'
1240
+
1241
+ c.desc 'Show count of occurrences'
1242
+ c.switch %i[c counts]
1243
+
1244
+ c.desc 'Sort by name or count'
1245
+ c.arg_name 'SORT_ORDER'
1246
+ c.flag %i[sort], default_value: 'name', must_match: /^(?:n(?:ame)?|c(?:ount)?)$/
1247
+
1248
+ c.desc 'Sort order (asc/desc)'
1249
+ c.arg_name 'ORDER'
1250
+ c.flag %i[o order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
1251
+
1252
+ c.desc 'Get tags for entries matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?).'
1253
+ c.arg_name 'TAG'
1254
+ c.flag [:tag]
1255
+
1256
+ c.desc 'Get tags for items matching search. Surround with
1257
+ slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
1258
+ c.arg_name 'QUERY'
1259
+ c.flag [:search]
1260
+
1261
+ # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
1262
+ # c.switch [:fuzzy], default_value: false, negatable: false
1263
+
1264
+ c.desc 'Force exact search string matching (case sensitive)'
1265
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1266
+
1267
+ c.desc 'Get tags from items that *don\'t* match search/tag filters'
1268
+ c.switch [:not], default_value: false, negatable: false
1269
+
1270
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1271
+ c.arg_name 'TYPE'
1272
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1273
+
1274
+ c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans.'
1275
+ c.arg_name 'BOOLEAN'
1276
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1277
+
1278
+ c.desc 'Select items to scan from a menu of matching entries'
1279
+ c.switch %i[i interactive], negatable: false, default_value: false
1280
+
1281
+ c.action do |_global, options, args|
1282
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
1283
+
1284
+ items = wwid.filter_items([], opt: options)
1285
+
1286
+ # items = wwid.content.in_section(section)
1287
+ tags = wwid.all_tags(items, counts: true)
1288
+
1289
+ if options[:sort] =~ /^n/i
1290
+ tags = tags.sort_by { |tag, count| tag }
1291
+ else
1292
+ tags = tags.sort_by { |tag, count| count }
1293
+ end
1294
+
1295
+ tags.reverse! if options[:order].normalize_order == 'desc'
1296
+
1297
+ if options[:counts]
1298
+ tags.each { |t, c| puts "#{t} (#{c})" }
1299
+ else
1300
+ tags.each { |t, c| puts "#{t}" }
1301
+ end
1302
+ end
1303
+ end
1304
+
1305
+
1189
1306
  desc 'Add tag(s) to last entry'
1190
1307
  long_desc 'Add (or remove) tags from the last entry, or from multiple entries
1191
1308
  (with `--count`), entries matching a search (with `--search`), or entries
@@ -1239,7 +1356,7 @@ command :tag do |c|
1239
1356
  c.switch %i[a autotag], negatable: false, default_value: false
1240
1357
 
1241
1358
  c.desc 'Tag the last X entries containing TAG.
1242
- Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
1359
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
1243
1360
  c.arg_name 'TAG'
1244
1361
  c.flag [:tag]
1245
1362
 
@@ -1251,18 +1368,18 @@ command :tag do |c|
1251
1368
  # c.switch [:fuzzy], default_value: false, negatable: false
1252
1369
 
1253
1370
  c.desc 'Force exact search string matching (case sensitive)'
1254
- c.switch %i[x exact], default_value: false, negatable: false
1371
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1255
1372
 
1256
1373
  c.desc 'Tag items that *don\'t* match search/tag filters'
1257
1374
  c.switch [:not], default_value: false, negatable: false
1258
1375
 
1259
1376
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1260
1377
  c.arg_name 'TYPE'
1261
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1378
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1262
1379
 
1263
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1380
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
1264
1381
  c.arg_name 'BOOLEAN'
1265
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1382
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1266
1383
 
1267
1384
  c.desc 'Select item(s) to tag from a menu of matching entries'
1268
1385
  c.switch %i[i interactive], negatable: false, default_value: false
@@ -1314,44 +1431,49 @@ command :tag do |c|
1314
1431
  options[:search] = search
1315
1432
  end
1316
1433
 
1434
+ options[:count] = count
1435
+ options[:section] = section
1436
+ options[:tag] = search_tags
1437
+ options[:tags] = tags
1438
+ options[:tag_bool] = options[:bool].normalize_bool
1439
+
1317
1440
  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
1441
+ matches = wwid.filter_items([], opt: options).count
1442
+
1443
+ if matches > 5
1444
+ if options[:search]
1445
+ section_q = ' matching your search terms'
1446
+ elsif options[:tag]
1447
+ section_q = ' matching your tag search'
1448
+ elsif section == 'All'
1449
+ section_q = ''
1450
+ else
1451
+ section_q = " in section #{section}"
1452
+ end
1327
1453
 
1328
1454
 
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
1455
+ question = if options[:autotag]
1456
+ "Are you sure you want to autotag #{matches} records#{section_q}"
1457
+ elsif options[:remove]
1458
+ "Are you sure you want to remove #{tags.join(' and ')} from #{matches} records#{section_q}"
1459
+ else
1460
+ "Are you sure you want to add #{tags.join(' and ')} to #{matches} records#{section_q}"
1461
+ end
1336
1462
 
1337
- res = Doing::Prompt.yn(question, default_response: false)
1463
+ res = Doing::Prompt.yn(question, default_response: false)
1338
1464
 
1339
- raise UserCancelled unless res
1465
+ raise UserCancelled unless res
1466
+ end
1340
1467
  end
1341
1468
 
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
1469
  wwid.tag_last(options)
1349
1470
  end
1350
1471
  end
1351
1472
 
1352
1473
  desc 'Mark last entry as flagged'
1353
- command [:mark, :flag] do |c|
1474
+ command %i[mark flag] do |c|
1354
1475
  c.example 'doing flag', desc: 'Add @flagged to the last entry created'
1476
+ c.example 'doing mark', desc: 'mark is an alias for flag'
1355
1477
  c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
1356
1478
  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
1479
 
@@ -1376,7 +1498,7 @@ command [:mark, :flag] do |c|
1376
1498
  c.switch %i[u unfinished], negatable: false, default_value: false
1377
1499
 
1378
1500
  c.desc 'Flag the last entry containing TAG.
1379
- Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
1501
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
1380
1502
  c.arg_name 'TAG'
1381
1503
  c.flag [:tag]
1382
1504
 
@@ -1388,18 +1510,18 @@ command [:mark, :flag] do |c|
1388
1510
  # c.switch [:fuzzy], default_value: false, negatable: false
1389
1511
 
1390
1512
  c.desc 'Force exact search string matching (case sensitive)'
1391
- c.switch %i[x exact], default_value: false, negatable: false
1513
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1392
1514
 
1393
1515
  c.desc 'Flag items that *don\'t* match search/tag/date filters'
1394
1516
  c.switch [:not], default_value: false, negatable: false
1395
1517
 
1396
1518
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1397
1519
  c.arg_name 'TYPE'
1398
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1520
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1399
1521
 
1400
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1522
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
1401
1523
  c.arg_name 'BOOLEAN'
1402
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1524
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1403
1525
 
1404
1526
  c.desc 'Select item(s) to flag from a menu of matching entries'
1405
1527
  c.switch %i[i interactive], negatable: false, default_value: false
@@ -1473,7 +1595,11 @@ end
1473
1595
  desc 'List all entries'
1474
1596
  long_desc %(
1475
1597
  The argument can be a section name, @tag(s) or both.
1476
- "pick" or "choose" as an argument will offer a section menu.
1598
+ "pick" or "choose" as an argument will offer a section menu. Run with `--menu` to get a menu of available tags.
1599
+
1600
+ Show tags by passing @tagname arguments. Multiple tags can be combined, and you can specify the boolean used to
1601
+ combine them with `--bool (AND|OR|NOT)`. You can also use @+tagname to require a tag to match, or @-tagname to ignore
1602
+ entries containing tagname. +/- operators require `--bool PATTERN` (which is the default).
1477
1603
  )
1478
1604
  arg_name '[SECTION|@TAGS]'
1479
1605
  command :show do |c|
@@ -1485,17 +1611,17 @@ command :show do |c|
1485
1611
  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
1612
  c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
1487
1613
 
1488
- c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
1614
+ 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
1615
  c.arg_name 'TAG'
1490
1616
  c.flag [:tag]
1491
1617
 
1492
- c.desc 'Tag boolean (AND,OR,NOT)'
1618
+ c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
1493
1619
  c.arg_name 'BOOLEAN'
1494
- c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
1620
+ c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1495
1621
 
1496
1622
  c.desc 'Max count to show'
1497
1623
  c.arg_name 'MAX'
1498
- c.flag %i[c count], default_value: 0
1624
+ c.flag %i[c count], default_value: 0, must_match: /^\d+$/, type: Integer
1499
1625
 
1500
1626
  c.desc 'Age (oldest|newest)'
1501
1627
  c.arg_name 'AGE'
@@ -1529,14 +1655,14 @@ command :show do |c|
1529
1655
  # c.switch [:fuzzy], default_value: false, negatable: false
1530
1656
 
1531
1657
  c.desc 'Force exact search string matching (case sensitive)'
1532
- c.switch %i[x exact], default_value: false, negatable: false
1658
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1533
1659
 
1534
1660
  c.desc 'Show items that *don\'t* match search/tag/date filters'
1535
1661
  c.switch [:not], default_value: false, negatable: false
1536
1662
 
1537
1663
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1538
1664
  c.arg_name 'TYPE'
1539
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1665
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1540
1666
 
1541
1667
  c.desc 'Sort order (asc/desc)'
1542
1668
  c.arg_name 'ORDER'
@@ -1545,6 +1671,9 @@ command :show do |c|
1545
1671
  c.desc 'Show time intervals on @done tasks'
1546
1672
  c.switch %i[t times], default_value: true, negatable: true
1547
1673
 
1674
+ c.desc 'Show elapsed time on entries without @done tag'
1675
+ c.switch [:duration]
1676
+
1548
1677
  c.desc 'Show intervals with totals at the end of output'
1549
1678
  c.switch [:totals], default_value: false, negatable: false
1550
1679
 
@@ -1561,6 +1690,9 @@ command :show do |c|
1561
1690
  c.desc 'Only show items with recorded time intervals'
1562
1691
  c.switch [:only_timed], default_value: false, negatable: false
1563
1692
 
1693
+ c.desc 'Select section or tag to display from a menu'
1694
+ c.switch %i[m menu], negatable: false, default_value: false
1695
+
1564
1696
  c.desc 'Select from a menu of matching entries to perform additional operations'
1565
1697
  c.switch %i[i interactive], negatable: false, default_value: false
1566
1698
 
@@ -1573,15 +1705,17 @@ command :show do |c|
1573
1705
 
1574
1706
  tag_filter = false
1575
1707
  tags = []
1708
+
1576
1709
  if args.length.positive?
1577
1710
  case args[0]
1578
1711
  when /^all$/i
1579
1712
  section = 'All'
1580
1713
  args.shift
1581
1714
  when /^(choose|pick)$/i
1582
- section = wwid.choose_section
1715
+ section = wwid.choose_section(include_all: true)
1716
+
1583
1717
  args.shift
1584
- when /^@/
1718
+ when /^[@+-]/
1585
1719
  section = 'All'
1586
1720
  else
1587
1721
  begin
@@ -1604,18 +1738,12 @@ command :show do |c|
1604
1738
  end
1605
1739
  end
1606
1740
  else
1607
- section = settings['current_section']
1741
+ section = options[:menu] ? wwid.choose_section(include_all: true) : settings['current_section']
1742
+ section ||= 'All'
1608
1743
  end
1609
1744
 
1610
1745
  tags.concat(options[:tag].to_tags) if options[:tag]
1611
1746
 
1612
- unless tags.empty?
1613
- tag_filter = {
1614
- 'tags' => tags,
1615
- 'bool' => options[:bool].normalize_bool
1616
- }
1617
- end
1618
-
1619
1747
  options[:times] = true if options[:totals]
1620
1748
 
1621
1749
  template = settings['templates']['default'].deep_merge({
@@ -1633,27 +1761,55 @@ command :show do |c|
1633
1761
  options[:search] = search
1634
1762
  end
1635
1763
 
1764
+ options[:section] = section
1765
+
1766
+ unless tags.empty?
1767
+ tag_filter = {
1768
+ 'tags' => tags,
1769
+ 'bool' => options[:bool].normalize_bool
1770
+ }
1771
+ end
1772
+
1773
+ options[:tag_filter] = tag_filter
1774
+ options[:tag] = nil
1775
+
1776
+ items = wwid.filter_items([], opt: options)
1777
+
1778
+ if options[:menu]
1779
+ tag = wwid.choose_tag(section, items: items, include_all: true)
1780
+ raise UserCancelled unless tag
1781
+
1782
+ # options[:bool] = :and unless tags.empty?
1783
+
1784
+ tags = tag.split(/ +/).map { |t| t.strip.sub(/^@?/, '') } if tag =~ /^@/
1785
+ unless tags.empty?
1786
+ tag_filter = {
1787
+ 'tags' => tags,
1788
+ 'bool' => options[:bool].normalize_bool
1789
+ }
1790
+ options[:tag_filter] = tag_filter
1791
+ end
1792
+ end
1793
+
1636
1794
  opt = options.dup
1637
1795
  opt[:sort_tags] = options[:tag_sort] =~ /^n/i
1638
1796
  opt[:count] = options[:count].to_i
1639
1797
  opt[:highlight] = true
1640
1798
  opt[:order] = options[:sort].normalize_order
1641
- opt[:section] = section
1642
1799
  opt[:tag] = nil
1643
- opt[:tag_filter] = tag_filter
1644
1800
  opt[:tag_order] = options[:tag_order].normalize_order
1645
1801
  opt[:tags_color] = template['tags_color']
1646
1802
 
1647
- Doing::Pager.page wwid.list_section(opt)
1803
+ Doing::Pager.page wwid.list_section(opt, items: items)
1648
1804
  end
1649
1805
  end
1650
1806
 
1651
1807
  desc 'Search for entries'
1652
- long_desc <<~'EODESC'
1653
- Search all sections (or limit to a single section) for entries matching text or regular expression. Normal strings are fuzzy matched.
1808
+ long_desc %(
1809
+ Search all sections (or limit to a single section) for entries matching text or regular expression. Normal strings are fuzzy matched.
1654
1810
 
1655
- To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
1656
- EODESC
1811
+ To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
1812
+ )
1657
1813
 
1658
1814
  arg_name 'SEARCH_PATTERN'
1659
1815
  command %i[grep search] do |c|
@@ -1692,6 +1848,9 @@ command %i[grep search] do |c|
1692
1848
  c.desc 'Show time intervals on @done tasks'
1693
1849
  c.switch %i[t times], default_value: true, negatable: true
1694
1850
 
1851
+ c.desc 'Show elapsed time on entries without @done tag'
1852
+ c.switch [:duration]
1853
+
1695
1854
  c.desc 'Show intervals with totals at the end of output'
1696
1855
  c.switch [:totals], default_value: false, negatable: false
1697
1856
 
@@ -1708,14 +1867,14 @@ command %i[grep search] do |c|
1708
1867
  # c.switch [:fuzzy], default_value: false, negatable: false
1709
1868
 
1710
1869
  c.desc 'Force exact string matching (case sensitive)'
1711
- c.switch %i[x exact], default_value: false, negatable: false
1870
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1712
1871
 
1713
1872
  c.desc 'Show items that *don\'t* match search string'
1714
1873
  c.switch [:not], default_value: false, negatable: false
1715
1874
 
1716
1875
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1717
1876
  c.arg_name 'TYPE'
1718
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1877
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1719
1878
 
1720
1879
  c.desc 'Display an interactive menu of results to perform further operations'
1721
1880
  c.switch %i[i interactive], default_value: false, negatable: false
@@ -1761,6 +1920,9 @@ command :recent do |c|
1761
1920
  c.desc 'Show time intervals on @done tasks'
1762
1921
  c.switch %i[t times], default_value: true, negatable: true
1763
1922
 
1923
+ c.desc 'Show elapsed time on entries without @done tag'
1924
+ c.switch [:duration]
1925
+
1764
1926
  c.desc 'Show intervals with totals at the end of output'
1765
1927
  c.switch [:totals], default_value: false, negatable: false
1766
1928
 
@@ -1800,7 +1962,8 @@ command :recent do |c|
1800
1962
  tags_color: tags_color,
1801
1963
  times: options[:times],
1802
1964
  totals: options[:totals],
1803
- interactive: options[:interactive]
1965
+ interactive: options[:interactive],
1966
+ duration: options[:duration]
1804
1967
  }
1805
1968
 
1806
1969
  Doing::Pager::page wwid.recent(count, section.cap_first, opts)
@@ -1810,6 +1973,8 @@ command :recent do |c|
1810
1973
  end
1811
1974
 
1812
1975
  desc 'List entries from today'
1976
+ long_desc 'List entries from the current day. Use --before, --after, and
1977
+ --from to specify time ranges.'
1813
1978
  command :today do |c|
1814
1979
  c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day'
1815
1980
  c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
@@ -1823,6 +1988,9 @@ command :today do |c|
1823
1988
  c.desc 'Show time intervals on @done tasks'
1824
1989
  c.switch %i[t times], default_value: true, negatable: true
1825
1990
 
1991
+ c.desc 'Show elapsed time on entries without @done tag'
1992
+ c.switch [:duration]
1993
+
1826
1994
  c.desc 'Show time totals at the end of output'
1827
1995
  c.switch [:totals], default_value: false, negatable: false
1828
1996
 
@@ -1855,7 +2023,7 @@ command :today do |c|
1855
2023
 
1856
2024
  options[:times] = true if options[:totals]
1857
2025
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1858
- filter_options = %i[after before from section sort_tags totals].each_with_object({}) { |k, hsh| hsh[k] = options[k] }
2026
+ filter_options = %i[after before duration from section sort_tags totals].each_with_object({}) { |k, hsh| hsh[k] = options[k] }
1859
2027
 
1860
2028
  Doing::Pager.page wwid.today(options[:times], options[:output], filter_options).chomp
1861
2029
  end
@@ -1878,6 +2046,9 @@ command :on do |c|
1878
2046
  c.desc 'Show time intervals on @done tasks'
1879
2047
  c.switch %i[t times], default_value: true, negatable: true
1880
2048
 
2049
+ c.desc 'Show elapsed time on entries without @done tag'
2050
+ c.switch [:duration]
2051
+
1881
2052
  c.desc 'Show time totals at the end of output'
1882
2053
  c.switch [:totals], default_value: false, negatable: false
1883
2054
 
@@ -1917,7 +2088,7 @@ command :on do |c|
1917
2088
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1918
2089
 
1919
2090
  Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
1920
- { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
2091
+ { duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1921
2092
  end
1922
2093
  end
1923
2094
 
@@ -1936,6 +2107,9 @@ command :since do |c|
1936
2107
  c.desc 'Show time intervals on @done tasks'
1937
2108
  c.switch %i[t times], default_value: true, negatable: true
1938
2109
 
2110
+ c.desc 'Show elapsed time on entries without @done tag'
2111
+ c.switch [:duration]
2112
+
1939
2113
  c.desc 'Show time totals at the end of output'
1940
2114
  c.switch [:totals], default_value: false, negatable: false
1941
2115
 
@@ -1970,11 +2144,13 @@ command :since do |c|
1970
2144
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1971
2145
 
1972
2146
  Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
1973
- { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
2147
+ { duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1974
2148
  end
1975
2149
  end
1976
2150
 
1977
2151
  desc 'List entries from yesterday'
2152
+ desc 'Show only entries with start times within the previous 24 hour period. Use --before, --after, and --from to limit to
2153
+ time spans within the day.'
1978
2154
  command :yesterday do |c|
1979
2155
  c.example 'doing yesterday', desc: 'List all entries from the previous day'
1980
2156
  c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm'
@@ -1991,6 +2167,9 @@ command :yesterday do |c|
1991
2167
  c.desc 'Show time intervals on @done tasks'
1992
2168
  c.switch %i[t times], default_value: true, negatable: true
1993
2169
 
2170
+ c.desc 'Show elapsed time on entries without @done tag'
2171
+ c.switch [:duration]
2172
+
1994
2173
  c.desc 'Show time totals at the end of output'
1995
2174
  c.switch [:totals], default_value: false, negatable: false
1996
2175
 
@@ -2031,6 +2210,7 @@ command :yesterday do |c|
2031
2210
  opt = {
2032
2211
  after: options[:after],
2033
2212
  before: options[:before],
2213
+ duration: options[:duration],
2034
2214
  from: options[:from],
2035
2215
  sort_tags: options[:sort_tags],
2036
2216
  tag_order: options[:tag_order].normalize_order,
@@ -2042,6 +2222,8 @@ command :yesterday do |c|
2042
2222
  end
2043
2223
 
2044
2224
  desc 'Show the last entry, optionally edit'
2225
+ long_desc 'Shows the last entry. Using --search and --tag filters, you can view/edit the last entry matching a filter,
2226
+ allowing `doing last` to target historical entries.'
2045
2227
  command :last do |c|
2046
2228
  c.example 'doing last', desc: 'Show the most recent entry in all sections'
2047
2229
  c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section'
@@ -2058,30 +2240,33 @@ command :last do |c|
2058
2240
  c.desc "Edit entry with #{Doing::Util.default_editor}"
2059
2241
  c.switch %i[e editor], negatable: false, default_value: false
2060
2242
 
2061
- c.desc 'Tag filter, combine multiple tags with a comma.'
2243
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
2062
2244
  c.arg_name 'TAG'
2063
2245
  c.flag [:tag]
2064
2246
 
2065
- c.desc 'Tag boolean'
2247
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
2066
2248
  c.arg_name 'BOOLEAN'
2067
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
2249
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2068
2250
 
2069
2251
  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2070
2252
  c.arg_name 'QUERY'
2071
2253
  c.flag [:search]
2072
2254
 
2255
+ c.desc 'Show elapsed time if entry is not tagged @done'
2256
+ c.switch [:duration]
2257
+
2073
2258
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
2074
2259
  # c.switch [:fuzzy], default_value: false, negatable: false
2075
2260
 
2076
2261
  c.desc 'Force exact search string matching (case sensitive)'
2077
- c.switch %i[x exact], default_value: false, negatable: false
2262
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
2078
2263
 
2079
2264
  c.desc 'Show items that *don\'t* match search string or tag filter'
2080
2265
  c.switch [:not], default_value: false, negatable: false
2081
2266
 
2082
2267
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
2083
2268
  c.arg_name 'TYPE'
2084
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2269
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2085
2270
 
2086
2271
  c.action do |global_options, options, _args|
2087
2272
  options[:fuzzy] = false
@@ -2091,15 +2276,7 @@ command :last do |c|
2091
2276
  tags = []
2092
2277
  else
2093
2278
  tags = options[:tag].to_tags
2094
- options[:bool] = case options[:bool]
2095
- when /(any|or)/i
2096
- :or
2097
- when /(not|none)/i
2098
- :not
2099
- else
2100
- :and
2101
- end
2102
-
2279
+ options[:bool] = options[:bool].normalize_bool
2103
2280
  end
2104
2281
 
2105
2282
  options[:case] = options[:case].normalize_case
@@ -2115,7 +2292,15 @@ command :last do |c|
2115
2292
  wwid.edit_last(section: options[:section], options: { search: search, fuzzy: options[:fuzzy], case: options[:case], tag: tags, tag_bool: options[:bool], not: options[:not] })
2116
2293
  else
2117
2294
  Doing::Pager::page wwid.last(times: true, section: options[:section],
2118
- options: { search: search, fuzzy: options[:fuzzy], case: options[:case], negate: options[:not], tag: tags, tag_bool: options[:bool] }).strip
2295
+ options: {
2296
+ duration: options[:duration],
2297
+ search: search,
2298
+ fuzzy: options[:fuzzy],
2299
+ case: options[:case],
2300
+ negate: options[:not],
2301
+ tag: tags,
2302
+ tag_bool: options[:bool]
2303
+ }).strip
2119
2304
  end
2120
2305
  end
2121
2306
  end
@@ -2196,6 +2381,8 @@ command :plugins do |c|
2196
2381
  end
2197
2382
 
2198
2383
  desc 'Generate shell completion scripts'
2384
+ desc 'Generates the necessary scripts to add command line completion to various shells, so typing \'doing\' and hitting
2385
+ tab will offer completions of subcommands and their options.'
2199
2386
  command :completion do |c|
2200
2387
  c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
2201
2388
  c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
@@ -2218,7 +2405,8 @@ command :completion do |c|
2218
2405
  end
2219
2406
 
2220
2407
  desc 'Display a user-created view'
2221
- long_desc 'Command line options override view configuration'
2408
+ long_desc 'Views are defined in your configuration (use `doing config` to edit).
2409
+ Command line options override view configuration.'
2222
2410
  arg_name 'VIEW_NAME'
2223
2411
  command :view do |c|
2224
2412
  c.example 'doing view color', desc: 'Display entries according to config for view "color"'
@@ -2239,19 +2427,22 @@ command :view do |c|
2239
2427
  c.desc 'Show time intervals on @done tasks'
2240
2428
  c.switch %i[t times], default_value: true, negatable: true
2241
2429
 
2430
+ c.desc 'Show elapsed time on entries without @done tag'
2431
+ c.switch [:duration]
2432
+
2242
2433
  c.desc 'Show intervals with totals at the end of output'
2243
2434
  c.switch [:totals], default_value: false, negatable: false
2244
2435
 
2245
2436
  c.desc 'Include colors in output'
2246
2437
  c.switch [:color], default_value: true, negatable: true
2247
2438
 
2248
- c.desc 'Tag filter, combine multiple tags with a comma.'
2439
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
2249
2440
  c.arg_name 'TAG'
2250
2441
  c.flag [:tag]
2251
2442
 
2252
- c.desc 'Tag boolean (AND,OR,NOT)'
2443
+ c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
2253
2444
  c.arg_name 'BOOLEAN'
2254
- c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
2445
+ c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2255
2446
 
2256
2447
  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2257
2448
  c.arg_name 'QUERY'
@@ -2261,14 +2452,14 @@ command :view do |c|
2261
2452
  # c.switch [:fuzzy], default_value: false, negatable: false
2262
2453
 
2263
2454
  c.desc 'Force exact search string matching (case sensitive)'
2264
- c.switch %i[x exact], default_value: false, negatable: false
2455
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
2265
2456
 
2266
2457
  c.desc 'Show items that *don\'t* match search string'
2267
2458
  c.switch [:not], default_value: false, negatable: false
2268
2459
 
2269
2460
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
2270
2461
  c.arg_name 'TYPE'
2271
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2462
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2272
2463
 
2273
2464
  c.desc 'Sort tags by (name|time)'
2274
2465
  c.arg_name 'KEY'
@@ -2330,6 +2521,7 @@ command :view do |c|
2330
2521
  end
2331
2522
 
2332
2523
  view = wwid.get_view(title)
2524
+
2333
2525
  if view
2334
2526
  page_title = view.key?('title') ? view['title'] : title.cap_first
2335
2527
  only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed]
@@ -2345,16 +2537,22 @@ command :view do |c|
2345
2537
  tag_filter = false
2346
2538
  if options[:tag]
2347
2539
  tag_filter = { 'tags' => [], 'bool' => 'OR' }
2348
- tag_filter['tags'] = options[:tag].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2349
- tag_filter['bool'] = options[:bool].normalize_bool
2540
+ bool = options[:bool].normalize_bool
2541
+ tag_filter['bool'] = bool
2542
+ tag_filter['tags'] = if bool == :pattern
2543
+ options[:tag]
2544
+ else
2545
+ options[:tag].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2546
+ end
2350
2547
  elsif view.key?('tags') && !(view['tags'].nil? || view['tags'].empty?)
2351
2548
  tag_filter = { 'tags' => [], 'bool' => 'OR' }
2549
+ bool = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :pattern
2550
+ tag_filter['bool'] = bool
2352
2551
  tag_filter['tags'] = if view['tags'].instance_of?(Array)
2353
- view['tags'].map(&:strip)
2552
+ bool == :pattern ? view['tags'].join(' ').strip : view['tags'].map(&:strip)
2354
2553
  else
2355
- view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2554
+ bool == :pattern ? view['tags'].strip : view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2356
2555
  end
2357
- tag_filter['bool'] = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :or
2358
2556
  end
2359
2557
 
2360
2558
  # If the -o/--output flag was specified, override any default in the view template
@@ -2391,7 +2589,7 @@ command :view do |c|
2391
2589
  false
2392
2590
  end
2393
2591
 
2394
- %w[before after from].each { |k| options[k.to_sym] = view[k] if view.key?(k) && !options[k.to_sym] }
2592
+ %w[before after from duration].each { |k| options[k.to_sym] = view[k] if view.key?(k) && !options[k.to_sym] }
2395
2593
 
2396
2594
  options[:case] = options[:case].normalize_case
2397
2595
 
@@ -2403,6 +2601,7 @@ command :view do |c|
2403
2601
  end
2404
2602
 
2405
2603
  opts = options.dup
2604
+ opts[:view_template] = title
2406
2605
  opts[:count] = count
2407
2606
  opts[:format] = date_format
2408
2607
  opts[:highlight] = options[:color]
@@ -2463,13 +2662,13 @@ command %i[archive move] do |c|
2463
2662
  c.desc 'Label moved items with @from(SECTION_NAME)'
2464
2663
  c.switch [:label], default_value: true, negatable: true
2465
2664
 
2466
- c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
2665
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands.'
2467
2666
  c.arg_name 'TAG'
2468
2667
  c.flag [:tag]
2469
2668
 
2470
- c.desc 'Tag boolean (AND|OR|NOT)'
2669
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
2471
2670
  c.arg_name 'BOOLEAN'
2472
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
2671
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2473
2672
 
2474
2673
  c.desc 'Search filter'
2475
2674
  c.arg_name 'QUERY'
@@ -2479,14 +2678,14 @@ command %i[archive move] do |c|
2479
2678
  # c.switch [:fuzzy], default_value: false, negatable: false
2480
2679
 
2481
2680
  c.desc 'Force exact search string matching (case sensitive)'
2482
- c.switch %i[x exact], default_value: false, negatable: false
2681
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
2483
2682
 
2484
2683
  c.desc 'Show items that *don\'t* match search string'
2485
2684
  c.switch [:not], default_value: false, negatable: false
2486
2685
 
2487
2686
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
2488
2687
  c.arg_name 'TYPE'
2489
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2688
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2490
2689
 
2491
2690
  c.desc 'Archive entries older than date
2492
2691
  (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
@@ -2532,6 +2731,9 @@ command %i[archive move] do |c|
2532
2731
  end
2533
2732
 
2534
2733
  desc 'Move entries to archive file'
2734
+ long_desc 'As your doing file grows, commands can get slow. Given that your historical data (and your archive section)
2735
+ probably aren\'t providing any useful insights a year later, use this command to "rotate" old entries out to an archive
2736
+ file. You\'ll still have access to all historical data, but it won\'t be slowing down daily operation.'
2535
2737
  command :rotate do |c|
2536
2738
  c.example 'doing rotate', desc: 'Move all entries in doing file to a dated secondary file'
2537
2739
  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'
@@ -2545,13 +2747,13 @@ command :rotate do |c|
2545
2747
  c.arg_name 'SECTION_NAME'
2546
2748
  c.flag %i[s section], default_value: 'All'
2547
2749
 
2548
- c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
2750
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands.'
2549
2751
  c.arg_name 'TAG'
2550
2752
  c.flag [:tag]
2551
2753
 
2552
- c.desc 'Tag boolean (AND|OR|NOT)'
2754
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
2553
2755
  c.arg_name 'BOOLEAN'
2554
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
2756
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2555
2757
 
2556
2758
  c.desc 'Search filter'
2557
2759
  c.arg_name 'QUERY'
@@ -2561,14 +2763,14 @@ command :rotate do |c|
2561
2763
  # c.switch [:fuzzy], default_value: false, negatable: false
2562
2764
 
2563
2765
  c.desc 'Force exact search string matching (case sensitive)'
2564
- c.switch %i[x exact], default_value: false, negatable: false
2766
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
2565
2767
 
2566
2768
  c.desc 'Rotate items that *don\'t* match search string or tag filter'
2567
2769
  c.switch [:not], default_value: false, negatable: false
2568
2770
 
2569
2771
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
2570
2772
  c.arg_name 'TYPE'
2571
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2773
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2572
2774
 
2573
2775
  c.desc 'Rotate entries older than date
2574
2776
  (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
@@ -2598,8 +2800,10 @@ command :rotate do |c|
2598
2800
  end
2599
2801
 
2600
2802
  desc 'Open the "doing" file in an editor'
2601
- long_desc "`doing open` defaults to using the editor_app setting in #{config.config_file} (#{settings.key?('editor_app') ? settings['editor_app'] : 'not set'})."
2803
+ long_desc "`doing open` defaults to using the editors->doing_file setting
2804
+ in #{config.config_file} (#{Doing::Util.find_default_editor('doing_file')})."
2602
2805
  command :open do |c|
2806
+ c.example 'doing open', desc: 'Open the doing file in the default editor'
2603
2807
  c.desc 'Open with editor command (e.g. vim, mate)'
2604
2808
  c.arg_name 'COMMAND'
2605
2809
  c.flag %i[e editor]
@@ -2773,7 +2977,7 @@ command :config do |c|
2773
2977
  c.command :undo do |undo|
2774
2978
  undo.action do |_global, options, args|
2775
2979
  config_file = config.choose_config
2776
- wwid.restore_backup(config_file)
2980
+ Doing::Util::Backup.restore_last_backup(config_file, count: 1)
2777
2981
  end
2778
2982
  end
2779
2983
 
@@ -2845,9 +3049,21 @@ command :config do |c|
2845
3049
 
2846
3050
  value = options[:remove] ? nil : args.pop
2847
3051
  keypath = args.join('.')
2848
- old_value = config.value_for_key(keypath).map { |k, v| v.to_s }
2849
- real_path = config.resolve_key_path(keypath)
2850
- raise InvalidArgument, 'Invalid key path' if real_path.empty?
3052
+ real_path = config.resolve_key_path(keypath, create: true)
3053
+
3054
+ old_value = settings.dig(*real_path) || nil
3055
+ old_type = old_value&.class.to_s || nil
3056
+
3057
+ if old_value.is_a?(Hash) && !options[:remove]
3058
+ Doing.logger.log_now(:warn, 'Config:', "Config key must point to a single value, #{real_path.join('->').boldwhite} is a mapping")
3059
+ didyou = 'Did you mean:'
3060
+ old_value.keys.each do |k|
3061
+ Doing.logger.log_now(:warn, "#{didyou}", "#{keypath}.#{k}?")
3062
+ didyou = '..........or:'
3063
+ end
3064
+ raise InvalidArgument, 'Config value is a mapping, can not be set to a single value'
3065
+
3066
+ end
2851
3067
 
2852
3068
  config_file = config.choose_config
2853
3069
  cfg = YAML.safe_load_file(config_file) || {}
@@ -2858,11 +3074,11 @@ command :config do |c|
2858
3074
  cfg.deep_set(real_path, nil)
2859
3075
  $stderr.puts "#{'Deleting key:'.yellow} #{real_path.join('->').boldwhite}"
2860
3076
  else
2861
- old_value = cfg.dig(*real_path) || 'empty'
2862
- cfg.deep_set(real_path, value.set_type)
3077
+ cfg.deep_set(real_path, value.set_type(old_type))
3078
+
2863
3079
  $stderr.puts "#{'Key path:'.yellow} #{real_path.join('->').boldwhite}"
2864
- $stderr.puts "#{'Previous:'.yellow} #{old_value.to_s.boldwhite}"
2865
- $stderr.puts "#{' New:'.yellow} #{value.set_type.to_s.boldwhite}"
3080
+ $stderr.puts "#{'Previous:'.yellow} #{(old_value ? old_value.to_s : 'empty').boldwhite}"
3081
+ $stderr.puts "#{' New:'.yellow} #{value.set_type(old_type).to_s.boldwhite}"
2866
3082
  end
2867
3083
 
2868
3084
  res = Doing::Prompt.yn('Update selected config', default_response: true)
@@ -2875,15 +3091,89 @@ command :config do |c|
2875
3091
  end
2876
3092
  end
2877
3093
 
2878
- desc 'Undo the last change to the Doing file'
3094
+ desc 'Undo the last X changes to the Doing file'
3095
+ long_desc 'Reverts the last X commands that altered the doing file.
3096
+ All changes performed by a single command are undone at once.
3097
+
3098
+ Specify a number to jump back multiple revisions, or use --select for an interactive menu.'
3099
+ arg_name 'COUNT'
2879
3100
  command :undo do |c|
3101
+ c.example 'doing undo', desc: 'Undo the most recent change to the doing file'
3102
+ c.example 'doing undo 5', desc: 'Undo the last 5 changes to the doing file'
3103
+ c.example 'doing undo --interactive', desc: 'Select from a menu of available revisions'
3104
+ c.example 'doing undo --redo', desc: 'Undo the last undo command'
3105
+
2880
3106
  c.desc 'Specify alternate doing file'
2881
3107
  c.arg_name 'PATH'
2882
3108
  c.flag %i[f file], default_value: wwid.doing_file
2883
3109
 
2884
- c.action do |_global_options, options, _args|
3110
+ c.desc 'Select from recent backups'
3111
+ c.switch %i[i interactive], negatable: false
3112
+
3113
+ c.desc 'Remove old backups, retaining X files'
3114
+ c.arg_name 'COUNT'
3115
+ c.flag %i[p prune], type: Integer
3116
+
3117
+ c.desc 'Redo last undo. Note: you cannot undo a redo.'
3118
+ c.switch %i[r redo]
3119
+
3120
+ c.action do |_global_options, options, args|
3121
+ file = options[:file] || wwid.doing_file
3122
+ count = args.empty? ? 1 : args[0].to_i
3123
+ raise InvalidArgument, "Invalid count specified for undo" unless count&.positive?
3124
+
3125
+ if options[:prune]
3126
+ Doing::Util::Backup.prune_backups(file, options[:prune])
3127
+ elsif options[:redo]
3128
+ if options[:interactive]
3129
+ Doing::Util::Backup.select_redo(file)
3130
+ else
3131
+ Doing::Util::Backup.redo_backup(file, count: count)
3132
+ end
3133
+ else
3134
+ if options[:interactive]
3135
+ Doing::Util::Backup.select_backup(file)
3136
+ else
3137
+ Doing::Util::Backup.restore_last_backup(file, count: count)
3138
+ end
3139
+ end
3140
+ end
3141
+ end
3142
+
3143
+ long_desc 'Shortcut for `doing undo -r`, reverses the last undo command. You cannot undo a redo.'
3144
+ arg_name 'COUNT'
3145
+ command :redo do |c|
3146
+ c.desc 'Specify alternate doing file'
3147
+ c.arg_name 'PATH'
3148
+ c.flag %i[f file], default_value: wwid.doing_file
3149
+
3150
+ c.desc 'Select from an interactive menu'
3151
+ c.switch %i[i interactive]
3152
+
3153
+ c.action do |_global, options, args|
2885
3154
  file = options[:file] || wwid.doing_file
2886
- wwid.restore_backup(file)
3155
+ count = args.empty? ? 1 : args[0].to_i
3156
+ raise InvalidArgument, "Invalid count specified for redo" unless count&.positive?
3157
+ if options[:interactive]
3158
+ Doing::Util::Backup.select_redo(file)
3159
+ else
3160
+ Doing::Util::Backup.redo_backup(file, count: count)
3161
+ end
3162
+ end
3163
+ end
3164
+
3165
+ desc 'List recent changes in Doing'
3166
+ long_desc 'Display a formatted list of changes in recent versions, latest at the top'
3167
+ command %i[changelog changes] do |c|
3168
+ c.action do |_global_options, options, args|
3169
+ changelog = File.expand_path(File.join(File.dirname(__FILE__), '..', 'CHANGELOG.md'))
3170
+ if File.exist?(changelog)
3171
+ parsed = TTY::Markdown.parse(IO.read(changelog), width: 80, symbols: {override: {bullet: "•"}})
3172
+ Doing::Pager.paginate = true
3173
+ Doing::Pager.page parsed
3174
+ else
3175
+ raise "Error locating changelog"
3176
+ end
2887
3177
  end
2888
3178
  end
2889
3179
 
@@ -2891,6 +3181,10 @@ desc 'Import entries from an external source'
2891
3181
  long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
2892
3182
  arg_name 'PATH'
2893
3183
  command :import do |c|
3184
+ c.example 'doing import --type timing "~/Desktop/All Activities.json"', desc: 'Import a Timing.app JSON report'
3185
+ 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'
3186
+ 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'
3187
+
2894
3188
  c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
2895
3189
  c.arg_name 'TYPE'
2896
3190
  c.flag :type, default_value: 'doing'
@@ -2903,14 +3197,14 @@ command :import do |c|
2903
3197
  # c.switch [:fuzzy], default_value: false, negatable: false
2904
3198
 
2905
3199
  c.desc 'Force exact search string matching (case sensitive)'
2906
- c.switch %i[x exact], default_value: false, negatable: false
3200
+ c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
2907
3201
 
2908
3202
  c.desc 'Import items that *don\'t* match search/tag/date filters'
2909
3203
  c.switch [:not], default_value: false, negatable: false
2910
3204
 
2911
3205
  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
2912
3206
  c.arg_name 'TYPE'
2913
- c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
3207
+ c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2914
3208
 
2915
3209
  c.desc 'Only import items with recorded time intervals'
2916
3210
  c.switch [:only_timed], default_value: false, negatable: false
@@ -2985,7 +3279,6 @@ end
2985
3279
 
2986
3280
  pre do |global, _command, _options, _args|
2987
3281
  # global[:pager] ||= settings['paginate']
2988
-
2989
3282
  Doing::Pager.paginate = global[:pager]
2990
3283
 
2991
3284
  $stdout.puts "doing v#{Doing::VERSION}" if global[:version]
@@ -3016,6 +3309,8 @@ post do |global, _command, _options, _args|
3016
3309
  # Use skips_post before a command to skip this
3017
3310
  # block on that command only
3018
3311
  Doing.logger.output_results
3312
+ Doing.logger.benchmark(:total, :finish)
3313
+ Doing.logger.log_benchmarks
3019
3314
  end
3020
3315
 
3021
3316
  around do |global, command, options, arguments, code|
@@ -3030,10 +3325,13 @@ around do |global, command, options, arguments, code|
3030
3325
 
3031
3326
  if global[:yes]
3032
3327
  Doing::Prompt.force_answer = true
3328
+ Doing.config.force_answer = true
3033
3329
  elsif global[:no]
3034
3330
  Doing::Prompt.force_answer = false
3331
+ Doing.config.force_answer = false
3035
3332
  else
3036
3333
  Doing::Prompt.default_answer = global[:default]
3334
+ Doing.config.force_answer = global[:default] ? true : false
3037
3335
  end
3038
3336
 
3039
3337
  if global[:config_file] && global[:config_file] != config.config_file
@@ -3046,13 +3344,13 @@ around do |global, command, options, arguments, code|
3046
3344
  config.config_file = cf
3047
3345
  settings = config.configure({ ignore_local: true })
3048
3346
  end
3049
-
3347
+ Doing.logger.benchmark(:init, :start)
3050
3348
  if global[:doing_file]
3051
3349
  wwid.init_doing_file(global[:doing_file])
3052
3350
  else
3053
3351
  wwid.init_doing_file
3054
3352
  end
3055
-
3353
+ Doing.logger.benchmark(:init, :finish)
3056
3354
  wwid.auto_tag = !global[:noauto]
3057
3355
 
3058
3356
  settings[:include_notes] = false unless global[:notes]