doing 2.1.3 → 2.1.4pre

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +13 -10
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +27 -0
  6. data/Gemfile.lock +23 -1
  7. data/README.md +1 -1
  8. data/bin/doing +253 -63
  9. data/doc/Array.html +1 -1
  10. data/doc/Doing/Color.html +1 -1
  11. data/doc/Doing/Completion.html +1 -1
  12. data/doc/Doing/Configuration.html +42 -1
  13. data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  14. data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  15. data/doc/Doing/Errors/DoingStandardError.html +1 -1
  16. data/doc/Doing/Errors/EmptyInput.html +1 -1
  17. data/doc/Doing/Errors/NoResults.html +1 -1
  18. data/doc/Doing/Errors/PluginException.html +1 -1
  19. data/doc/Doing/Errors/UserCancelled.html +1 -1
  20. data/doc/Doing/Errors/WrongCommand.html +1 -1
  21. data/doc/Doing/Errors.html +1 -1
  22. data/doc/Doing/Hooks.html +1 -1
  23. data/doc/Doing/Item.html +37 -3
  24. data/doc/Doing/Items.html +35 -1
  25. data/doc/Doing/LogAdapter.html +1 -1
  26. data/doc/Doing/Note.html +1 -1
  27. data/doc/Doing/Pager.html +1 -1
  28. data/doc/Doing/Plugins.html +1 -1
  29. data/doc/Doing/Prompt.html +35 -1
  30. data/doc/Doing/Section.html +1 -1
  31. data/doc/Doing/Util.html +16 -4
  32. data/doc/Doing/WWID.html +131 -71
  33. data/doc/Doing.html +3 -3
  34. data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  35. data/doc/GLI/Commands.html +1 -1
  36. data/doc/GLI.html +1 -1
  37. data/doc/Hash.html +1 -1
  38. data/doc/Status.html +1 -1
  39. data/doc/String.html +104 -2
  40. data/doc/Symbol.html +1 -1
  41. data/doc/Time.html +70 -2
  42. data/doc/_index.html +125 -4
  43. data/doc/class_list.html +1 -1
  44. data/doc/file.README.html +2 -2
  45. data/doc/index.html +2 -2
  46. data/doc/method_list.html +480 -144
  47. data/doc/top-level-namespace.html +2 -2
  48. data/doing.gemspec +2 -0
  49. data/doing.rdoc +155 -66
  50. data/lib/doing/boolean_term_parser.rb +86 -0
  51. data/lib/doing/configuration.rb +13 -4
  52. data/lib/doing/item.rb +94 -8
  53. data/lib/doing/items.rb +6 -0
  54. data/lib/doing/phrase_parser.rb +124 -0
  55. data/lib/doing/prompt.rb +8 -0
  56. data/lib/doing/string.rb +16 -2
  57. data/lib/doing/string_chronify.rb +5 -1
  58. data/lib/doing/time.rb +32 -0
  59. data/lib/doing/util.rb +2 -5
  60. data/lib/doing/util_backup.rb +235 -0
  61. data/lib/doing/version.rb +1 -1
  62. data/lib/doing/wwid.rb +81 -26
  63. data/lib/doing.rb +6 -0
  64. metadata +47 -4
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)
@@ -195,7 +195,16 @@ command %i[now next] do |c|
195
195
  end
196
196
 
197
197
  desc 'Reset the start time of an entry'
198
+ long_desc 'Update the start time of the last entry or the last entry matching a tag/search filter.
199
+ If no argument is provided, the start time will be reset to the current time.
200
+ If a date string is provided as an argument, the start time will be set to the parsed result.'
201
+ arg_name 'DATE_STRING'
198
202
  command %i[reset begin] do |c|
203
+ c.example 'doing reset', desc: 'Reset the start time of the last entry to the current time'
204
+ c.example 'doing reset --tag project1', desc: 'Reset the start time of the most recent entry tagged @project1 to the current time'
205
+ c.example 'doing reset 3pm', desc: 'Reset the start time of the last entry to 3pm of the current day'
206
+ c.example 'doing begin --tag todo --resume', desc: 'alias for reset. Updates the last @todo entry to the current time, removing @done tag.'
207
+
199
208
  c.desc 'Limit search to section'
200
209
  c.arg_name 'NAME'
201
210
  c.flag %i[s section], default_value: 'All'
