doing 2.1.0pre → 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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +13 -9
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +42 -10
  6. data/Gemfile.lock +23 -1
  7. data/README.md +1 -1
  8. data/Rakefile +2 -0
  9. data/bin/doing +421 -156
  10. data/doc/Array.html +1 -1
  11. data/doc/Doing/Color.html +1 -1
  12. data/doc/Doing/Completion.html +1 -1
  13. data/doc/Doing/Configuration.html +81 -90
  14. data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  15. data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  16. data/doc/Doing/Errors/DoingStandardError.html +1 -1
  17. data/doc/Doing/Errors/EmptyInput.html +1 -1
  18. data/doc/Doing/Errors/NoResults.html +1 -1
  19. data/doc/Doing/Errors/PluginException.html +1 -1
  20. data/doc/Doing/Errors/UserCancelled.html +1 -1
  21. data/doc/Doing/Errors/WrongCommand.html +1 -1
  22. data/doc/Doing/Errors.html +1 -1
  23. data/doc/Doing/Hooks.html +1 -1
  24. data/doc/Doing/Item.html +84 -20
  25. data/doc/Doing/Items.html +35 -1
  26. data/doc/Doing/LogAdapter.html +1 -1
  27. data/doc/Doing/Note.html +1 -1
  28. data/doc/Doing/Pager.html +1 -1
  29. data/doc/Doing/Plugins.html +1 -1
  30. data/doc/Doing/Prompt.html +70 -18
  31. data/doc/Doing/Section.html +1 -1
  32. data/doc/Doing/Util.html +16 -4
  33. data/doc/Doing/WWID.html +27 -147
  34. data/doc/Doing.html +3 -3
  35. data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  36. data/doc/GLI/Commands.html +1 -1
  37. data/doc/GLI.html +1 -1
  38. data/doc/Hash.html +1 -1
  39. data/doc/Status.html +1 -1
  40. data/doc/String.html +344 -4
  41. data/doc/Symbol.html +1 -1
  42. data/doc/Time.html +70 -2
  43. data/doc/_index.html +125 -4
  44. data/doc/class_list.html +1 -1
  45. data/doc/file.README.html +2 -2
  46. data/doc/index.html +2 -2
  47. data/doc/method_list.html +537 -193
  48. data/doc/top-level-namespace.html +2 -2
  49. data/doing.gemspec +2 -0
  50. data/doing.rdoc +276 -75
  51. data/lib/completion/doing.bash +20 -20
  52. data/lib/doing/boolean_term_parser.rb +86 -0
  53. data/lib/doing/configuration.rb +36 -19
  54. data/lib/doing/item.rb +102 -9
  55. data/lib/doing/items.rb +6 -0
  56. data/lib/doing/phrase_parser.rb +124 -0
  57. data/lib/doing/plugins/export/template_export.rb +29 -2
  58. data/lib/doing/prompt.rb +21 -11
  59. data/lib/doing/string.rb +47 -3
  60. data/lib/doing/string_chronify.rb +85 -0
  61. data/lib/doing/time.rb +32 -0
  62. data/lib/doing/util.rb +2 -5
  63. data/lib/doing/util_backup.rb +235 -0
  64. data/lib/doing/version.rb +1 -1
  65. data/lib/doing/wwid.rb +224 -124
  66. data/lib/doing.rb +7 -0
  67. metadata +46 -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)
@@ -146,9 +146,9 @@ command %i[now next] do |c|
146
146
 
147
147
  c.action do |_global_options, options, args|
148
148
  if options[:back]
149
- date = wwid.chronify(options[:back], guess: :begin)
149
+ date = options[:back].chronify(guess: :begin)
150
150
 
151
- raise InvalidTimeExpression.new('unable to parse date string', topic: 'Date parser:') if date.nil?
151
+ raise InvalidTimeExpression.new('unable to parse date string', topic: 'Parser:') if date.nil?
152
152
  else
153
153
  date = Time.now
154
154
  end
@@ -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'
@@ -421,7 +441,7 @@ command :meanwhile do |c|
421
441
 
422
442
  c.action do |_global_options, options, args|
423
443
  if options[:back]
424
- date = wwid.chronify(options[:back], guess: :begin)
444
+ date = options[:back].chronify(guess: :begin)
425
445
 
426
446
  raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
427
447
  else
@@ -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]
@@ -548,6 +579,25 @@ command :select do |c|
548
579
  c.arg_name 'QUERY'
549
580
  c.flag %i[q query search]
550
581
 
582
+ c.desc 'Select from entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
583
+ c.arg_name 'DATE_STRING'
584
+ c.flag [:before]
585
+
586
+ c.desc 'Select from entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
587
+ c.arg_name 'DATE_STRING'
588
+ c.flag [:after]
589
+
590
+ c.desc %(
591
+ Date range to show, or a single day to filter date on.
592
+ Date range argument should be quoted. Date specifications can be natural language.
593
+ To specify a range, use "to" or "through": `doing select --from "monday 8am to friday 5pm"`.
594
+
595
+ If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
596
+ by time of day.
597
+ )
598
+ c.arg_name 'DATE_OR_RANGE'
599
+ c.flag [:from]
600
+
551
601
  c.desc 'Force exact search string matching (case sensitive)'
552
602
  c.switch %i[x exact], default_value: false, negatable: false
553
603
 
@@ -620,7 +670,7 @@ command :later do |c|
620
670
 
621
671
  c.action do |_global_options, options, args|
622
672
  if options[:back]
623
- date = wwid.chronify(options[:back], guess: :begin)
673
+ date = options[:back].chronify(guess: :begin)
624
674
  raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
625
675
  else
626
676
  date = Time.now
@@ -661,6 +711,9 @@ command :later do |c|
661
711
  end
662
712
 
663
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.'
664
717
  arg_name 'ENTRY'
665
718
  command %i[done did] do |c|
666
719
  c.example 'doing done', desc: 'Tag the last entry @done'
