doing 2.1.12 → 2.1.16

Sign up to get free protection for your applications and to get access to all the features.
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'