@@ -203,7 +212,7 @@ command %i[reset begin] do |c|
203
212
  c.desc 'Resume entry (remove @done)'
204
213
  c.switch %i[r resume], default_value: true
205
214
 
206
- c.desc 'Reset last entry matching tag'
215
+ c.desc 'Reset last entry matching tag. Wildcards allowed (*, ?).'
207
216
  c.arg_name 'TAG'
208
217
  c.flag [:tag]
209
218
 
@@ -226,12 +235,19 @@ command %i[reset begin] do |c|
226
235
 
227
236
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
228
237
  c.arg_name 'BOOLEAN'
229
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
238
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
230
239
 
231
240
  c.desc 'Select from a menu of matching entries'
232
241
  c.switch %i[i interactive], negatable: false, default_value: false
233
242
 
234
243
  c.action do |global_options, options, args|
244
+ if args.count > 0
245
+ reset_date = args.join(' ').chronify(guess: :begin)
246
+ raise InvalidArgument, 'Invalid date string' unless reset_date
247
+ else
248
+ reset_date = Time.now
249
+ end
250
+
235
251
  options[:fuzzy] = false
236
252
  if options[:section]
237
253
  options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
@@ -259,7 +275,7 @@ command %i[reset begin] do |c|
259
275
  sort: false,
260
276
  show_if_single: true)
261
277
  else
262
- last_entry = items.last
278
+ last_entry = items.reverse.last
263
279
  end
264
280
 
265
281
  unless last_entry
@@ -267,7 +283,7 @@ command %i[reset begin] do |c|
267
283
  return
268
284
  end
269
285
 
270
- wwid.reset_item(last_entry, resume: options[:resume])
286
+ wwid.reset_item(last_entry, date: reset_date, resume: options[:resume])
271
287
 
272
288
  # new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
273
289
 
@@ -301,7 +317,7 @@ command :note do |c|
301
317
  c.desc "Replace/Remove last entry's note (default append)"
302
318
  c.switch %i[r remove], negatable: false, default_value: false
303
319
 
304
- c.desc 'Add/remove note from last entry matching tag'
320
+ c.desc 'Add/remove note from last entry matching tag. Wildcards allowed (*, ?).'
305
321
  c.arg_name 'TAG'
306
322
  c.flag [:tag]
307
323
 
@@ -322,9 +338,9 @@ command :note do |c|
322
338
  c.arg_name 'TYPE'
323
339
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
324
340
 
325
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
341
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
326
342
  c.arg_name 'BOOLEAN'
327
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
343
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
328
344
 
329
345
  c.desc 'Select item for new note from a menu of matching entries'
330
346
  c.switch %i[i interactive], negatable: false, default_value: false
@@ -394,6 +410,10 @@ command :note do |c|
394
410
  end
395
411
 
396
412
  desc 'Finish any running @meanwhile tasks and optionally create a new one'
413
+ long_desc 'The @meanwhile tag allows you to have long-running entries that encompass smaller entries.
414
+ This command makes it easy to start and stop these overarching entries. Just run `doing meanwhile Starting work on this
415
+ big project` to start a @meanwhile entry, add other entries as you work on the project, then use `doing meanwhile` by
416
+ itself to mark the entry as @done.'
397
417
  arg_name 'ENTRY'
398
418
  command :meanwhile do |c|
399
419
  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 +541,19 @@ long_desc 'List all entries and select with typeahead fuzzy matching.
521
541
 
522
542
  Multiple selections are allowed, hit tab to add the highlighted entry to the
523
543
  selection, and use ctrl-a to select all visible items. Return processes the
524
- selected entries.'
544
+ selected entries.
545
+
546
+ Search in the menu by typing:
547
+
548
+ sbtrkt fuzzy-match Items that match sbtrkt
549
+
550
+ \'wild exact-match (quoted) Items that include wild
551
+
552
+ !fire inverse-exact-match Items that do not include fire'
525
553
  command :select do |c|
554
+ c.example 'doing select', desc: 'Select from all entries. A menu of available actions will be presented after confirming the selection.'
555
+ c.example 'doing select --editor', desc: 'Select entries from a menu and batch edit them in your default editor'
556
+ 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
557
  c.desc 'Select from a specific section'
527
558
  c.arg_name 'SECTION'
528
559
  c.flag %i[s section]
@@ -680,6 +711,9 @@ command :later do |c|
680
711
  end
681
712
 