@@ -715,19 +768,19 @@ command %i[done did] do |c|
715
768
  donedate = nil
716
769
 
717
770
  if options[:took]
718
- took = wwid.chronify_qty(options[:took])
771
+ took = options[:took].chronify_qty
719
772
  raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
720
773
  end
721
774
 
722
775
  if options[:back]
723
- date = wwid.chronify(options[:back], guess: :begin)
776
+ date = options[:back].chronify(guess: :begin)
724
777
  raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
725
778
  else
726
779
  date = options[:took] ? Time.now - took : Time.now
727
780
  end
728
781
 
729
782
  if options[:at]
730
- finish_date = wwid.chronify(options[:at], guess: :begin)
783
+ finish_date = options[:at].chronify(guess: :begin)
731
784
  raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
732
785
 
733
786
  date = options[:took] ? finish_date - took : finish_date
@@ -871,13 +924,13 @@ command :cancel do |c|
871
924
  c.arg_name 'NAME'
872
925
  c.flag %i[s section]
873
926
 
874
- 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 (*, ?).'
875
928
  c.arg_name 'TAG'
876
929
  c.flag [:tag]
877
930
 
878
- 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.'
879
932
  c.arg_name 'BOOLEAN'
880
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
933
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
881
934
 
882
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")'
883
936
  c.arg_name 'QUERY'
@@ -978,7 +1031,7 @@ command :finish do |c|
978
1031
  c.flag [:at]
979
1032
 
980
1033
  c.desc 'Finish the last X entries containing TAG.
981
- 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 (*, ?).'
982
1035
  c.arg_name 'TAG'
983
1036
  c.flag [:tag]
984
1037
 
@@ -999,9 +1052,9 @@ command :finish do |c|
999
1052
  c.arg_name 'TYPE'
1000
1053
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1001
1054
 
1002
- 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.'
1003
1056
  c.arg_name 'BOOLEAN'
1004
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1057
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1005
1058
 
1006
1059
  c.desc 'Remove done tag'
1007
1060
  c.switch %i[r remove], negatable: false, default_value: false
@@ -1028,7 +1081,7 @@ command :finish do |c|
1028
1081
  options[:fuzzy] = false
1029
1082
  unless options[:auto]
1030
1083
  if options[:took]
1031
- took = wwid.chronify_qty(options[:took])
1084
+ took = options[:took].chronify_qty
1032
1085
  raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
1033
1086
  end
1034
1087
 
@@ -1037,21 +1090,21 @@ command :finish do |c|
1037
1090
  raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1038
1091
 
1039
1092
  if options[:at]
1040
- finish_date = wwid.chronify(options[:at], guess: :begin)
1093
+ finish_date = options[:at].chronify(guess: :begin)
1041
1094
  raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
1042
1095
 
1043
1096
  date = options[:took] ? finish_date - took : finish_date
1044
1097
  elsif options[:back]
1045
- date = wwid.chronify(options[:back])
1098
+ date = options[:back].chronify()
1046
1099
 
1047
1100
  raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
1048
- elsif options[:took]
1049
- date = wwid.chronify_qty(options[:took])
1050
1101
  else
1051
1102
  date = Time.now
1052
1103
  end
1053
1104
  end
1054
1105
 
1106
+ options[:took] = options[:took].chronify_qty if options[:took]
1107
+
1055
1108
  if options[:tag].nil?
1056
1109
  tags = []
1057
1110
  else
@@ -1091,6 +1144,7 @@ command :finish do |c|
1091
1144
  tag: tags,
1092
1145
  tag_bool: options[:bool].normalize_bool,
1093
1146
  tags: ['done'],
1147
+ took: options[:took],
1094
1148
  unfinished: options[:unfinished]
1095
1149
  }
1096
1150
 
@@ -1099,7 +1153,14 @@ command :finish do |c|
1099
1153
  end
1100
1154
 
1101
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.'
1102
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
+
1103
1164
  c.desc 'Get last entry from a specific section'
1104
1165
  c.arg_name 'NAME'
1105
1166
  c.flag %i[s section], default_value: 'All'
@@ -1108,7 +1169,7 @@ command %i[again resume] do |c|
1108
1169
  c.arg_name 'SECTION_NAME'
1109
1170
  c.flag [:in]
1110
1171
 
1111
- 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 (*, ?).'
1112
1173
  c.arg_name 'TAG'
1113
1174
  c.flag [:tag]
1114
1175
 
@@ -1130,9 +1191,9 @@ command %i[again resume] do |c|
1130
1191
  c.arg_name 'TYPE'
1131
1192
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1132
1193
 
1133
- c.desc 'Boolean used to combine multiple tags'
1194
+ c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans.'
1134
1195
  c.arg_name 'BOOLEAN'
1135
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1196
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1136
1197
 
1137
1198
  c.desc "Edit duplicated entry with #{Doing::Util.default_editor} before adding"
1138
1199
  c.switch %i[e editor], negatable: false, default_value: false
@@ -1166,6 +1227,46 @@ command %i[again resume] do |c|
1166
1227
  end
1167
1228
  end
1168
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
+
1169
1270
  desc 'Add tag(s) to last entry'
1170
1271
  long_desc 'Add (or remove) tags from the last entry, or from multiple entries
1171
1272
  (with `--count`), entries matching a search (with `--search`), or entries
@@ -1219,7 +1320,7 @@ command :tag do |c|
1219
1320
  c.switch %i[a autotag], negatable: false, default_value: false
1220
1321
 
1221
1322
  c.desc 'Tag the last X entries containing TAG.
1222
- 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 (*, ?).'
1223
1324
  c.arg_name 'TAG'
1224
1325
  c.flag [:tag]
1225
1326
 
@@ -1240,9 +1341,9 @@ command :tag do |c|
1240
1341
  c.arg_name 'TYPE'
1241
1342
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1242
1343
 
