doing 2.1.12 → 2.1.16

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +1 -0
  3. data/.yardoc/checksums +16 -14
  4. data/.yardoc/object_types +0 -0
  5. data/.yardoc/objects/root.dat +0 -0
  6. data/CHANGELOG.md +67 -0
  7. data/Gemfile.lock +9 -2
  8. data/README.md +56 -19
  9. data/bin/doing +317 -113
  10. data/docs/doc/Array.html +117 -3
  11. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  12. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  13. data/docs/doc/BooleanTermParser/Query.html +1 -1
  14. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  15. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  16. data/docs/doc/BooleanTermParser.html +1 -1
  17. data/docs/doc/Doing/Color.html +1 -1
  18. data/docs/doc/Doing/Completion.html +1 -1
  19. data/docs/doc/Doing/Configuration.html +7 -4
  20. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  21. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  22. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  23. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  24. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  25. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  26. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  27. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  28. data/docs/doc/Doing/Errors.html +1 -1
  29. data/docs/doc/Doing/Hooks.html +1 -1
  30. data/docs/doc/Doing/Item.html +337 -14
  31. data/docs/doc/Doing/Items.html +66 -2
  32. data/docs/doc/Doing/LogAdapter.html +1 -1
  33. data/docs/doc/Doing/Note.html +2 -2
  34. data/docs/doc/Doing/Pager.html +1 -1
  35. data/docs/doc/Doing/Plugins.html +1 -1
  36. data/docs/doc/Doing/Prompt.html +103 -1
  37. data/docs/doc/Doing/Section.html +1 -1
  38. data/docs/doc/Doing/TemplateString.html +2 -2
  39. data/docs/doc/Doing/Util/Backup.html +84 -1
  40. data/docs/doc/Doing/Util.html +1 -1
  41. data/docs/doc/Doing/WWID.html +214 -35
  42. data/docs/doc/Doing.html +3 -3
  43. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  44. data/docs/doc/GLI/Commands.html +1 -1
  45. data/docs/doc/GLI.html +1 -1
  46. data/docs/doc/Hash.html +1 -1
  47. data/docs/doc/Numeric.html +279 -0
  48. data/docs/doc/PhraseParser/Operator.html +1 -1
  49. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  50. data/docs/doc/PhraseParser/Query.html +1 -1
  51. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  52. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  53. data/docs/doc/PhraseParser/TermClause.html +1 -1
  54. data/docs/doc/PhraseParser.html +1 -1
  55. data/docs/doc/Status.html +1 -1
  56. data/docs/doc/String.html +881 -138
  57. data/docs/doc/Symbol.html +1 -1
  58. data/docs/doc/Time.html +1 -1
  59. data/docs/doc/_index.html +14 -9
  60. data/docs/doc/class_list.html +1 -1
  61. data/docs/doc/file.README.html +41 -15
  62. data/docs/doc/index.html +41 -15
  63. data/docs/doc/method_list.html +408 -256
  64. data/docs/doc/top-level-namespace.html +2 -2
  65. data/docs/index.md +56 -19
  66. data/doing.gemspec +2 -0
  67. data/doing.rdoc +257 -48
  68. data/example_plugin.rb +2 -4
  69. data/lib/completion/_doing.zsh +31 -27
  70. data/lib/completion/doing.bash +50 -39
  71. data/lib/completion/doing.fish +37 -7
  72. data/lib/doing/array_chronify.rb +57 -0
  73. data/lib/doing/configuration.rb +4 -1
  74. data/lib/doing/item.rb +176 -0
  75. data/lib/doing/log_adapter.rb +1 -1
  76. data/lib/doing/numeric_chronify.rb +40 -0
  77. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  78. data/lib/doing/plugins/export/json_export.rb +2 -2
  79. data/lib/doing/plugins/export/template_export.rb +47 -90
  80. data/lib/doing/plugins/import/calendar_import.rb +13 -1
  81. data/lib/doing/plugins/import/doing_import.rb +12 -1
  82. data/lib/doing/plugins/import/timing_import.rb +13 -1
  83. data/lib/doing/prompt.rb +54 -1
  84. data/lib/doing/string.rb +97 -33
  85. data/lib/doing/string_chronify.rb +112 -14
  86. data/lib/doing/template_string.rb +1 -1
  87. data/lib/doing/time.rb +6 -6
  88. data/lib/doing/util_backup.rb +1 -1
  89. data/lib/doing/version.rb +1 -1
  90. data/lib/doing/wwid.rb +128 -103
  91. data/lib/doing.rb +36 -31
  92. data/lib/examples/plugins/say_export.rb +1 -4
  93. metadata +46 -2
data/bin/doing CHANGED
@@ -27,6 +27,7 @@ autocomplete_commands true
27
27
 
28
28
  REGEX_BOOL = /^(?:and|all|any|or|not|none|p(?:at(?:tern)?)?)$/i
29
29
  REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i
30
+ REGEX_VALUE_QUERY = /^(?:!)?@?(?:\S+) +(?:!?[<>=][=*]?|[$*^]=) +(?:.*?)$/
30
31
 
31
32
  InvalidExportType = Class.new(RuntimeError)
32
33
  MissingConfigFile = Class.new(RuntimeError)
@@ -69,6 +70,12 @@ if settings.dig('plugins', 'command_path')
69
70
  commands_from File.expand_path(settings.dig('plugins', 'command_path'))
70
71
  end
71
72
 
73
+ class TagArray < Array; end
74
+
75
+ accept TagArray do |value|
76
+ value.gsub(/[, ]+/, ' ').split(' ').map { |tag| tag.sub(/^@/, '')}.map(&:strip)
77
+ end
78
+
72
79
  program_desc 'A CLI for a What Was I Doing system'
73
80
  program_long_desc %(Doing uses a TaskPaper-like formatting to keep a plain text
74
81
  record of what you've been doing, complete with tag-based time tracking. The
@@ -110,7 +117,7 @@ switch %i[q quiet], default_value: false, negatable: false
110
117
  desc 'Verbose output'
111
118
  switch %i[debug], default_value: false, negatable: false
112
119
 
113
- desc 'Use a specific configuration file. Deprecated, set $DOING_CONFIG instead.'
120
+ desc 'Use a specific configuration file. Deprecated, set $DOING_CONFIG instead'
114
121
  flag [:config_file], default_value: config.config_file
115
122
 
116
123
  desc 'Specify a different doing_file'
@@ -120,7 +127,7 @@ flag %i[f doing_file]
120
127
 
121
128
  # @@again @@resume
122
129
  desc 'Repeat last entry as new entry'
123
- 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.'
130
+ 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'
124
131
  command %i[again resume] do |c|
125
132
  c.example 'doing resume', desc: 'Duplicate the most recent entry with a new start time, removing any @done tag'
126
133
  c.example 'doing again', desc: 'again is an alias for resume'
@@ -136,15 +143,19 @@ command %i[again resume] do |c|
136
143
  c.arg_name 'SECTION_NAME'
137
144
  c.flag [:in]
138
145
 
139
- c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?).'
146
+ c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?)'
140
147
  c.arg_name 'TAG'