682
713
  desc 'Add a completed item with @done(date). No argument finishes last entry.'
714
+ desc 'Use this command to add an entry after you\'ve already finished it. It will be immediately marked as @done.
715
+ You can modify the start and end times of the entry using the --back, --took, and --at flags, making it an easy
716
+ way to add entries in post and maintain accurate (albeit manual) time tracking.'
683
717
  arg_name 'ENTRY'
684
718
  command %i[done did] do |c|
685
719
  c.example 'doing done', desc: 'Tag the last entry @done'
@@ -890,13 +924,13 @@ command :cancel do |c|
890
924
  c.arg_name 'NAME'
891
925
  c.flag %i[s section]
892
926
 
893
- c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2)'
927
+ c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2). Wildcards allowed (*, ?).'
894
928
  c.arg_name 'TAG'
895
929
  c.flag [:tag]
896
930
 
897
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
931
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
898
932
  c.arg_name 'BOOLEAN'
899
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
933
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
900
934
 
901
935
  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
936
  c.arg_name 'QUERY'
@@ -997,7 +1031,7 @@ command :finish do |c|
997
1031
  c.flag [:at]
998
1032
 
999
1033
  c.desc 'Finish the last X entries containing TAG.
1000
- Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
1034
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
1001
1035
  c.arg_name 'TAG'
1002
1036
  c.flag [:tag]
1003
1037
 
@@ -1018,9 +1052,9 @@ command :finish do |c|
1018
1052
  c.arg_name 'TYPE'
1019
1053
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1020
1054
 
1021
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1055
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
1022
1056
  c.arg_name 'BOOLEAN'
1023
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1057
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1024
1058
 
1025
1059
  c.desc 'Remove done tag'
1026
1060
  c.switch %i[r remove], negatable: false, default_value: false
@@ -1119,7 +1153,14 @@ command :finish do |c|
1119
1153
  end
1120
1154
 
1121
1155
  desc 'Repeat last entry as new entry'
1156
+ 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
1157
  command %i[again resume] do |c|
1158
+ c.example 'doing resume', desc: 'Duplicate the most recent entry with a new start time, removing any @done tag'
1159
+ c.example 'doing again', desc: 'again is an alias for resume'
1160
+ c.example 'doing resume --editor', desc: 'Repeat the last entry, opening the new entry in the default editor'
1161
+ c.example 'doing resume --tag project1 --in Projects', desc: 'Repeat the last entry tagged @project1, creating the new entry in the Projects section'
1162
+ c.example 'doing resume --interactive', desc: 'Select the entry to repeat from a menu'
1163
+
1123
1164
  c.desc 'Get last entry from a specific section'
1124
1165
  c.arg_name 'NAME'
1125
1166
  c.flag %i[s section], default_value: 'All'
@@ -1128,7 +1169,7 @@ command %i[again resume] do |c|
1128
1169
  c.arg_name 'SECTION_NAME'
1129
1170
  c.flag [:in]
1130
1171
 
1131
- c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma.'
1172
+ c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?).'
1132
1173
  c.arg_name 'TAG'
1133
1174
  c.flag [:tag]
1134
1175
 
@@ -1150,9 +1191,9 @@ command %i[again resume] do |c|
1150
1191
  c.arg_name 'TYPE'
1151
1192
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1152
1193
 
1153
- c.desc 'Boolean used to combine multiple tags'
1194
+ c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans.'
1154
1195
  c.arg_name 'BOOLEAN'
1155
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1196
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1156
1197
 
1157
1198
  c.desc "Edit duplicated entry with #{Doing::Util.default_editor} before adding"
1158
1199
  c.switch %i[e editor], negatable: false, default_value: false
@@ -1186,6 +1227,46 @@ command %i[again resume] do |c|
1186
1227
  end
1187
1228
  end
1188
1229
 