1243
- 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.'
1244
1345
  c.arg_name 'BOOLEAN'
1245
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1346
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1246
1347
 
1247
1348
  c.desc 'Select item(s) to tag from a menu of matching entries'
1248
1349
  c.switch %i[i interactive], negatable: false, default_value: false
@@ -1330,8 +1431,9 @@ command :tag do |c|
1330
1431
  end
1331
1432
 
1332
1433
  desc 'Mark last entry as flagged'
1333
- command [:mark, :flag] do |c|
1434
+ command %i[mark flag] do |c|
1334
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'
1335
1437
  c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
1336
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'
1337
1439
 
@@ -1356,7 +1458,7 @@ command [:mark, :flag] do |c|
1356
1458
  c.switch %i[u unfinished], negatable: false, default_value: false
1357
1459
 
1358
1460
  c.desc 'Flag the last entry containing TAG.
1359
- 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 (*, ?).'
1360
1462
  c.arg_name 'TAG'
1361
1463
  c.flag [:tag]
1362
1464
 
@@ -1377,9 +1479,9 @@ command [:mark, :flag] do |c|
1377
1479
  c.arg_name 'TYPE'
1378
1480
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1379
1481
 
1380
- 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.'
1381
1483
  c.arg_name 'BOOLEAN'
1382
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1484
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1383
1485
 
1384
1486
  c.desc 'Select item(s) to flag from a menu of matching entries'
1385
1487
  c.switch %i[i interactive], negatable: false, default_value: false
@@ -1453,7 +1555,11 @@ end
1453
1555
  desc 'List all entries'
1454
1556
  long_desc %(
1455
1557
  The argument can be a section name, @tag(s) or both.
1456
- "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).
1457
1563
  )
1458
1564
  arg_name '[SECTION|@TAGS]'
1459
1565
  command :show do |c|
@@ -1465,13 +1571,13 @@ command :show do |c|
1465
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.'
1466
1572
  c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
1467
1573
 
1468
- 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.'
1469
1575
  c.arg_name 'TAG'
1470
1576
  c.flag [:tag]
1471
1577
 
1472
- c.desc 'Tag boolean (AND,OR,NOT)'
1578
+ c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
1473
1579
  c.arg_name 'BOOLEAN'
1474
- 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'
1475
1581
 
1476
1582
  c.desc 'Max count to show'
1477
1583
  c.arg_name 'MAX'
@@ -1481,14 +1587,26 @@ command :show do |c|
1481
1587
  c.arg_name 'AGE'
1482
1588
  c.flag %i[a age], default_value: 'newest'
1483
1589
 
1484
- c.desc 'View entries older than date'
1590
+ c.desc 'Show entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
1485
1591
  c.arg_name 'DATE_STRING'
1486
1592
  c.flag [:before]
1487
1593
 
1488
- c.desc 'View entries newer than date'
1594
+ c.desc 'Show entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
1489
1595
  c.arg_name 'DATE_STRING'
1490
1596
  c.flag [:after]
1491
1597
 
1598
+ c.desc %(
1599
+ Date range to show, or a single day to filter date on.
1600
+ Date range argument should be quoted. Date specifications can be natural language.
1601
+ To specify a range, use "to" or "through": `doing show --from "monday 8am to friday 5pm"`.
1602
+
1603
+ If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
1604
+ by time of day.
1605
+ )
1606
+
1607
+ c.arg_name 'DATE_OR_RANGE'
1608
+ c.flag [:from]
1609
+
1492
1610
  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
1493
1611
  c.arg_name 'QUERY'
1494
1612
  c.flag [:search]
@@ -1510,17 +1628,12 @@ command :show do |c|
1510
1628
  c.arg_name 'ORDER'
1511
1629
  c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
1512
1630
 
1513
- c.desc %(
1514
- Date range to show, or a single day to filter date on.
1515
- Date range argument should be quoted. Date specifications can be natural language.
1516
- To specify a range, use "to" or "through": `doing show --from "monday to friday"`
1517
- )
1518
- c.arg_name 'DATE_OR_RANGE'
1519
- c.flag %i[f from]
1520
-
1521
1631
  c.desc 'Show time intervals on @done tasks'
1522
1632
  c.switch %i[t times], default_value: true, negatable: true
1523
1633
 
1634
+ c.desc 'Show elapsed time on entries without @done tag'
1635
+ c.switch [:duration]
1636
+
1524
1637
  c.desc 'Show intervals with totals at the end of output'
1525
1638
  c.switch [:totals], default_value: false, negatable: false
1526
1639
 
@@ -1537,6 +1650,9 @@ command :show do |c|
1537
1650
  c.desc 'Only show items with recorded time intervals'
1538
1651
  c.switch [:only_timed], default_value: false, negatable: false
1539
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
+
1540
1656
  c.desc 'Select from a menu of matching entries to perform additional operations'
1541
1657
  c.switch %i[i interactive], negatable: false, default_value: false
1542
1658
 
@@ -1549,15 +1665,17 @@ command :show do |c|
1549
1665
 
1550
1666
  tag_filter = false
1551
1667
  tags = []
1668
+
1552
1669
  if args.length.positive?
1553
1670
  case args[0]
1554
1671
  when /^all$/i
1555
1672
  section = 'All'
1556
1673
  args.shift
1557
1674
  when /^(choose|pick)$/i
1558
- section = wwid.choose_section
1675
+ section = wwid.choose_section(include_all: true)
1676
+
1559
1677
  args.shift
1560
- when /^@/
1678
+ when /^[@+-]/
1561
1679
  section = 'All'
1562
1680
  else
1563
1681
  begin
@@ -1580,7 +1698,14 @@ command :show do |c|
1580
1698
  end
1581
1699
  end
1582
1700
  else
1583
- 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 =~ /^@/
1584
1709
  end
1585
1710
 
1586
1711
  tags.concat(options[:tag].to_tags) if options[:tag]
@@ -1592,21 +1717,6 @@ command :show do |c|
1592
1717
  }