141
- c.flag [:tag]
148
+ c.flag [:tag], type: TagArray
142
149
 
143
150
  c.desc 'Repeat last entry matching search. Surround with
144
151
  slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
145
152
  c.arg_name 'QUERY'
146
153
  c.flag [:search]
147
154
 
155
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
156
+ c.arg_name 'QUERY'
157
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
158
+
148
159
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
149
160
  # c.switch [:fuzzy], default_value: false, negatable: false
150
161
 
@@ -158,23 +169,26 @@ command %i[again resume] do |c|
158
169
  c.arg_name 'TYPE'
159
170
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
160
171
 
161
- c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans.'
172
+ c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans'
162
173
  c.arg_name 'BOOLEAN'
163
174
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
164
175
 
165
176
  c.desc "Edit duplicated entry with #{Doing::Util.default_editor} before adding"
166
177
  c.switch %i[e editor], negatable: false, default_value: false
167
178
 
168
- c.desc 'Note'
179
+ c.desc 'Add a note'
169
180
  c.arg_name 'TEXT'
170
181
  c.flag %i[n note]
171
182
 
183
+ c.desc 'Prompt for note via multi-line input'
184
+ c.switch %i[ask], negatable: false, default_value: false
185
+
172
186
  c.desc 'Select item to resume from a menu of matching entries'
173
187
  c.switch %i[i interactive], negatable: false, default_value: false
174
188
 
175
189
  c.action do |_global_options, options, _args|
176
190
  options[:fuzzy] = false
177
- tags = options[:tag].nil? ? [] : options[:tag].to_tags
191
+ tags = options[:tag].nil? ? [] : options[:tag]
178
192
 
179
193
  options[:case] = options[:case].normalize_case
180
194
 
@@ -184,6 +198,11 @@ command %i[again resume] do |c|
184
198
  options[:search] = search
185
199
  end
186
200
 
201
+ note = Doing::Note.new(options[:note])
202
+ note.add(Doing::Prompt.read_lines(prompt: 'Add a note')) if options[:ask]
203
+
204
+ options[:note] = note
205
+
187
206
  opts = options.dup
188
207
 
189
208
  opts[:tag] = tags
@@ -196,7 +215,7 @@ end
196
215
 
197
216
  # @@cancel
198
217
  desc 'End last X entries with no time tracked'
199
- long_desc 'Adds @done tag without datestamp so no elapsed time is recorded. Alias for `doing finish --no-date`.'
218
+ long_desc 'Adds @done tag without datestamp so no elapsed time is recorded. Alias for `doing finish --no-date`'
200
219
  arg_name 'COUNT'
201
220
  command :cancel do |c|
202
221
  c.example 'doing cancel', desc: 'Cancel the last entry'
@@ -209,11 +228,11 @@ command :cancel do |c|
209
228
  c.arg_name 'NAME'
210
229
  c.flag %i[s section]
211
230
 
212
- c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2). Wildcards allowed (*, ?).'
231
+ c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2). Wildcards allowed (*, ?)'
213
232
  c.arg_name 'TAG'
214
- c.flag [:tag]
233
+ c.flag [:tag], type: TagArray
215
234
 
216
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
235
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
217
236
  c.arg_name 'BOOLEAN'
218
237
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
219
238
 
@@ -221,6 +240,10 @@ command :cancel do |c|
221
240
  c.arg_name 'QUERY'
222
241
  c.flag [:search]
223
242
 
243
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
244
+ c.arg_name 'QUERY'
245
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
246
+
224
247
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
225
248
  # c.switch [:fuzzy], default_value: false, negatable: false
226
249
 
@@ -251,7 +274,7 @@ command :cancel do |c|
251
274
  if options[:tag].nil?
252
275
  tags = []
253
276
  else
254
- tags = options[:tag].to_tags
277
+ tags = options[:tag]
255
278
  end
256
279
 
257
280
  raise InvalidArgument, 'Only one argument allowed' if args.length > 1
@@ -285,7 +308,8 @@ command :cancel do |c|
285
308
  tag: tags,
286
309
  tag_bool: options[:bool].normalize_bool,
287
310
  tags: ['done'],
288
- unfinished: options[:unfinished]
311
+ unfinished: options[:unfinished],
312
+ val: options[:val]
289
313
  }
290
314
 
291
315
  wwid.tag_last(opts)
@@ -293,7 +317,7 @@ command :cancel do |c|
293
317
  end
294
318
 
295
319
  # @@done @@did
296
- desc 'Add a completed item with @done(date). No argument finishes last entry.'
320
+ desc 'Add a completed item with @done(date). No argument finishes last entry'
297
321
  long_desc 'Use this command to add an entry after you\'ve already finished it. It will be immediately marked as @done.
298
322
  You can modify the start and end times of the entry using the --back, --took, and --at flags, making it an easy
299
323
  way to add entries in post and maintain accurate (albeit manual) time tracking.'
@@ -314,19 +338,26 @@ command %i[done did] do |c|
314
338
  c.switch %i[a archive], negatable: false, default_value: false
315
339
 
316
340
  c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm).
317
- If used, ignores --back. Used with --took, backdates start date)
341
+ Used with --took, backdates start date)
318
342
  c.arg_name 'DATE_STRING'
319
- c.flag [:at]
343
+ c.flag %i[at finished]
320
344
 
321
345
  c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
322
346
  c.arg_name 'DATE_STRING'
323
347
  c.flag %i[b back started]
324
348
 
349
+ c.desc %(
350
+ Start and end times as a date/time range `doing done --from "1am to 8am"`.
351
+ Overrides other date flags.
352
+ )
353
+ c.arg_name 'TIME_RANGE'
354
+ c.flag [:from]
355
+
325
356
  c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM).
326
357
  If used without the --back option, the start date will be moved back to allow
327
358
  the completion date to be the current time.)
328
359
  c.arg_name 'INTERVAL'