1230
+ desc 'List all tags in the current Doing file'
1231
+ command :tags do |c|
1232
+ c.desc 'Section'
1233
+ c.arg_name 'SECTION_NAME'
1234
+ c.flag %i[s section], default_value: 'All'
1235
+
1236
+ c.desc 'Show count of occurrences'
1237
+ c.switch %i[c counts]
1238
+
1239
+ c.desc 'Sort by name or count'
1240
+ c.arg_name 'SORT_ORDER'
1241
+ c.flag %i[sort], default_value: 'name', must_match: /^(?:n(?:ame)?|c(?:ount)?)$/
1242
+
1243
+ c.desc 'Sort order (asc/desc)'
1244
+ c.arg_name 'ORDER'
1245
+ c.flag %i[o order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
1246
+
1247
+ c.action do |_global, options, args|
1248
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
1249
+
1250
+ items = wwid.content.in_section(section)
1251
+ tags = wwid.all_tags(items, counts: true)
1252
+
1253
+ if options[:sort] =~ /^n/i
1254
+ tags = tags.sort_by { |tag, count| tag }
1255
+ else
1256
+ tags = tags.sort_by { |tag, count| count }
1257
+ end
1258
+
1259
+ tags.reverse! if options[:order].normalize_order == 'desc'
1260
+
1261
+ if options[:counts]
1262
+ tags.each { |t, c| puts "#{t} (#{c})" }
1263
+ else
1264
+ tags.each { |t, c| puts "#{t}" }
1265
+ end
1266
+ end
1267
+ end
1268
+
1269
+
1189
1270
  desc 'Add tag(s) to last entry'
1190
1271
  long_desc 'Add (or remove) tags from the last entry, or from multiple entries
1191
1272
  (with `--count`), entries matching a search (with `--search`), or entries
@@ -1239,7 +1320,7 @@ command :tag do |c|
1239
1320
  c.switch %i[a autotag], negatable: false, default_value: false
1240
1321
 
1241
1322
  c.desc 'Tag the last X entries containing TAG.
1242
- Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
1323
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
1243
1324
  c.arg_name 'TAG'
1244
1325
  c.flag [:tag]
1245
1326
 
@@ -1260,9 +1341,9 @@ command :tag do |c|
1260
1341
  c.arg_name 'TYPE'
1261
1342
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1262
1343
 
1263
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1344
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
1264
1345
  c.arg_name 'BOOLEAN'
1265
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1346
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1266
1347
 
1267
1348
  c.desc 'Select item(s) to tag from a menu of matching entries'
1268
1349
  c.switch %i[i interactive], negatable: false, default_value: false
@@ -1350,8 +1431,9 @@ command :tag do |c|
1350
1431
  end
1351
1432
 
1352
1433
  desc 'Mark last entry as flagged'
1353
- command [:mark, :flag] do |c|
1434
+ command %i[mark flag] do |c|
1354
1435
  c.example 'doing flag', desc: 'Add @flagged to the last entry created'
1436
+ c.example 'doing mark', desc: 'mark is an alias for flag'
1355
1437
  c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
1356
1438
  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
1439
 
@@ -1376,7 +1458,7 @@ command [:mark, :flag] do |c|
1376
1458
  c.switch %i[u unfinished], negatable: false, default_value: false
1377
1459
 
1378
1460
  c.desc 'Flag the last entry containing TAG.
1379
- Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
1461
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
1380
1462
  c.arg_name 'TAG'
1381
1463
  c.flag [:tag]
1382
1464
 
@@ -1397,9 +1479,9 @@ command [:mark, :flag] do |c|
1397
1479
  c.arg_name 'TYPE'
1398
1480
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1399
1481
 
1400
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1482
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
1401
1483
  c.arg_name 'BOOLEAN'
1402
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1484
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1403
1485
 
1404
1486
  c.desc 'Select item(s) to flag from a menu of matching entries'
1405
1487
  c.switch %i[i interactive], negatable: false, default_value: false
@@ -1473,7 +1555,11 @@ end
1473
1555
  desc 'List all entries'
1474
1556
  long_desc %(
1475
1557
  The argument can be a section name, @tag(s) or both.
1476
- "pick" or "choose" as an argument will offer a section menu.
1558
+ "pick" or "choose" as an argument will offer a section menu. Run with `--menu` to get a menu of available tags.
1559
+
1560
+ Show tags by passing @tagname arguments. Multiple tags can be combined, and you can specify the boolean used to
1561
+ combine them with `--bool (AND|OR|NOT)`. You can also use @+tagname to require a tag to match, or @-tagname to ignore
1562
+ entries containing tagname. +/- operators require `--bool PATTERN` (which is the default).
1477
1563
  )
1478
1564
  arg_name '[SECTION|@TAGS]'
1479
1565
  command :show do |c|
@@ -1485,13 +1571,13 @@ command :show do |c|
1485
1571
  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
1572
  c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
1487
1573
 
1488
- c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
1574
+ 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
1575
  c.arg_name 'TAG'
1490
1576
  c.flag [:tag]
1491
1577
 
1492
- c.desc 'Tag boolean (AND,OR,NOT)'
1578
+ c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
1493
1579
  c.arg_name 'BOOLEAN'
1494
- c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
1580
+ c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1495
1581
 
1496
1582
  c.desc 'Max count to show'
1497
1583
  c.arg_name 'MAX'
@@ -1564,6 +1650,9 @@ command :show do |c|
1564
1650
  c.desc 'Only show items with recorded time intervals'
1565
1651
  c.switch [:only_timed], default_value: false, negatable: false
1566
1652
 
1653
+ c.desc 'Select section or tag to display from a menu'
1654
+ c.switch %i[m menu], negatable: false, default_value: false
1655
+
1567
1656
  c.desc 'Select from a menu of matching entries to perform additional operations'
1568
1657
  c.switch %i[i interactive], negatable: false, default_value: false
1569
1658
 
@@ -1576,15 +1665,17 @@ command :show do |c|
1576
1665
 
1577
1666
  tag_filter = false
1578
1667
  tags = []
1668
+
1579
1669
  if args.length.positive?
1580
1670
  case args[0]
1581
1671
  when /^all$/i
1582
1672
  section = 'All'
1583
1673
  args.shift
1584
1674
  when /^(choose|pick)$/i
1585
- section = wwid.choose_section
1675
+ section = wwid.choose_section(include_all: true)
1676
+
1586
1677
  args.shift
1587
- when /^@/
1678
+ when /^[@+-]/
1588
1679
  section = 'All'
1589
1680
  else
1590
1681
  begin
@@ -1607,7 +1698,14 @@ command :show do |c|
1607
1698
  end
1608
1699
  end
1609
1700
  else
1610
- section = settings['current_section']
1701
+ section = options[:menu] ? wwid.choose_section(include_all: true) : settings['current_section']
1702
+ end
1703
+
1704
+ if options[:menu]
1705
+ tag = wwid.choose_tag(section, include_all: true)
1706
+ raise UserCancelled unless tag
1707
+
1708
+ tags.concat(tag.split(/ +/).map { |t| t.strip.sub(/^@/, '') }) if tag =~ /^@/
1611
1709
  end
1612
1710
 
1613
1711
  tags.concat(options[:tag].to_tags) if options[:tag]
@@ -1652,11 +1750,11 @@ command :show do |c|
1652
1750
  end
1653
1751
 
1654
1752
  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.
1753
+ long_desc %(
1754
+ Search all sections (or limit to a single section) for entries matching text or regular expression. Normal strings are fuzzy matched.
1657
1755
 
1658
- To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
1659
- EODESC
1756
+ To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
1757
+ )
1660
1758
 
1661
1759
  arg_name 'SEARCH_PATTERN'
1662
1760
  command %i[grep search] do |c|
@@ -1820,6 +1918,8 @@ command :recent do |c|
1820
1918
  end
1821
1919
 
1822
1920
  desc 'List entries from today'
1921
+ long_desc 'List entries from the current day. Use --before, --after, and
1922
+ --from to specify time ranges.'
1823
1923
  command :today do |c|
1824
1924
  c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day'
1825
1925
  c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
@@ -1994,6 +2094,8 @@ command :since do |c|
1994
2094
  end
1995
2095
 
1996
2096
  desc 'List entries from yesterday'
2097
+ desc 'Show only entries with start times within the previous 24 hour period. Use --before, --after, and --from to limit to
2098
+ time spans within the day.'
1997
2099
  command :yesterday do |c|
1998
2100
  c.example 'doing yesterday', desc: 'List all entries from the previous day'
1999
2101
  c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm'
@@ -2065,6 +2167,8 @@ command :yesterday do |c|
2065
2167
  end
2066
2168
 
2067
2169
  desc 'Show the last entry, optionally edit'
2170
+ long_desc 'Shows the last entry. Using --search and --tag filters, you can view/edit the last entry matching a filter,
2171
+ allowing `doing last` to target historical entries.'
2068
2172
  command :last do |c|
2069
2173
  c.example 'doing last', desc: 'Show the most recent entry in all sections'
2070
2174
  c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section'
@@ -2081,13 +2185,13 @@ command :last do |c|
2081
2185
  c.desc "Edit entry with #{Doing::Util.default_editor}"
2082
2186
  c.switch %i[e editor], negatable: false, default_value: false
2083
2187
 
2084
- c.desc 'Tag filter, combine multiple tags with a comma.'
2188
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
2085
2189
  c.arg_name 'TAG'
2086
2190
  c.flag [:tag]
2087
2191
 
2088
- c.desc 'Tag boolean'
2192
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
2089
2193
  c.arg_name 'BOOLEAN'
2090
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
2194
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2091
2195
 
2092
2196
  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2093
2197
  c.arg_name 'QUERY'
@@ -2118,6 +2222,8 @@ command :last do |c|
2118
2222
  else
2119
2223
  tags = options[:tag].to_tags
2120
2224
  options[:bool] = case options[:bool]
2225
+ when /^p/i
2226
+ :pattern
2121
2227
  when /(any|or)/i
2122
2228
  :or
2123
2229
  when /(not|none)/i
@@ -2230,6 +2336,8 @@ command :plugins do |c|
2230
2336
  end
2231
2337
 
2232
2338
  desc 'Generate shell completion scripts'
2339
+ desc 'Generates the necessary scripts to add command line completion to various shells, so typing \'doing\' and hitting
2340
+ tab will offer completions of subcommands and their options.'
2233
2341
  command :completion do |c|
2234
2342
  c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
2235
2343
  c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
@@ -2252,7 +2360,8 @@ command :completion do |c|
2252
2360
  end
2253
2361
 
2254
2362
  desc 'Display a user-created view'
2255
- long_desc 'Command line options override view configuration'
2363
+ long_desc 'Views are defined in your configuration (use `doing config` to edit).
2364
+ Command line options override view configuration.'
2256
2365
  arg_name 'VIEW_NAME'
2257
2366
  command :view do |c|
2258
2367
  c.example 'doing view color', desc: 'Display entries according to config for view "color"'
@@ -2282,13 +2391,13 @@ command :view do |c|
2282
2391
  c.desc 'Include colors in output'
2283
2392
  c.switch [:color], default_value: true, negatable: true
2284
2393
 
2285
- c.desc 'Tag filter, combine multiple tags with a comma.'
2394
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
2286
2395
  c.arg_name 'TAG'
2287
2396
  c.flag [:tag]
2288
2397
 
2289
- c.desc 'Tag boolean (AND,OR,NOT)'
2398
+ c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
2290
2399
  c.arg_name 'BOOLEAN'
2291
- c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
2400
+ c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2292
2401
 
2293
2402
  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2294
2403
  c.arg_name 'QUERY'
@@ -2382,16 +2491,22 @@ command :view do |c|
2382
2491
  tag_filter = false
2383
2492
  if options[:tag]
2384
2493
  tag_filter = { 'tags' => [], 'bool' => 'OR' }
2385
- tag_filter['tags'] = options[:tag].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2386
- tag_filter['bool'] = options[:bool].normalize_bool
2494
+ bool = options[:bool].normalize_bool
2495
+ tag_filter['bool'] = bool
2496
+ tag_filter['tags'] = if bool == :pattern
2497
+ options[:tag]
2498
+ else
2499
+ options[:tag].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2500
+ end
2387
2501
  elsif view.key?('tags') && !(view['tags'].nil? || view['tags'].empty?)
2388
2502
  tag_filter = { 'tags' => [], 'bool' => 'OR' }
2503
+ bool = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :pattern
2504
+ tag_filter['bool'] = bool
2389
2505
  tag_filter['tags'] = if view['tags'].instance_of?(Array)
2390
- view['tags'].map(&:strip)
2506
+ bool == :pattern ? view['tags'].join(' ').strip : view['tags'].map(&:strip)
2391
2507
  else
2392
- view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2508
+ bool == :pattern ? view['tags'].strip : view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2393
2509
  end
2394
- tag_filter['bool'] = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :or
2395
2510
  end
2396
2511
 
2397
2512
  # If the -o/--output flag was specified, override any default in the view template
@@ -2500,13 +2615,13 @@ command %i[archive move] do |c|
2500
2615
  c.desc 'Label moved items with @from(SECTION_NAME)'
2501
2616
  c.switch [:label], default_value: true, negatable: true
2502
2617
 
2503
- c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
2618
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands.'
2504
2619
  c.arg_name 'TAG'
2505
2620
  c.flag [:tag]
2506
2621
 
2507
- c.desc 'Tag boolean (AND|OR|NOT)'
2622
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
2508
2623
  c.arg_name 'BOOLEAN'
2509
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
2624
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2510
2625
 
2511
2626
  c.desc 'Search filter'
2512
2627
  c.arg_name 'QUERY'
@@ -2569,6 +2684,9 @@ command %i[archive move] do |c|
2569
2684
  end
2570
2685
 
2571
2686
  desc 'Move entries to archive file'
2687
+ long_desc 'As your doing file grows, commands can get slow. Given that your historical data (and your archive section)
2688
+ probably aren\'t providing any useful insights a year later, use this command to "rotate" old entries out to an archive
2689
+ file. You\'ll still have access to all historical data, but it won\'t be slowing down daily operation.'
2572
2690
  command :rotate do |c|
2573
2691
  c.example 'doing rotate', desc: 'Move all entries in doing file to a dated secondary file'
2574
2692
  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 +2700,13 @@ command :rotate do |c|
2582
2700
  c.arg_name 'SECTION_NAME'
2583
2701
  c.flag %i[s section], default_value: 'All'
2584
2702
 
2585
- c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
2703
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands.'
2586
2704
  c.arg_name 'TAG'
2587
2705
  c.flag [:tag]
2588
2706
 
2589
- c.desc 'Tag boolean (AND|OR|NOT)'
2707
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
2590
2708
  c.arg_name 'BOOLEAN'
2591
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
2709
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2592
2710
 
2593
2711
  c.desc 'Search filter'
2594
2712
  c.arg_name 'QUERY'
@@ -2635,8 +2753,10 @@ command :rotate do |c|
2635
2753
  end
2636
2754
 
2637
2755
  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'})."
2756
+ long_desc "`doing open` defaults to using the editors->doing_file setting
2757
+ in #{config.config_file} (#{Doing::Util.find_default_editor('doing_file')})."
2639
2758
  command :open do |c|
2759
+ c.example 'doing open', desc: 'Open the doing file in the default editor'
2640
2760
  c.desc 'Open with editor command (e.g. vim, mate)'
2641
2761
  c.arg_name 'COMMAND'
2642
2762
  c.flag %i[e editor]
@@ -2885,6 +3005,7 @@ command :config do |c|
2885
3005
  real_path = config.resolve_key_path(keypath, create: true)
2886
3006
 
2887
3007
  old_value = settings.dig(*real_path) || nil
3008
+ old_type = old_value&.class.to_s || nil
2888
3009
 
2889
3010
  if old_value.is_a?(Hash) && !options[:remove]
2890
3011
  Doing.logger.log_now(:warn, 'Config:', "Config key must point to a single value, #{real_path.join('->').boldwhite} is a mapping")
@@ -2906,13 +3027,11 @@ command :config do |c|
2906
3027
  cfg.deep_set(real_path, nil)
2907
3028
  $stderr.puts "#{'Deleting key:'.yellow} #{real_path.join('->').boldwhite}"
2908
3029
  else
2909
-
2910
- p real_path
2911
- cfg.deep_set(real_path, value.set_type)
3030
+ cfg.deep_set(real_path, value.set_type(old_type))
2912
3031
 
2913
3032
  $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}"