1593
1718
  end
1594
1719
 
1595
- if options[:from]
1596
-
1597
- date_string = options[:from]
1598
- if date_string =~ / (to|through|thru|(un)?til|-+) /
1599
- dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
1600
- start = wwid.chronify(dates[0], guess: :begin)
1601
- finish = wwid.chronify(dates[2], guess: :end)
1602
- else
1603
- start = wwid.chronify(date_string, guess: :begin)
1604
- finish = false
1605
- end
1606
- raise InvalidTimeExpression, 'Unrecognized date string' unless start
1607
- dates = [start, finish]
1608
- end
1609
-
1610
1720
  options[:times] = true if options[:totals]
1611
1721
 
1612
1722
  template = settings['templates']['default'].deep_merge({
@@ -1627,7 +1737,6 @@ command :show do |c|
1627
1737
  opt = options.dup
1628
1738
  opt[:sort_tags] = options[:tag_sort] =~ /^n/i
1629
1739
  opt[:count] = options[:count].to_i
1630
- opt[:date_filter] = dates
1631
1740
  opt[:highlight] = true
1632
1741
  opt[:order] = options[:sort].normalize_order
1633
1742
  opt[:section] = section
@@ -1641,11 +1750,11 @@ command :show do |c|
1641
1750
  end
1642
1751
 
1643
1752
  desc 'Search for entries'
1644
- long_desc <<~'EODESC'
1645
- 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.
1646
1755
 
1647
- To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
1648
- EODESC
1756
+ To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
1757
+ )
1649
1758
 
1650
1759
  arg_name 'SEARCH_PATTERN'
1651
1760
  command %i[grep search] do |c|
@@ -1658,14 +1767,25 @@ command %i[grep search] do |c|
1658
1767
  c.arg_name 'NAME'
1659
1768
  c.flag %i[s section], default_value: 'All'
1660
1769
 
1661
- c.desc 'Constrain search to entries older than date'
1770
+ c.desc 'Search entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
1662
1771
  c.arg_name 'DATE_STRING'
1663
1772
  c.flag [:before]
1664
1773
 
1665
- c.desc 'Constrain search to entries newer than date'
1774
+ c.desc 'Search entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
1666
1775
  c.arg_name 'DATE_STRING'
1667
1776
  c.flag [:after]
1668
1777
 
1778
+ c.desc %(
1779
+ Date range to show, or a single day to filter date on.
1780
+ Date range argument should be quoted. Date specifications can be natural language.
1781
+ To specify a range, use "to" or "through": `doing search --from "monday 8am to friday 5pm"`.
1782
+
1783
+ If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
1784
+ by time of day.
1785
+ )
1786
+ c.arg_name 'DATE_OR_RANGE'
1787
+ c.flag [:from]
1788
+
1669
1789
  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1670
1790
  c.arg_name 'FORMAT'
1671
1791
  c.flag %i[o output]
@@ -1673,6 +1793,9 @@ command %i[grep search] do |c|
1673
1793
  c.desc 'Show time intervals on @done tasks'
1674
1794
  c.switch %i[t times], default_value: true, negatable: true
1675
1795
 
1796
+ c.desc 'Show elapsed time on entries without @done tag'
1797
+ c.switch [:duration]
1798
+
1676
1799
  c.desc 'Show intervals with totals at the end of output'
1677
1800
  c.switch [:totals], default_value: false, negatable: false
1678
1801
 
@@ -1742,6 +1865,9 @@ command :recent do |c|
1742
1865
  c.desc 'Show time intervals on @done tasks'
1743
1866
  c.switch %i[t times], default_value: true, negatable: true
1744
1867
 
1868
+ c.desc 'Show elapsed time on entries without @done tag'
1869
+ c.switch [:duration]
1870
+
1745
1871
  c.desc 'Show intervals with totals at the end of output'
1746
1872
  c.switch [:totals], default_value: false, negatable: false
1747
1873
 
@@ -1781,7 +1907,8 @@ command :recent do |c|
1781
1907
  tags_color: tags_color,
1782
1908
  times: options[:times],
1783
1909
  totals: options[:totals],
1784
- interactive: options[:interactive]
1910
+ interactive: options[:interactive],
1911
+ duration: options[:duration]
1785
1912
  }
1786
1913
 
1787
1914
  Doing::Pager::page wwid.recent(count, section.cap_first, opts)
@@ -1791,6 +1918,8 @@ command :recent do |c|
1791
1918
  end
1792
1919
 
1793
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.'
1794
1923
  command :today do |c|
1795
1924
  c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day'
1796
1925
  c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
@@ -1804,6 +1933,9 @@ command :today do |c|
1804
1933
  c.desc 'Show time intervals on @done tasks'
1805
1934
  c.switch %i[t times], default_value: true, negatable: true
1806
1935
 
1936
+ c.desc 'Show elapsed time on entries without @done tag'
1937
+ c.switch [:duration]
1938
+
1807
1939
  c.desc 'Show time totals at the end of output'
1808
1940
  c.switch [:totals], default_value: false, negatable: false
1809
1941
 
@@ -1825,19 +1957,20 @@ command :today do |c|
1825
1957
  c.arg_name 'TIME_STRING'
1826
1958
  c.flag [:after]
1827
1959
 
1960
+ c.desc %(
1961
+ Time range to show `doing today --from "12pm to 4pm"`
1962
+ )
1963
+ c.arg_name 'DATE_OR_RANGE'
1964
+ c.flag [:from]
1965
+
1828
1966
  c.action do |_global_options, options, _args|
1829
1967
  raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1830
1968
 
1831
1969
  options[:times] = true if options[:totals]