329
- c.flag %i[t took]
360
+ c.flag %i[t took for]
330
361
 
331
362
  c.desc 'Section'
332
363
  c.arg_name 'NAME'
@@ -339,6 +370,9 @@ command %i[done did] do |c|
339
370
  c.arg_name 'TEXT'
340
371
  c.flag %i[n note]
341
372
 
373
+ c.desc 'Prompt for note via multi-line input'
374
+ c.switch %i[ask], negatable: false, default_value: false
375
+
342
376
  c.desc 'Finish last entry not already marked @done'
343
377
  c.switch %i[u unfinished], negatable: false, default_value: false
344
378
 
@@ -350,30 +384,41 @@ command %i[done did] do |c|
350
384
  took = 0
351
385
  donedate = nil
352
386
 
353
- if options[:took]
354
- took = options[:took].chronify_qty
355
- raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
356
- end
357
-
358
- if options[:back]
359
- date = options[:back].chronify(guess: :begin)
360
- raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
387
+ if options[:from]
388
+ date, finish_date = options[:from].split_date_range
389
+ finish_date ||= Time.now
361
390
  else
362
- date = options[:took] ? Time.now - took : Time.now
363
- end
391
+ if options[:took]
392
+ took = options[:took].chronify_qty
393
+ raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
394
+ end
364
395
 
365
- if options[:at]
366
- finish_date = options[:at].chronify(guess: :begin)
367
- raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
396
+ if options[:back]
397
+ date = options[:back].chronify(guess: :begin)
398
+ raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
399
+ else
400
+ date = options[:took] ? Time.now - took : Time.now
401
+ end
368
402
 
369
- date = options[:took] ? finish_date - took : finish_date
370
- elsif options[:took]
371
- finish_date = date + took
372
- else
373
- finish_date = Time.now
403
+ if options[:at]
404
+ finish_date = options[:at].chronify(guess: :begin)
405
+ raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
406
+
407
+ if options[:took]
408
+ date = finish_date - took
409
+ else
410
+ date ||= finish_date
411
+ end
412
+ elsif options[:took]
413
+ finish_date = date + took
414
+ else
415
+ finish_date = Time.now
416
+ end
374
417
  end
375
418
 
376
419
  if options[:date]
420
+ finish_date = wwid.verify_duration(date, finish_date) unless options[:took] || options[:from]
421
+
377
422
  donedate = finish_date.strftime('%F %R')
378
423
  end
379
424
 
@@ -383,9 +428,14 @@ command %i[done did] do |c|
383
428
  section = settings['current_section']
384
429
  end
385
430
 
431
+
386
432
  note = Doing::Note.new
387
433
  note.add(options[:note]) if options[:note]
388
434
 
435
+ if options[:ask] && !options[:editor]
436
+ note.add(Doing::Prompt.read_lines(prompt: 'Add a note'))
437
+ end
438
+
389
439
  if options[:editor]
390
440
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
391
441
  is_new = false
@@ -410,6 +460,12 @@ command %i[done did] do |c|
410
460
  raise EmptyInput, 'No content' unless input && !input.empty?
411
461
 
412
462
  d, title, note = wwid.format_input(input)
463
+
464
+ if options[:ask]
465
+ ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
466
+ note.add(ask_note) unless ask_note.empty?
467
+ end
468
+
413
469
  date = d.nil? ? date : d
414
470
  new_entry = Doing::Item.new(date, title, section, note)
415
471
  if new_entry.should_finish?
@@ -439,7 +495,6 @@ command %i[done did] do |c|
439
495
  if options[:remove]
440
496
  wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
441
497
  else
442
- note = options[:note] ? Doing::Note.new(options[:note]) : nil
443
498
  opt = {
444
499
  archive: options[:archive],
445
500
  back: finish_date,
@@ -454,12 +509,13 @@ command %i[done did] do |c|
454
509
  wwid.tag_last(opt)
455
510
  end
456
511
  elsif !args.empty?
457
- note = Doing::Note.new(options[:note])
458
512
  d, title, new_note = wwid.format_input([args.join(' '), note.strip_lines.join("\n")].join("\n"))
459
513
  date = d.nil? ? date : d
514
+ new_note.add(options[:note])
460
515
  title.chomp!
461
516
  section = 'Archive' if options[:archive]
462
517
  new_entry = Doing::Item.new(date, title, section, new_note)
518
+
463
519
  if new_entry.should_finish?
464
520
  if new_entry.should_time?
465
521
  new_entry.tag('done', value: donedate)
@@ -467,12 +523,14 @@ command %i[done did] do |c|
467
523
  new_entry.tag('done')
468
524
  end
469
525
  end
526
+
470
527
  Doing::Hooks.trigger :pre_entry_add, wwid, new_entry
471
528
  wwid.content.push(new_entry)
472
529
  Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
473
530
  wwid.write(wwid.doing_file)
474
531
  Doing.logger.info('Entry Added:', new_entry.title)
475
532
  elsif $stdin.stat.size.positive?
533
+ note = Doing::Note.new(options[:note])
476
534
  d, title, note = wwid.format_input($stdin.read.strip)
477
535
  unless d.nil?
478
536
  Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
@@ -520,7 +578,7 @@ command :finish do |c|
520
578
 
521
579
  c.desc 'Set the completed date to the start date plus XX[hmd]'
522
580
  c.arg_name 'INTERVAL'
523
- c.flag %i[t took]
581
+ c.flag %i[t took for]
524
582
 
525
583
  c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.)
526
584
  c.arg_name 'DATE_STRING'
@@ -529,12 +587,16 @@ command :finish do |c|
529
587
  c.desc 'Finish the last X entries containing TAG.
530
588
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
531
589
  c.arg_name 'TAG'
532
- c.flag [:tag]
590
+ c.flag [:tag], type: TagArray
533
591
 
534
592
  c.desc 'Finish the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
535
593
  c.arg_name 'QUERY'
536
594
  c.flag [:search]
537
595
 
596
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
597
+ c.arg_name 'QUERY'
598
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
599
+
538
600
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
539
601
  # c.switch [:fuzzy], default_value: false, negatable: false
540
602
 
@@ -548,7 +610,7 @@ command :finish do |c|
548
610
  c.arg_name 'TYPE'
549
611
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
550
612
 
551
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
613
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
552
614
  c.arg_name 'BOOLEAN'
553
615
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
554
616
 
@@ -604,7 +666,7 @@ command :finish do |c|
604
666
  if options[:tag].nil?
605
667
  tags = []
606
668
  else