3033
+ $stderr.puts "#{'Previous:'.yellow} #{(old_value ? old_value.to_s : 'empty').boldwhite}"
3034
+ $stderr.puts "#{' New:'.yellow} #{value.set_type(old_type).to_s.boldwhite}"
2916
3035
  end
2917
3036
 
2918
3037
  res = Doing::Prompt.yn('Update selected config', default_response: true)
@@ -2925,15 +3044,79 @@ command :config do |c|
2925
3044
  end
2926
3045
  end
2927
3046
 
2928
- desc 'Undo the last change to the Doing file'
3047
+ desc 'Undo the last X changes to the Doing file'
3048
+ long_desc 'Reverts the last X commands that altered the doing file.
3049
+ All changes performed by a single command are undone at once.
3050
+
3051
+ Specify a number to jump back multiple revisions, or use --select for an interactive menu.'
3052
+ arg_name 'COUNT'
2929
3053
  command :undo do |c|
3054
+ c.example 'doing undo', desc: 'Undo the most recent change to the doing file'
3055
+ c.example 'doing undo 5', desc: 'Undo the last 5 changes to the doing file'
3056
+ c.example 'doing undo --interactive', desc: 'Select from a menu of available revisions'
3057
+ c.example 'doing undo --redo', desc: 'Undo the last undo command'
3058
+
2930
3059
  c.desc 'Specify alternate doing file'