1832
1970
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1833
- opt = {
1834
- after: options[:after],
1835
- before: options[:before],
1836
- section: options[:section],
1837
- sort_tags: options[:sort_tags],
1838
- totals: options[:totals]
1839
- }
1840
- Doing::Pager.page wwid.today(options[:times], options[:output], opt).chomp
1971
+ filter_options = %i[after before duration from section sort_tags totals].each_with_object({}) { |k, hsh| hsh[k] = options[k] }
1972
+
1973
+ Doing::Pager.page wwid.today(options[:times], options[:output], filter_options).chomp
1841
1974
  end
1842
1975
  end
1843
1976
 
@@ -1858,6 +1991,9 @@ command :on do |c|
1858
1991
  c.desc 'Show time intervals on @done tasks'
1859
1992
  c.switch %i[t times], default_value: true, negatable: true
1860
1993
 
1994
+ c.desc 'Show elapsed time on entries without @done tag'
1995
+ c.switch [:duration]
1996
+
1861
1997
  c.desc 'Show time totals at the end of output'
1862
1998
  c.switch [:totals], default_value: false, negatable: false
1863
1999
 
@@ -1880,10 +2016,10 @@ command :on do |c|
1880
2016
 
1881
2017
  if date_string =~ / (to|through|thru) /
1882
2018
  dates = date_string.split(/ (to|through|thru) /)
1883
- start = wwid.chronify(dates[0], guess: :begin)
1884
- finish = wwid.chronify(dates[2], guess: :end)
2019
+ start = dates[0].chronify(guess: :begin)
2020
+ finish = dates[2].chronify(guess: :end)
1885
2021
  else
1886
- start = wwid.chronify(date_string, guess: :begin)
2022
+ start = date_string.chronify(guess: :begin)
1887
2023
  finish = false
1888
2024
  end
1889
2025
 
@@ -1897,7 +2033,7 @@ command :on do |c|
1897
2033
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1898
2034
 
1899
2035
  Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