607
- tags = options[:tag].to_tags
669
+ tags = options[:tag]
608
670
  end
609
671
 
610
672
  raise InvalidArgument, 'Only one argument allowed' if args.length > 1
@@ -641,7 +703,8 @@ command :finish do |c|
641
703
  tag_bool: options[:bool].normalize_bool,
642
704
  tags: ['done'],
643
705
  took: options[:took],
644
- unfinished: options[:unfinished]
706
+ unfinished: options[:unfinished],
707
+ val: options[:val]
645
708
  }
646
709
 
647
710
  wwid.tag_last(opts)
@@ -666,6 +729,9 @@ command :later do |c|
666
729
  c.arg_name 'TEXT'
667
730
  c.flag %i[n note]
668
731
 
732
+ c.desc 'Prompt for note via multi-line input'
733
+ c.switch %i[ask], negatable: false, default_value: false
734
+
669
735
  c.action do |_global_options, options, args|
670
736
  if options[:back]
671
737
  date = options[:back].chronify(guess: :begin)
@@ -674,23 +740,36 @@ command :later do |c|
674
740
  date = Time.now
675
741
  end
676
742
 
677
- if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
743
+ ask_note = options[:ask] && !options[:editor] && args.count.positive? ? Doing::Prompt.read_lines(prompt: 'Add a note') : ''
744
+
745
+ if options[:editor]
678
746
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
679
747
 
748
+ input = ''
680
749
  input += date.strftime('%F %R | ')
681
750
  input += args.empty? ? '' : args.join(' ')
751
+ input += "\n#{options[:note]}" if options[:note]
752
+ input += "\n#{ask_note}" unless ask_note.empty?
753
+
682
754
  input = wwid.fork_editor(input).strip
683
- raise EmptyInput, 'No content' unless input && !input.empty?
684
755
 
685
756
  d, title, note = wwid.format_input(input)
686
- date = d.nil? ? date : d
757
+ raise EmptyInput, 'No content' if title.empty?
758
+
687
759
  note.add(options[:note]) if options[:note]
760
+ if ask_note.empty? && options[:ask]
761
+ ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
762
+ note.add(ask_note) unless ask_note.empty?
763
+ end
764
+
765
+ date = d.nil? ? date : d
688
766
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
689
767
  wwid.write(wwid.doing_file)
690
768
  elsif !args.empty?
691
769
  d, title, note = wwid.format_input(args.join(' '))
692
770
  date = d.nil? ? date : d
693
771
  note.add(options[:note]) if options[:note]
772
+ note.add(ask_note) unless ask_note.empty?
694
773
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
695
774
  wwid.write(wwid.doing_file)
696
775
  elsif $stdin.stat.size.positive?
@@ -700,10 +779,20 @@ command :later do |c|
700
779
  date = d
701
780
  end
702
781
  note.add(options[:note]) if options[:note]
782
+ note.add(ask_note) unless ask_note.empty?
703
783
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
704
784
  wwid.write(wwid.doing_file)
705
785
  else
706
- raise EmptyInput, 'You must provide content when creating a new entry'
786
+ title = Doing::Prompt.read_line(prompt: 'Entry content')
787
+ raise EmptyInput, 'You must provide content when creating a new entry' if title.strip.empty?
788
+
789
+ note = Doing::Note.new
790
+ res = Doing::Prompt.yn('Add a note', default_response: false)
791
+ ask_note = res ? Doing::Prompt.read_lines(prompt: 'Enter note') : []
792
+ note.add(ask_note)
793
+
794
+ wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
795
+ wwid.write(wwid.doing_file)
707
796
  end
708
797
  end
709
798
  end
@@ -739,12 +828,16 @@ command %i[mark flag] do |c|
739
828
  c.desc 'Flag the last entry containing TAG.
740
829
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
741
830
  c.arg_name 'TAG'
742
- c.flag [:tag]
831
+ c.flag [:tag], type: TagArray
743
832
 
744
833
  c.desc 'Flag the last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
745
834
  c.arg_name 'QUERY'
746
835
  c.flag [:search]
747
836
 
837
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
838
+ c.arg_name 'QUERY'
839
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
840
+
748
841
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
749
842
  # c.switch [:fuzzy], default_value: false, negatable: false
750
843
 
@@ -758,7 +851,7 @@ command %i[mark flag] do |c|
758
851
  c.arg_name 'TYPE'
759
852
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
760
853
 
761
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
854
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
762
855
  c.arg_name 'BOOLEAN'
763
856
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
764
857
 
@@ -780,7 +873,7 @@ command %i[mark flag] do |c|
780
873
  if options[:tag].nil?
781
874
  search_tags = []
782
875
  else
783
- search_tags = options[:tag].to_tags
876
+ search_tags = options[:tag]
784
877
  end
785
878
 
786
879
  if options[:interactive]
@@ -862,6 +955,9 @@ command :meanwhile do |c|
862
955
  c.arg_name 'TEXT'
863
956
  c.flag %i[n note]
864
957
 
958
+ c.desc 'Prompt for note via multi-line input'
959
+ c.switch %i[ask], negatable: false, default_value: false
960
+
865
961
  c.action do |_global_options, options, args|
866
962
  if options[:back]
867
963
  date = options[:back].chronify(guess: :begin)
@@ -878,10 +974,15 @@ command :meanwhile do |c|
878
974
  end
879
975
  input = ''
880
976
 
977
+ ask_note = options[:ask] ? Doing::Prompt.read_lines(prompt: 'Add a note') : []
978
+
881
979
  if options[:editor]
882
980
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
883
981
  input += date.strftime('%F %R | ')
884
982
  input += args.join(' ') unless args.empty?
983
+ input += "\n#{options[:note]}" if options[:note]
984
+ input += "\n#{ask_note}" unless ask_note.empty?
985
+
885
986
  input = wwid.fork_editor(input).strip
886
987
  elsif !args.empty?
887
988
  input = args.join(' ')
@@ -900,10 +1001,9 @@ command :meanwhile do |c|
900
1001
  note = []
901
1002
  end
902
1003
 
903
- if options[:note]
904
- note.push(options[:note])
905
- elsif note.empty?
906
- note = nil
1004
+ unless options[:editor]
1005
+ note.add(options[:note]) if options[:note]
1006
+ note.add(ask_note) unless ask_note.empty?
907
1007
  end
908
1008
 
909
1009
  wwid.stop_start('meanwhile', { new_item: input, back: date, section: section, archive: options[:archive], note: note })