2931
3060
  c.arg_name 'PATH'
2932
3061
  c.flag %i[f file], default_value: wwid.doing_file
2933
3062
 
2934
- c.action do |_global_options, options, _args|
3063
+ c.desc 'Select from recent backups'
3064
+ c.switch %i[i interactive], negatable: false
3065
+
3066
+ c.desc 'Remove old backups, retaining X files'
3067
+ c.arg_name 'COUNT'
3068
+ c.flag %i[p prune], type: Integer
3069
+
3070
+ c.desc 'Redo last undo. Note: you cannot undo a redo.'
3071
+ c.switch %i[r redo]
3072
+
3073
+ c.action do |_global_options, options, args|
2935
3074
  file = options[:file] || wwid.doing_file
2936
- wwid.restore_backup(file)
3075
+ count = args.empty? ? 1 : args[0].to_i
3076
+ raise InvalidArgument, "Invalid count specified for undo" unless count&.positive?
3077
+
3078
+ if options[:prune]
3079
+ Doing::Util::Backup.prune_backups(file, options[:prune])
3080
+ elsif options[:redo]
3081
+ Doing::Util::Backup.redo_backup(file, count: count)
3082
+ else
3083
+ if options[:interactive]
3084
+ Doing::Util::Backup.select_backup(file)
3085
+ else
3086
+ Doing::Util::Backup.restore_last_backup(file, count: count)
3087
+ end
3088
+ end
3089
+ end
3090
+ end
3091
+
3092
+ long_desc 'Shortcut for `doing undo -r`, reverses the last undo command. You cannot undo a redo.'
3093
+ arg_name 'COUNT'
3094
+ command :redo do |c|
3095
+ c.desc 'Specify alternate doing file'
3096
+ c.arg_name 'PATH'
3097
+ c.flag %i[f file], default_value: wwid.doing_file
3098
+
3099
+ c.action do |_global, options, args|
3100
+ file = options[:file] || wwid.doing_file
3101
+ count = args.empty? ? 1 : args[0].to_i
3102
+ raise InvalidArgument, "Invalid count specified for redo" unless count&.positive?
3103
+
3104
+ Doing::Util::Backup.redo_backup(file, count: count)
3105
+ end
3106
+ end
3107
+
3108
+ desc 'List recent changes in Doing'
3109
+ long_desc 'Display a formatted list of changes in recent versions, latest at the top'
3110
+ command %i[changelog changes] do |c|
3111
+ c.action do |_global_options, options, args|
3112
+ changelog = File.expand_path(File.join(File.dirname(__FILE__), '..', 'CHANGELOG.md'))
3113
+ if File.exist?(changelog)
3114
+ parsed = TTY::Markdown.parse(IO.read(changelog), width: 80, symbols: {override: {bullet: "•"}})
3115
+ Doing::Pager.paginate = true
3116
+ Doing::Pager.page parsed
3117
+ else
3118
+ raise "Error locating changelog"
3119
+ end
2937
3120
  end
2938
3121
  end
2939
3122
 
@@ -2941,6 +3124,10 @@ desc 'Import entries from an external source'
2941
3124
  long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
2942
3125
  arg_name 'PATH'
2943
3126
  command :import do |c|
3127
+ c.example 'doing import --type timing "~/Desktop/All Activities.json"', desc: 'Import a Timing.app JSON report'
3128
+ 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'
3129
+ 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'
3130
+
2944
3131
  c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
2945
3132
  c.arg_name 'TYPE'
2946
3133
  c.flag :type, default_value: 'doing'
@@ -3080,10 +3267,13 @@ around do |global, command, options, arguments, code|
3080
3267
 
3081
3268
  if global[:yes]
3082
3269
  Doing::Prompt.force_answer = true
3270
+ Doing.config.force_answer = true
3083
3271
  elsif global[:no]
3084
3272
  Doing::Prompt.force_answer = false
3273
+ Doing.config.force_answer = false
3085
3274
  else
3086
3275
  Doing::Prompt.default_answer = global[:default]
3276
+ Doing.config.force_answer = global[:default] ? true : false
3087
3277
  end
3088
3278
 
3089
3279
  if global[:config_file] && global[:config_file] != config.config_file