1900
- { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
2036
+ { duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1901
2037
  end
1902
2038
  end
1903
2039
 
@@ -1916,6 +2052,9 @@ command :since do |c|
1916
2052
  c.desc 'Show time intervals on @done tasks'
1917
2053
  c.switch %i[t times], default_value: true, negatable: true
1918
2054
 
2055
+ c.desc 'Show elapsed time on entries without @done tag'
2056
+ c.switch [:duration]
2057
+
1919
2058
  c.desc 'Show time totals at the end of output'
1920
2059
  c.switch [:totals], default_value: false, negatable: false
1921
2060
 
@@ -1939,7 +2078,7 @@ command :since do |c|
1939
2078
  date_string.sub!(/(day) (\d)/, '\1 at \2')
1940
2079
  date_string.sub!(/(\d+)d( ago)?/, '\1 days ago')
1941
2080
 
1942
- start = wwid.chronify(date_string, guess: :begin)
2081
+ start = date_string.chronify(guess: :begin)
1943
2082
  finish = Time.now
1944
2083
 
1945
2084
  raise InvalidTimeExpression, 'Unrecognized date string' unless start
@@ -1950,11 +2089,13 @@ command :since do |c|
1950
2089
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1951
2090
 
1952
2091
  Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
1953
- { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
2092
+ { duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1954
2093
  end
1955
2094
  end
1956
2095
 
1957
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.'
1958
2099
  command :yesterday do |c|
1959
2100
  c.example 'doing yesterday', desc: 'List all entries from the previous day'
1960
2101
  c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm'
@@ -1971,6 +2112,9 @@ command :yesterday do |c|
1971
2112
  c.desc 'Show time intervals on @done tasks'
1972
2113
  c.switch %i[t times], default_value: true, negatable: true
1973
2114
 
2115
+ c.desc 'Show elapsed time on entries without @done tag'
2116
+ c.switch [:duration]
2117
+
1974
2118
  c.desc 'Show time totals at the end of output'
1975
2119
  c.switch [:totals], default_value: false, negatable: false
1976
2120
 
@@ -1987,6 +2131,12 @@ command :yesterday do |c|
1987
2131
  c.arg_name 'TIME_STRING'
1988
2132
  c.flag [:after]
1989
2133
 
2134
+ c.desc %(
2135
+ Time range to show, e.g. `doing yesterday --from "1am to 8am"`
2136
+ )
2137
+ c.arg_name 'TIME_RANGE'
2138
+ c.flag [:from]
2139
+
1990
2140
  c.desc 'Tag sort direction (asc|desc)'
1991
2141
  c.arg_name 'DIRECTION'
1992
2142
  c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
@@ -1996,9 +2146,17 @@ command :yesterday do |c|
1996
2146
 
1997
2147
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1998
2148
 
2149
+ if options[:from]
2150
+ options[:from] = options[:from].split(/ (?:to|through|thru|(?:un)?til|-+) /).map do |time|
2151
+ "yesterday #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}"
2152
+ end.join(' to ')
2153
+ end
2154
+
1999
2155
  opt = {
2000
2156
  after: options[:after],
2001
2157
  before: options[:before],
2158
+ duration: options[:duration],
2159
+ from: options[:from],
2002
2160
  sort_tags: options[:sort_tags],
2003
2161
  tag_order: options[:tag_order].normalize_order,
2004
2162
  totals: options[:totals],
@@ -2009,6 +2167,8 @@ command :yesterday do |c|
2009
2167
  end
2010
2168
 
2011
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.'
2012
2172
  command :last do |c|
2013
2173
  c.example 'doing last', desc: 'Show the most recent entry in all sections'
2014
2174
  c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section'
@@ -2025,18 +2185,21 @@ command :last do |c|
2025
2185
  c.desc "Edit entry with #{Doing::Util.default_editor}"
2026
2186
  c.switch %i[e editor], negatable: false, default_value: false
2027
2187
 
2028
- c.desc 'Tag filter, combine multiple tags with a comma.'
2188
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
2029
2189
  c.arg_name 'TAG'
2030
2190
  c.flag [:tag]
2031
2191
 
2032
- c.desc 'Tag boolean'
2192
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
2033
2193
  c.arg_name 'BOOLEAN'
2034
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
2194
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2035
2195
 
2036
2196
  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2037
2197
  c.arg_name 'QUERY'
2038
2198
  c.flag [:search]
2039
2199
 
2200
+ c.desc 'Show elapsed time if entry is not tagged @done'
2201
+ c.switch [:duration]
2202
+
2040
2203
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
2041
2204
  # c.switch [:fuzzy], default_value: false, negatable: false
2042
2205
 
@@ -2059,6 +2222,8 @@ command :last do |c|
2059
2222
  else
2060
2223
  tags = options[:tag].to_tags
2061
2224
  options[:bool] = case options[:bool]
2225
+ when /^p/i
2226
+ :pattern
2062
2227
  when /(any|or)/i
2063
2228
  :or
2064
2229
  when /(not|none)/i
@@ -2082,7 +2247,15 @@ command :last do |c|
2082
2247
  wwid.edit_last(section: options[:section], options: { search: search, fuzzy: options[:fuzzy], case: options[:case], tag: tags, tag_bool: options[:bool], not: options[:not] })
2083
2248
  else
2084
2249
  Doing::Pager::page wwid.last(times: true, section: options[:section],
2085
- options: { search: search, fuzzy: options[:fuzzy], case: options[:case], negate: options[:not], tag: tags, tag_bool: options[:bool] }).strip
2250
+ options: {
2251
+ duration: options[:duration],
2252
+ search: search,
2253
+ fuzzy: options[:fuzzy],
2254
+ case: options[:case],
2255
+ negate: options[:not],
2256
+ tag: tags,
2257
+ tag_bool: options[:bool]
2258
+ }).strip
2086
2259
  end
2087
2260
  end
2088
2261
  end
@@ -2163,6 +2336,8 @@ command :plugins do |c|
2163
2336
  end
2164
2337
 
2165
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.'
2166
2341
  command :completion do |c|
2167
2342
  c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
2168
2343
  c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
@@ -2185,7 +2360,8 @@ command :completion do |c|
2185
2360
  end
2186
2361
 
2187
2362
  desc 'Display a user-created view'
2188
- 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.'
2189
2365
  arg_name 'VIEW_NAME'
2190
2366
  command :view do |c|
2191
2367
  c.example 'doing view color', desc: 'Display entries according to config for view "color"'
@@ -2206,19 +2382,22 @@ command :view do |c|
2206
2382
  c.desc 'Show time intervals on @done tasks'
2207
2383
  c.switch %i[t times], default_value: true, negatable: true
2208
2384
 
2385
+ c.desc 'Show elapsed time on entries without @done tag'
2386
+ c.switch [:duration]
2387
+
2209
2388
  c.desc 'Show intervals with totals at the end of output'
2210
2389
  c.switch [:totals], default_value: false, negatable: false
2211
2390
 
2212
2391
  c.desc 'Include colors in output'
2213
2392
  c.switch [:color], default_value: true, negatable: true
2214
2393
 
2215
- c.desc 'Tag filter, combine multiple tags with a comma.'
2394
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
2216
2395
  c.arg_name 'TAG'
2217
2396
  c.flag [:tag]
2218
2397
 
2219
- c.desc 'Tag boolean (AND,OR,NOT)'
2398
+ c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
2220
2399
  c.arg_name 'BOOLEAN'
2221
- 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'
2222
2401
 
2223
2402
  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2224
2403
  c.arg_name 'QUERY'
@@ -2245,14 +2424,25 @@ command :view do |c|
2245
2424
  c.arg_name 'DIRECTION'
2246
2425
  c.flag [:tag_order], must_match: REGEX_SORT_ORDER
2247
2426
 
2248
- c.desc 'View entries older than date'
2427
+ c.desc 'View entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
2249
2428
  c.arg_name 'DATE_STRING'
2250
2429
  c.flag [:before]
2251
2430
 
2252
- c.desc 'View entries newer than date'
2431
+ c.desc 'View entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
2253
2432
  c.arg_name 'DATE_STRING'
2254
2433
  c.flag [:after]
2255
2434
 
2435
+ c.desc %(
2436
+ Date range to show, or a single day to filter date on.
2437
+ Date range argument should be quoted. Date specifications can be natural language.
2438
+ To specify a range, use "to" or "through": `doing view --from "monday 8am to friday 5pm" view_name`.
2439
+
2440
+ If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
2441
+ by time of day.
2442
+ )
2443
+ c.arg_name 'DATE_OR_RANGE'
2444
+ c.flag [:from]
2445
+
2256
2446
  c.desc 'Only show items with recorded time intervals (override view settings)'
2257
2447
  c.switch [:only_timed], default_value: false, negatable: false
2258
2448
 
@@ -2301,16 +2491,22 @@ command :view do |c|
2301
2491
  tag_filter = false
2302
2492
  if options[:tag]
2303
2493
  tag_filter = { 'tags' => [], 'bool' => 'OR' }
2304
- tag_filter['tags'] = options[:tag].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2305
- 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
2306
2501
  elsif view.key?('tags') && !(view['tags'].nil? || view['tags'].empty?)
2307
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
2308
2505
  tag_filter['tags'] = if view['tags'].instance_of?(Array)
2309
- view['tags'].map(&:strip)
2506
+ bool == :pattern ? view['tags'].join(' ').strip : view['tags'].map(&:strip)
2310
2507
  else
2311
- view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2508
+ bool == :pattern ? view['tags'].strip : view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2312
2509
  end
2313
- tag_filter['bool'] = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :or
2314
2510
  end
2315
2511
 
2316
2512
  # If the -o/--output flag was specified, override any default in the view template
@@ -2346,27 +2542,8 @@ command :view do |c|
2346
2542
  else
2347
2543
  false
2348
2544
  end
2349
- if view.key?('after') && !options[:after]
2350
- options[:after] = view['after']
2351
- end
2352
2545
 
2353
- if view.key?('before') && !options[:before]
2354
- options[:before] = view['before']
2355
- end
2356
-
2357
- if view.key?('from')
2358
- date_string = view['from']
2359
- if date_string =~ / (to|through|thru|(un)?til|-+) /
2360
- dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
2361
- start = wwid.chronify(dates[0], guess: :begin)
2362
- finish = wwid.chronify(dates[2], guess: :end)
2363
- else
2364
- start = wwid.chronify(date_string, guess: :begin)
2365
- finish = false
2366
- end
2367
- raise InvalidTimeExpression, 'Unrecognized date string' unless start
2368
- dates = [start, finish]
2369
- end
2546
+ %w[before after from duration].each { |k| options[k.to_sym] = view[k] if view.key?(k) && !options[k.to_sym] }
2370
2547
 
2371
2548
  options[:case] = options[:case].normalize_case
2372
2549
 
@@ -2378,22 +2555,21 @@ command :view do |c|
2378
2555
  end
2379
2556
 
2380
2557
  opts = options.dup
2381
- opts[:search] = search
2382
- opts[:output] = output_format
2383
2558
  opts[:count] = count
2384
2559
  opts[:format] = date_format
2385
2560
  opts[:highlight] = options[:color]
2386
2561
  opts[:only_timed] = only_timed
2387
2562
  opts[:order] = order
2563
+ opts[:output] = options[:interactive] ? nil : options[:output]
2564
+ opts[:output] = output_format
2565
+ opts[:page_title] = page_title
2566
+ opts[:search] = search
2388
2567
  opts[:section] = section
2389
2568
  opts[:tag_filter] = tag_filter
2390
2569
  opts[:tag_order] = tag_order
2391
2570
  opts[:tags_color] = tags_color
2392
2571
  opts[:template] = template
2393
2572
  opts[:totals] = totals
2394
- opts[:page_title] = page_title
2395
- opts[:date_filter] = dates
2396
- opts[:output] = options[:interactive] ? nil : options[:output]
2397
2573
 
2398
2574
  Doing::Pager.page wwid.list_section(opts)
2399
2575
  elsif title.instance_of?(FalseClass)
@@ -2439,13 +2615,13 @@ command %i[archive move] do |c|
2439
2615
  c.desc 'Label moved items with @from(SECTION_NAME)'
2440
2616
  c.switch [:label], default_value: true, negatable: true
2441
2617
 
2442
- 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.'
2443
2619
  c.arg_name 'TAG'
2444
2620
  c.flag [:tag]
2445
2621
 
2446
- c.desc 'Tag boolean (AND|OR|NOT)'
2622
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
2447
2623
  c.arg_name 'BOOLEAN'
2448
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
2624
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2449
2625
 
2450
2626
  c.desc 'Search filter'
2451
2627
  c.arg_name 'QUERY'
@@ -2508,6 +2684,9 @@ command %i[archive move] do |c|
2508
2684
  end
2509
2685
 
2510
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.'
2511
2690
  command :rotate do |c|
2512
2691
  c.example 'doing rotate', desc: 'Move all entries in doing file to a dated secondary file'
2513
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'
@@ -2521,13 +2700,13 @@ command :rotate do |c|
2521
2700
  c.arg_name 'SECTION_NAME'
2522
2701
  c.flag %i[s section], default_value: 'All'
2523
2702
 
2524
- 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.'
2525
2704
  c.arg_name 'TAG'
2526
2705
  c.flag [:tag]
2527
2706
 
2528
- c.desc 'Tag boolean (AND|OR|NOT)'
2707
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
2529
2708
  c.arg_name 'BOOLEAN'
2530
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
2709
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2531
2710
 
2532
2711
  c.desc 'Search filter'
2533
2712
  c.arg_name 'QUERY'
@@ -2574,8 +2753,10 @@ command :rotate do |c|
2574
2753
  end
2575
2754
 
2576
2755
  desc 'Open the "doing" file in an editor'
2577
- long_desc "`doing open` defaults to using the editor_app setting in #{config.config_file} (#{settings.key?('editor_app') ? settings['editor_app'] : 'not set'})."
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')})."
2578
2758
  command :open do |c|
2759
+ c.example 'doing open', desc: 'Open the doing file in the default editor'
2579
2760
  c.desc 'Open with editor command (e.g. vim, mate)'
2580
2761
  c.arg_name 'COMMAND'
2581
2762
  c.flag %i[e editor]
@@ -2597,7 +2778,7 @@ command :open do |c|
2597
2778
  end
2598
2779
 
2599
2780
  if options[:editor]
2600
- raise MissingEditor, "Editor #{options[:editor]} not found" unless Doing::Util.exec_available(options[:editor])
2781
+ raise MissingEditor, "Editor #{options[:editor]} not found" unless Doing::Util.exec_available(options[:editor].split(/ /).first)
2601
2782
 
2602
2783
  editor = TTY::Which.which(options[:editor])
2603
2784
  system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
@@ -2608,7 +2789,7 @@ command :open do |c|
2608
2789
  system %(open -b "#{options[:bundle_id]}" "#{File.expand_path(wwid.doing_file)}")
2609
2790
  elsif Doing::Util.find_default_editor('doing_file')
2610
2791
  editor = Doing::Util.find_default_editor('doing_file')
2611
- if Doing::Util.exec_available(editor)
2792
+ if Doing::Util.exec_available(editor.split(/ /).first)
2612
2793
  system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
2613
2794
  else
2614
2795
  system %(open -a "#{editor}" "#{File.expand_path(wwid.doing_file)}")
@@ -2703,7 +2884,7 @@ command :config do |c|
2703
2884
  if options[:default]
2704
2885
  editor = Doing::Util.find_default_editor('config')
2705
2886
  if editor
2706
- if Doing::Util.exec_available(editor)
2887
+ if Doing::Util.exec_available(editor.split(/ /).first)
2707
2888
  system %(#{editor} "#{config_file}")
2708
2889
  else
2709
2890
  `open -a "#{editor}" "#{config_file}"`
@@ -2722,7 +2903,7 @@ command :config do |c|
2722
2903
 
2723
2904
  raise MissingEditor, 'No viable editor defined in config or environment' unless editor
2724
2905
 
2725
- if Doing::Util.exec_available(editor)
2906
+ if Doing::Util.exec_available(editor.split(/ /).first)
2726
2907
  system %(#{editor} "#{config_file}")
2727
2908
  else
2728
2909
  `open -a "#{editor}" "#{config_file}"`
@@ -2730,7 +2911,7 @@ command :config do |c|
2730
2911
  end
2731
2912
  else
2732
2913
  editor = options[:editor] || Doing::Util.default_editor
2733
- raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
2914
+ raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor.split(/ /).first)
2734
2915
 
2735
2916
  system %(#{editor} "#{config_file}")
2736
2917
  end
@@ -2821,9 +3002,21 @@ command :config do |c|
2821
3002
 
2822
3003
  value = options[:remove] ? nil : args.pop
2823
3004
  keypath = args.join('.')
2824
- old_value = config.value_for_key(keypath).map { |k, v| v.to_s }
2825
- real_path = config.resolve_key_path(keypath)
2826
- raise InvalidArgument, 'Invalid key path' if real_path.empty?
3005
+ real_path = config.resolve_key_path(keypath, create: true)
3006
+
3007
+ old_value = settings.dig(*real_path) || nil
3008
+ old_type = old_value&.class.to_s || nil
3009
+
3010
+ if old_value.is_a?(Hash) && !options[:remove]
3011
+ Doing.logger.log_now(:warn, 'Config:', "Config key must point to a single value, #{real_path.join('->').boldwhite} is a mapping")
3012
+ didyou = 'Did you mean:'
3013
+ old_value.keys.each do |k|
3014
+ Doing.logger.log_now(:warn, "#{didyou}", "#{keypath}.#{k}?")
3015
+ didyou = '..........or:'
3016
+ end
3017
+ raise InvalidArgument, 'Config value is a mapping, can not be set to a single value'
3018
+
3019
+ end
2827
3020
 
2828
3021
  config_file = config.choose_config
2829
3022
  cfg = YAML.safe_load_file(config_file) || {}
@@ -2834,11 +3027,11 @@ command :config do |c|
2834
3027
  cfg.deep_set(real_path, nil)
2835
3028
  $stderr.puts "#{'Deleting key:'.yellow} #{real_path.join('->').boldwhite}"
2836
3029
  else
2837
- old_value = cfg.dig(*real_path) || 'empty'
2838
- cfg.deep_set(real_path, value.set_type)
3030
+ cfg.deep_set(real_path, value.set_type(old_type))
3031
+
2839
3032
  $stderr.puts "#{'Key path:'.yellow} #{real_path.join('->').boldwhite}"
2840
- $stderr.puts "#{'Previous:'.yellow} #{old_value.to_s.boldwhite}"
2841
- $stderr.puts "#{' New:'.yellow} #{value.set_type.to_s.boldwhite}"
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}"
2842
3035
  end
2843
3036
 
2844
3037
  res = Doing::Prompt.yn('Update selected config', default_response: true)
@@ -2851,15 +3044,79 @@ command :config do |c|
2851
3044
  end
2852
3045
  end
2853
3046
 
2854
- 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'
2855
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
+
2856
3059
  c.desc 'Specify alternate doing file'
2857
3060
  c.arg_name 'PATH'
2858
3061
  c.flag %i[f file], default_value: wwid.doing_file
2859
3062
 
2860
- 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|
3074
+ file = options[:file] || wwid.doing_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|
2861
3100
  file = options[:file] || wwid.doing_file
2862
- wwid.restore_backup(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
2863
3120
  end
2864
3121
  end
2865
3122
 
@@ -2867,6 +3124,10 @@ desc 'Import entries from an external source'
2867
3124
  long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
2868
3125
  arg_name 'PATH'
2869
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
+
2870
3131
  c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
2871
3132
  c.arg_name 'TYPE'
2872
3133
  c.flag :type, default_value: 'doing'
@@ -2906,6 +3167,7 @@ command :import do |c|
2906
3167
  c.arg_name 'PREFIX'
2907
3168
  c.flag :prefix
2908
3169
 
3170
+ # TODO: Allow time range filtering
2909
3171
  c.desc 'Import entries older than date'
2910
3172
  c.arg_name 'DATE_STRING'
2911
3173
  c.flag [:before]
@@ -2935,10 +3197,10 @@ command :import do |c|
2935
3197
  date_string = options[:from]
2936
3198
  if date_string =~ / (to|through|thru|(un)?til|-+) /
2937
3199
  dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
2938
- start = wwid.chronify(dates[0], guess: :begin)
2939
- finish = wwid.chronify(dates[2], guess: :end)
3200
+ start = dates[0].chronify(guess: :begin)
3201
+ finish = dates[2].chronify(guess: :end)
2940
3202
  else
2941
- start = wwid.chronify(date_string, guess: :begin)
3203
+ start = date_string.chronify(guess: :begin)
2942
3204
  finish = false
2943
3205
  end
2944
3206
  raise InvalidTimeExpression, 'Unrecognized date string' unless start
@@ -3005,10 +3267,13 @@ around do |global, command, options, arguments, code|
3005
3267
 
3006
3268
  if global[:yes]
3007
3269
  Doing::Prompt.force_answer = true
3270
+ Doing.config.force_answer = true
3008
3271
  elsif global[:no]
3009
3272
  Doing::Prompt.force_answer = false
3273
+ Doing.config.force_answer = false
3010
3274
  else
3011
3275
  Doing::Prompt.default_answer = global[:default]
3276
+ Doing.config.force_answer = global[:default] ? true : false
3012
3277
  end
3013
3278
 
3014
3279
  if global[:config_file] && global[:config_file] != config.config_file