@@ -937,14 +1037,18 @@ command :note do |c|
937
1037
  c.desc "Replace/Remove last entry's note (default append)"
938
1038
  c.switch %i[r remove], negatable: false, default_value: false
939
1039
 
940
- c.desc 'Add/remove note from last entry matching tag. Wildcards allowed (*, ?).'
1040
+ c.desc 'Add/remove note from last entry matching tag. Wildcards allowed (*, ?)'
941
1041
  c.arg_name 'TAG'
942
- c.flag [:tag]
1042
+ c.flag [:tag], type: TagArray
943
1043
 
944
1044
  c.desc 'Add/remove note from last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
945
1045
  c.arg_name 'QUERY'
946
1046
  c.flag [:search]
947
1047
 
1048
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1049
+ c.arg_name 'QUERY'
1050
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1051
+
948
1052
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
949
1053
  # c.switch [:fuzzy], default_value: false, negatable: false
950
1054
 
@@ -958,13 +1062,16 @@ command :note do |c|
958
1062
  c.arg_name 'TYPE'
959
1063
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
960
1064
 
961
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
1065
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
962
1066
  c.arg_name 'BOOLEAN'
963
1067
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
964
1068
 
965
1069
  c.desc 'Select item for new note from a menu of matching entries'
966
1070
  c.switch %i[i interactive], negatable: false, default_value: false
967
1071
 
1072
+ c.desc 'Prompt for note via multi-line input'
1073
+ c.switch %i[ask], negatable: false, default_value: false
1074
+
968
1075
  c.action do |_global_options, options, args|
969
1076
  options[:fuzzy] = false
970
1077
  if options[:section]
@@ -991,8 +1098,9 @@ command :note do |c|
991
1098
 
992
1099
  last_note = last_entry.note || Doing::Note.new
993
1100
  new_note = Doing::Note.new
1101
+ ask_note = options[:ask] ? Doing::Prompt.read_lines(prompt: 'Add a note') : ''
994
1102
 
995
- if options[:editor] || (args.empty? && $stdin.stat.size.zero? && !options[:remove])
1103
+ if options[:editor] || (args.empty? && $stdin.stat.size.zero? && !options[:remove] && !options[:ask])
996
1104
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
997
1105
 
998
1106
  input = !args.empty? ? args.join(' ') : ''
@@ -1003,7 +1111,9 @@ command :note do |c|
1003
1111
  prev_input = last_entry.note || Doing::Note.new
1004
1112
  end
1005
1113
 
1114
+
1006
1115
  input = prev_input.add(input)
1116
+ input.add(ask_note) unless ask_note.empty?
1007
1117
 
1008
1118
  input = wwid.fork_editor(prev_input.strip_lines.join("\n"), message: nil).strip
1009
1119
  note = input
@@ -1014,9 +1124,12 @@ command :note do |c|
1014
1124
  elsif $stdin.stat.size.positive?
1015
1125
  new_note.add($stdin.read.strip)
1016
1126
  else
1017
- raise EmptyInput, 'You must provide content when adding a note' unless options[:remove]
1127
+ raise EmptyInput, 'You must provide content when adding a note' unless options[:remove] || !ask_note.empty?
1128
+
1018
1129
  end
1019
1130
 
1131
+ new_note.add(ask_note) unless ask_note.empty?
1132
+
1020
1133
  if last_note.equal?(new_note)
1021
1134
  Doing.logger.debug('Skipped:', 'No note change')
1022
1135
  else
@@ -1035,10 +1148,13 @@ long_desc %(Record what you're starting now, or backdate the start time using na
1035
1148
 
1036
1149
  A parenthetical at the end of the entry will be converted to a note.
1037
1150
 
1038
- Run with no argument to create a new entry using #{Doing::Util.default_editor}.)
1151
+ Run without arguments to create a new entry interactively.
1152
+
1153
+ Run with --editor to create a new entry using #{Doing::Util.default_editor}.)
1039
1154
  arg_name 'ENTRY'
1040
1155
  command %i[now next] do |c|
1041
- c.example 'doing now', desc: "Open #{Doing::Util.default_editor} to input an entry and optional note."
1156
+ c.example 'doing now', desc: 'Create a new entry with interactive prompts'
1157
+ c.example 'doing now -e', desc: "Open #{Doing::Util.default_editor} to input an entry and optional note"
1042
1158
  c.example 'doing now working on a new project', desc: 'Add a new entry at the current time'
1043
1159
  c.example 'doing now debugging @project2', desc: 'Add an entry with a tag'
1044
1160
  c.example 'doing now adding an entry (with a note)', desc: 'Parenthetical at end is converted to note'
@@ -1062,6 +1178,9 @@ command %i[now next] do |c|
1062
1178
  c.arg_name 'TEXT'
1063
1179
  c.flag %i[n note]
1064
1180
 
1181
+ c.desc 'Prompt for note via multi-line input'
1182
+ c.switch %i[ask], negatable: false, default_value: false
1183
+
1065
1184
  # c.desc "Edit entry with specified app"
1066
1185
  # c.arg_name 'editor_app'
1067
1186
  # # c.flag [:a, :app]
@@ -1081,23 +1200,32 @@ command %i[now next] do |c|
1081
1200
  options[:section] = settings['current_section']
1082
1201
  end
1083
1202
 
1084
- if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
1203
+ ask_note = options[:ask] && !options[:editor] && args.count.positive? ? Doing::Prompt.read_lines(prompt: 'Add a note') : ''
1204
+
1205
+ if options[:editor]
1085
1206
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
1086
1207
 
1087
1208
  input = date.strftime('%F %R | ')
1088
1209
  input += args.join(' ') unless args.empty?
1210
+ input += "\n#{options[:note]}" if options[:note]
1211
+ input += "\n#{ask_note}" unless ask_note.empty?
1089
1212
  input = wwid.fork_editor(input).strip
1090
1213
 
1091
- raise EmptyInput, 'No content' if input.empty?
1092
-
1093
1214
  date, title, note = wwid.format_input(input)
1094
- note.add(options[:note]) if options[:note]
1215
+ raise EmptyInput, 'No content' if title.strip.empty?
1216
+
1217
+ if ask_note.empty? && options[:ask]
1218
+ ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
1219
+ note.add(ask_note) unless ask_note.empty?
1220
+ end
1221
+
1095
1222
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1096
1223
  wwid.write(wwid.doing_file)
1097
1224
  elsif args.length.positive?
1098
1225
  d, title, note = wwid.format_input(args.join(' '))
1099
1226
  date = d.nil? ? date : d
1100
1227
  note.add(options[:note]) if options[:note]
1228
+ note.add(ask_note) unless ask_note.empty?
1101
1229
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1102
1230
  wwid.write(wwid.doing_file)
1103
1231
  elsif $stdin.stat.size.positive?
@@ -1108,10 +1236,20 @@ command %i[now next] do |c|
1108
1236
  date = d
1109
1237
  end
1110
1238
  note.add(options[:note]) if options[:note]
1239
+ note.add(ask_note) unless ask_note.empty?
1111
1240
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1112
1241
  wwid.write(wwid.doing_file)
1113
1242
  else
1114
- raise EmptyInput, 'You must provide content when creating a new entry'
1243
+ title = Doing::Prompt.read_line(prompt: 'Entry content')
1244
+ raise EmptyInput, 'You must provide content when creating a new entry' if title.strip.empty?
1245
+
1246
+ note = Doing::Note.new
1247
+ res = Doing::Prompt.yn('Add a note', default_response: false)
1248
+ ask_note = res ? Doing::Prompt.read_lines(prompt: 'Enter note') : []
1249
+ note.add(ask_note)
1250
+
1251
+ wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1252
+ wwid.write(wwid.doing_file)
1115
1253
  end
1116
1254
  end
1117
1255
  end
@@ -1135,7 +1273,7 @@ command %i[reset begin] do |c|
1135
1273
  c.desc 'Resume entry (remove @done)'
1136
1274
  c.switch %i[r resume], default_value: true
1137
1275
 
1138
- c.desc 'Reset last entry matching tag. Wildcards allowed (*, ?).'
1276
+ c.desc 'Reset last entry matching tag. Wildcards allowed (*, ?)'
1139
1277
  c.arg_name 'TAG'
1140
1278
  c.flag [:tag]
1141
1279
 
@@ -1143,6 +1281,10 @@ command %i[reset begin] do |c|
1143
1281
  c.arg_name 'QUERY'
1144
1282
  c.flag [:search]
1145
1283
 
1284
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1285
+ c.arg_name 'QUERY'
1286
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1287
+
1146
1288
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
1147
1289
  # c.switch [:fuzzy], default_value: false, negatable: false
1148
1290
 
@@ -1156,7 +1298,7 @@ command %i[reset begin] do |c|
1156
1298
  c.arg_name 'TYPE'
1157
1299
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1158
1300
 
1159
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1301
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
1160
1302
  c.arg_name 'BOOLEAN'
1161
1303
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1162
1304
 
@@ -1256,13 +1398,21 @@ command :select do |c|
1256
1398
 
1257
1399
  c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"'
1258
1400
  c.arg_name 'QUERY'
1259
- c.flag %i[q query search]
1401
+ c.flag %i[q query]
1402
+
1403
+ c.desc 'Select from entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
1404
+ c.arg_name 'QUERY'
1405
+ c.flag [:search]
1406
+
1407
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1408
+ c.arg_name 'QUERY'
1409
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1260
1410
 
1261
- 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.'
1411
+ 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'
1262
1412
  c.arg_name 'DATE_STRING'
1263
1413
  c.flag [:before]
1264
1414
 
1265
- 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.'
1415
+ 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'
1266
1416
  c.arg_name 'DATE_STRING'
1267
1417
  c.flag [:after]
1268
1418
 
@@ -1287,7 +1437,7 @@ command :select do |c|
1287
1437
  c.arg_name 'TYPE'
1288
1438
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1289
1439
 
1290
- c.desc 'Use --no-menu to skip the interactive menu. Use with --query to filter items and act on results automatically. Test with `--output doing` to preview matches.'
1440
+ c.desc 'Use --no-menu to skip the interactive menu. Use with --query to filter items and act on results automatically. Test with `--output doing` to preview matches'
1291
1441
  c.switch %i[menu], negatable: true, default_value: true
1292
1442
 
1293
1443
  c.desc 'Cancel selected items (add @done without timestamp)'
@@ -1305,7 +1455,7 @@ command :select do |c|
1305
1455
  c.desc 'Add flag to selected item(s)'
1306
1456
  c.switch %i[flag], negatable: false, default_value: false
1307
1457
 
1308
- c.desc 'Perform action without confirmation.'
1458
+ c.desc 'Perform action without confirmation'
1309
1459
  c.switch %i[force], negatable: false, default_value: false
1310
1460
 
1311
1461
  c.desc 'Save selected entries to file using --output format'
@@ -1365,6 +1515,10 @@ command :tag do |c|
1365
1515
  c.arg_name 'ORIG_TAG'
1366
1516
  c.flag %i[rename]
1367
1517
 
1518
+ c.desc 'Include a value, e.g. @tag(value)'
1519
+ c.arg_name 'VALUE'
1520
+ c.flag %i[v value]
1521
+
1368
1522
  c.desc 'Don\'t ask permission to tag all entries when count is 0'
1369
1523
  c.switch %i[force], negatable: false, default_value: false
1370
1524
 
@@ -1386,12 +1540,16 @@ command :tag do |c|
1386
1540
  c.desc 'Tag the last X entries containing TAG.
1387
1541
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
1388
1542
  c.arg_name 'TAG'
1389
- c.flag [:tag]
1543
+ c.flag [:tag], type: TagArray
1390
1544
 
1391
1545
  c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
1392
1546
  c.arg_name 'QUERY'
1393
1547
  c.flag [:search]
1394
1548
 
1549
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1550
+ c.arg_name 'QUERY'
1551
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1552
+
1395
1553
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
1396
1554
  # c.switch [:fuzzy], default_value: false, negatable: false
1397
1555
 
@@ -1405,7 +1563,7 @@ command :tag do |c|
1405
1563
  c.arg_name 'TYPE'
1406
1564
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1407
1565
 
1408
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
1566
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
1409
1567
  c.arg_name 'BOOLEAN'
1410
1568
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1411
1569
 
@@ -1414,7 +1572,7 @@ command :tag do |c|
1414
1572
 
1415
1573
  c.action do |_global_options, options, args|
1416
1574
  options[:fuzzy] = false
1417
- raise MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:autotag]
1575
+ # raise MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:autotag]
1418
1576
 
1419
1577
  raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1420
1578
 
@@ -1428,17 +1586,21 @@ command :tag do |c|
1428
1586
  if options[:tag].nil?
1429
1587
  search_tags = []
1430
1588
  else
1431
- search_tags = options[:tag].to_tags
1589
+ search_tags = options[:tag]
1432
1590
  end
1433
1591
 
1434
1592
  if options[:autotag]
1435
1593
  tags = []
1436
1594
  else
1437
- tags = if args.join('') =~ /,/
1438
- args.join('').split(/,/)
1439
- else
1440
- args.join(' ').split(' ') # in case tags are quoted as one arg
1441
- end
1595
+ if args.empty?
1596
+ tags = []
1597
+ else
1598
+ tags = if args.join('') =~ /,/
1599
+ args.join('').split(/ *, */)
1600
+ else
1601
+ args.join(' ').split(' ') # in case tags are quoted as one arg
1602
+ end
1603
+ end
1442
1604
 
1443
1605
  tags.map! { |tag| tag.sub(/^@/, '').strip }
1444
1606
  end
@@ -1528,11 +1690,11 @@ command %i[grep search] do |c|
1528
1690
  c.arg_name 'NAME'
1529
1691
  c.flag %i[s section], default_value: 'All'
1530
1692
 
1531
- 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.'
1693
+ 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'
1532
1694
  c.arg_name 'DATE_STRING'
1533
1695
  c.flag [:before]
1534
1696
 
1535
- 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.'
1697
+ 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'
1536
1698
  c.arg_name 'DATE_STRING'
1537
1699
  c.flag [:after]
1538
1700
 
@@ -1591,6 +1753,13 @@ command %i[grep search] do |c|
1591
1753
  c.desc 'Display an interactive menu of results to perform further operations'
1592
1754
  c.switch %i[i interactive], default_value: false, negatable: false
1593
1755
 
1756
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1757
+ c.arg_name 'QUERY'
1758
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1759
+
1760
+ c.desc 'Combine multiple tags or value queries using AND, OR, or NOT'
1761
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1762
+
1594
1763
  c.action do |_global_options, options, args|
1595
1764
  options[:fuzzy] = false
1596
1765
  raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
@@ -1601,6 +1770,7 @@ command %i[grep search] do |c|
1601
1770
  section = wwid.guess_section(options[:section]) if options[:section]
1602
1771
 
1603
1772
  options[:case] = options[:case].normalize_case
1773
+ options[:bool] = options[:bool].normalize_bool
1604
1774
 
1605
1775
  search = args.join(' ')
1606
1776
  search.sub!(/^'?/, "'") if options[:exact]
@@ -1639,11 +1809,11 @@ command :last do |c|
1639
1809
  c.desc "Delete the last entry"
1640
1810
  c.switch %i[d delete], negatable: false, default_value: false
1641
1811
 
1642
- c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
1812
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?)'
1643
1813
  c.arg_name 'TAG'
1644
- c.flag [:tag]
1814
+ c.flag [:tag], type: TagArray
1645
1815
 
1646
- c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
1816
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans'
1647
1817
  c.arg_name 'BOOLEAN'
1648
1818
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1649
1819
 
@@ -1651,6 +1821,10 @@ command :last do |c|
1651
1821
  c.arg_name 'QUERY'
1652
1822
  c.flag [:search]
1653
1823
 
1824
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1825
+ c.arg_name 'QUERY'
1826
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1827
+
1654
1828
  c.desc 'Show elapsed time if entry is not tagged @done'
1655
1829
  c.switch [:duration]
1656
1830
 
@@ -1674,7 +1848,7 @@ command :last do |c|
1674
1848
  if options[:tag].nil?
1675
1849
  options[:tag] = []
1676
1850
  else
1677
- options[:tag] = options[:tag].to_tags
1851
+ options[:tag] = options[:tag]
1678
1852
  options[:bool] = options[:bool].normalize_bool
1679
1853
  end
1680
1854
 
@@ -1685,12 +1859,14 @@ command :last do |c|
1685
1859
  if options[:editor]
1686
1860
  wwid.edit_last(section: options[:section],
1687
1861
  options: {
1688
- search: search,
1862
+ search: options[:search],
1689
1863
  fuzzy: options[:fuzzy],
1690
1864
  case: options[:case],
1691
- tag: tags,
1865
+ tag: options[:tag],
1692
1866
  tag_bool: options[:bool],
1693
- not: options[:not]
1867
+ not: options[:not],
1868
+ val: options[:val],
1869
+ bool: options[:bool]
1694
1870
  })
1695
1871
  else
1696
1872
  last = wwid.last(times: true, section: options[:section],
@@ -1702,7 +1878,9 @@ command :last do |c|
1702
1878
  negate: options[:not],
1703
1879
  tag: options[:tag],
1704
1880
  tag_bool: options[:bool],
1705
- delete: options[:delete]
1881
+ delete: options[:delete],
1882
+ bool: options[:bool],
1883
+ val: options[:val]
1706
1884
  })
1707
1885
  Doing::Pager::page last.strip if last
1708
1886
  end
@@ -1798,11 +1976,15 @@ command :show do |c|
1798
1976
  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.'
1799
1977
  c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
1800
1978
 
1801
- 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.'
1979
+ 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'
1802
1980
  c.arg_name 'TAG'
1803
- c.flag [:tag]
1981
+ c.flag [:tag], type: TagArray
1982
+
1983
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1984
+ c.arg_name 'QUERY'
1985
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1804
1986
 
1805
- c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
1987
+ c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans'
1806
1988
  c.arg_name 'BOOLEAN'
1807
1989
  c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1808
1990
 
@@ -1814,11 +1996,11 @@ command :show do |c|
1814
1996
  c.arg_name 'AGE'
1815
1997
  c.flag %i[a age], default_value: 'newest'
1816
1998
 
1817
- 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.'
1999
+ 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'
1818
2000
  c.arg_name 'DATE_STRING'
1819
2001
  c.flag [:before]
1820
2002
 
1821
- 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.'
2003
+ 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'
1822
2004
  c.arg_name 'DATE_STRING'
1823
2005
  c.flag [:after]
1824
2006
 
@@ -1929,7 +2111,7 @@ command :show do |c|
1929
2111
  section ||= 'All'
1930
2112
  end
1931
2113
 
1932
- tags.concat(options[:tag].to_tags) if options[:tag]
2114
+ tags.concat(options[:tag]) if options[:tag]
1933
2115
 
1934
2116
  options[:times] = true if options[:totals]
1935
2117
 
@@ -2012,7 +2194,7 @@ command :tags do |c|
2012
2194
  c.arg_name 'ORDER'
2013
2195
  c.flag %i[o order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
2014
2196
 
2015
- c.desc 'Get tags for entries matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?).'
2197
+ c.desc 'Get tags for entries matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?)'
2016
2198
  c.arg_name 'TAG'
2017
2199
  c.flag [:tag]
2018
2200
 
@@ -2021,6 +2203,10 @@ command :tags do |c|
2021
2203
  c.arg_name 'QUERY'
2022
2204
  c.flag [:search]
2023
2205
 
2206
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
2207
+ c.arg_name 'QUERY'
2208
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
2209
+
2024
2210
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
2025
2211
  # c.switch [:fuzzy], default_value: false, negatable: false
2026
2212
 
@@ -2034,7 +2220,7 @@ command :tags do |c|
2034
2220
  c.arg_name 'TYPE'
2035
2221
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2036
2222
 
2037
- c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans.'
2223
+ c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans'
2038
2224
  c.arg_name 'BOOLEAN'
2039
2225
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2040
2226
 
@@ -2291,11 +2477,15 @@ command :view do |c|
2291
2477
  c.desc 'Include colors in output'
2292
2478
  c.switch [:color], default_value: true, negatable: true
2293
2479
 
2294
- c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
2480
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?)'
2295
2481
  c.arg_name 'TAG'
2296
2482
  c.flag [:tag]
2297
2483
 
2298
- c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
2484
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
2485
+ c.arg_name 'QUERY'
2486
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
2487
+
2488
+ c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans'
2299
2489
  c.arg_name 'BOOLEAN'
2300
2490
  c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2301
2491
 
@@ -2324,11 +2514,11 @@ command :view do |c|
2324
2514
  c.arg_name 'DIRECTION'
2325
2515
  c.flag [:tag_order], must_match: REGEX_SORT_ORDER
2326
2516
 
2327
- 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.'
2517
+ 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'
2328
2518
  c.arg_name 'DATE_STRING'
2329
2519
  c.flag [:before]
2330
2520
 
2331
- 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.'
2521
+ 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'
2332
2522
  c.arg_name 'DATE_STRING'
2333
2523
  c.flag [:after]
2334
2524
 
@@ -2935,11 +3125,11 @@ command %i[archive move] do |c|
2935
3125
  c.desc 'Label moved items with @from(SECTION_NAME)'
2936
3126
  c.switch [:label], default_value: true, negatable: true
2937
3127
 
2938
- c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands.'
3128
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands'
2939
3129
  c.arg_name 'TAG'
2940
- c.flag [:tag]
3130
+ c.flag [:tag], type: TagArray
2941
3131
 
2942
- c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
3132
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans'
2943
3133
  c.arg_name 'BOOLEAN'
2944
3134
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2945
3135
 
@@ -2947,6 +3137,10 @@ command %i[archive move] do |c|
2947
3137
  c.arg_name 'QUERY'
2948
3138
  c.flag [:search]
2949
3139
 
3140
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
3141
+ c.arg_name 'QUERY'
3142
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
3143
+
2950
3144
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
2951
3145
  # c.switch [:fuzzy], default_value: false, negatable: false
2952
3146
 
@@ -2982,7 +3176,7 @@ command %i[archive move] do |c|
2982
3176
 
2983
3177
  raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
2984
3178
 
2985
- tags.concat(options[:tag].to_tags) if options[:tag]
3179
+ tags.concat(options[:tag]) if options[:tag]
2986
3180
 
2987
3181
  search = nil
2988
3182
 
@@ -3042,7 +3236,7 @@ command :import do |c|
3042
3236
 
3043
3237
  c.desc 'Tag all imported entries'
3044
3238
  c.arg_name 'TAGS'
3045
- c.flag :tag
3239
+ c.flag %i[t tag]
3046
3240
 
3047
3241
  c.desc 'Autotag entries'
3048
3242
  c.switch :autotag, negatable: true, default_value: true
@@ -3077,6 +3271,12 @@ command :import do |c|
3077
3271
  options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
3078
3272
  end
3079
3273
 
3274
+ if options[:search]
3275
+ search = options[:search]
3276
+ search.sub!(/^'?/, "'") if options[:exact]
3277
+ options[:search] = search
3278
+ end
3279
+
3080
3280
  if options[:from]
3081
3281
  date_string = options[:from]
3082
3282
  if date_string =~ / (to|through|thru|(un)?til|-+) /
@@ -3085,7 +3285,7 @@ command :import do |c|
3085
3285
  finish = dates[2].chronify(guess: :end)
3086
3286
  else
3087
3287
  start = date_string.chronify(guess: :begin)
3088
- finish = false
3288
+ finish = date_string.chronify(guess: :end)
3089
3289
  end
3090
3290
  raise InvalidTimeExpression, 'Unrecognized date string' unless start
3091
3291
  dates = [start, finish]
@@ -3122,11 +3322,11 @@ command :rotate do |c|
3122
3322
  c.arg_name 'SECTION_NAME'
3123
3323
  c.flag %i[s section], default_value: 'All'
3124
3324
 
3125
- c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands.'
3325
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands'
3126
3326
  c.arg_name 'TAG'
3127
3327
  c.flag [:tag]
3128
3328
 
3129
- c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
3329
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans'
3130
3330
  c.arg_name 'BOOLEAN'
3131
3331
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
3132
3332
 
@@ -3134,6 +3334,10 @@ command :rotate do |c|
3134
3334
  c.arg_name 'QUERY'
3135
3335
  c.flag [:search]
3136
3336
 
3337
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
3338
+ c.arg_name 'QUERY'
3339
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
3340
+
3137
3341
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
3138
3342
  # c.switch [:fuzzy], default_value: false, negatable: false
3139
3343
 
@@ -3357,7 +3561,7 @@ command :undo do |c|
3357
3561
  c.arg_name 'COUNT'
3358
3562
  c.flag %i[p prune], type: Integer
3359
3563
 
3360
- c.desc 'Redo last undo. Note: you cannot undo a redo.'
3564
+ c.desc 'Redo last undo. Note: you cannot undo a redo'
3361
3565
  c.switch %i[r redo]
3362
3566
 
3363
3567
  c.action do |_global_options, options, args|
@@ -3384,7 +3588,7 @@ command :undo do |c|
3384
3588
  end
3385
3589
 
3386
3590
  # @@redo
3387
- long_desc 'Shortcut for `doing undo -r`, reverses the last undo command. You cannot undo a redo.'
3591
+ long_desc 'Shortcut for `doing undo -r`, reverses the last undo command. You cannot undo a redo'
3388
3592
  arg_name 'COUNT'
3389
3593
  command :redo do |c|
3390
3594
  c.desc 'Specify alternate doing file'