doing 2.1.11 → 2.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +1 -0
  3. data/.yardoc/checksums +15 -13
  4. data/.yardoc/object_types +0 -0
  5. data/.yardoc/objects/root.dat +0 -0
  6. data/CHANGELOG.md +44 -1
  7. data/Gemfile.lock +9 -2
  8. data/README.md +56 -19
  9. data/bin/doing +215 -76
  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 +35 -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 +180 -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 +767 -115
  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 +407 -279
  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 +244 -45
  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 +35 -6
  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 +13 -1
  84. data/lib/doing/string.rb +97 -33
  85. data/lib/doing/string_chronify.rb +83 -13
  86. data/lib/doing/time.rb +6 -6
  87. data/lib/doing/util_backup.rb +1 -1
  88. data/lib/doing/version.rb +1 -1
  89. data/lib/doing/wwid.rb +88 -93
  90. data/lib/doing.rb +31 -27
  91. data/lib/examples/plugins/say_export.rb +1 -4
  92. 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.request_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.'
@@ -326,7 +350,7 @@ command %i[done did] do |c|
326
350
  If used without the --back option, the start date will be moved back to allow
327
351
  the completion date to be the current time.)
328
352
  c.arg_name 'INTERVAL'
329
- c.flag %i[t took]
353
+ c.flag %i[t took for]
330
354
 
331
355
  c.desc 'Section'
332
356
  c.arg_name 'NAME'
@@ -339,6 +363,9 @@ command %i[done did] do |c|
339
363
  c.arg_name 'TEXT'
340
364
  c.flag %i[n note]
341
365
 
366
+ c.desc 'Prompt for note via multi-line input'
367
+ c.switch %i[ask], negatable: false, default_value: false
368
+
342
369
  c.desc 'Finish last entry not already marked @done'
343
370
  c.switch %i[u unfinished], negatable: false, default_value: false
344
371
 
@@ -374,6 +401,8 @@ command %i[done did] do |c|
374
401
  end
375
402
 
376
403
  if options[:date]
404
+ finish_date = wwid.verify_duration(date, finish_date) unless options[:took]
405
+
377
406
  donedate = finish_date.strftime('%F %R')
378
407
  end
379
408
 
@@ -385,6 +414,7 @@ command %i[done did] do |c|
385
414
 
386
415
  note = Doing::Note.new
387
416
  note.add(options[:note]) if options[:note]
417
+ note.add(Doing::Prompt.request_lines(prompt: 'Add a note')) if options[:ask]
388
418
 
389
419
  if options[:editor]
390
420
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
@@ -439,7 +469,6 @@ command %i[done did] do |c|
439
469
  if options[:remove]
440
470
  wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
441
471
  else
442
- note = options[:note] ? Doing::Note.new(options[:note]) : nil
443
472
  opt = {
444
473
  archive: options[:archive],
445
474
  back: finish_date,
@@ -520,7 +549,7 @@ command :finish do |c|
520
549
 
521
550
  c.desc 'Set the completed date to the start date plus XX[hmd]'
522
551
  c.arg_name 'INTERVAL'
523
- c.flag %i[t took]
552
+ c.flag %i[t took for]
524
553
 
525
554
  c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.)
526
555
  c.arg_name 'DATE_STRING'
@@ -529,12 +558,16 @@ command :finish do |c|
529
558
  c.desc 'Finish the last X entries containing TAG.
530
559
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
531
560
  c.arg_name 'TAG'
532
- c.flag [:tag]
561
+ c.flag [:tag], type: TagArray
533
562
 
534
563
  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
564
  c.arg_name 'QUERY'
536
565
  c.flag [:search]
537
566
 
567
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
568
+ c.arg_name 'QUERY'
569
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
570
+
538
571
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
539
572
  # c.switch [:fuzzy], default_value: false, negatable: false
540
573
 
@@ -548,7 +581,7 @@ command :finish do |c|
548
581
  c.arg_name 'TYPE'
549
582
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
550
583
 
551
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
584
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
552
585
  c.arg_name 'BOOLEAN'
553
586
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
554
587
 
@@ -604,7 +637,7 @@ command :finish do |c|
604
637
  if options[:tag].nil?
605
638
  tags = []
606
639
  else
607
- tags = options[:tag].to_tags
640
+ tags = options[:tag]
608
641
  end
609
642
 
610
643
  raise InvalidArgument, 'Only one argument allowed' if args.length > 1
@@ -641,7 +674,8 @@ command :finish do |c|
641
674
  tag_bool: options[:bool].normalize_bool,
642
675
  tags: ['done'],
643
676
  took: options[:took],
644
- unfinished: options[:unfinished]
677
+ unfinished: options[:unfinished],
678
+ val: options[:val]
645
679
  }
646
680
 
647
681
  wwid.tag_last(opts)
@@ -666,6 +700,9 @@ command :later do |c|
666
700
  c.arg_name 'TEXT'
667
701
  c.flag %i[n note]
668
702
 
703
+ c.desc 'Prompt for note via multi-line input'
704
+ c.switch %i[ask], negatable: false, default_value: false
705
+
669
706
  c.action do |_global_options, options, args|
670
707
  if options[:back]
671
708
  date = options[:back].chronify(guess: :begin)
@@ -674,11 +711,16 @@ command :later do |c|
674
711
  date = Time.now
675
712
  end
676
713
 
714
+ ask_note = options[:ask] ? Doing::Prompt.request_lines(prompt: 'Add a note') : ''
715
+
677
716
  if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
678
717
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
679
718
 
680
719
  input += date.strftime('%F %R | ')
681
720
  input += args.empty? ? '' : args.join(' ')
721
+ input += "\n#{options[:note]}" if options[:note]
722
+ input += "\n#{ask_note}" unless ask_note.empty?
723
+
682
724
  input = wwid.fork_editor(input).strip
683
725
  raise EmptyInput, 'No content' unless input && !input.empty?
684
726
 
@@ -691,6 +733,7 @@ command :later do |c|
691
733
  d, title, note = wwid.format_input(args.join(' '))
692
734
  date = d.nil? ? date : d
693
735
  note.add(options[:note]) if options[:note]
736
+ note.add(ask_note) unless ask_note.empty?
694
737
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
695
738
  wwid.write(wwid.doing_file)
696
739
  elsif $stdin.stat.size.positive?
@@ -700,6 +743,7 @@ command :later do |c|
700
743
  date = d
701
744
  end
702
745
  note.add(options[:note]) if options[:note]
746
+ note.add(ask_note) unless ask_note.empty?
703
747
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
704
748
  wwid.write(wwid.doing_file)
705
749
  else
@@ -739,12 +783,16 @@ command %i[mark flag] do |c|
739
783
  c.desc 'Flag the last entry containing TAG.
740
784
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
741
785
  c.arg_name 'TAG'
742
- c.flag [:tag]
786
+ c.flag [:tag], type: TagArray
743
787
 
744
788
  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
789
  c.arg_name 'QUERY'
746
790
  c.flag [:search]
747
791
 
792
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
793
+ c.arg_name 'QUERY'
794
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
795
+
748
796
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
749
797
  # c.switch [:fuzzy], default_value: false, negatable: false
750
798
 
@@ -758,7 +806,7 @@ command %i[mark flag] do |c|
758
806
  c.arg_name 'TYPE'
759
807
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
760
808
 
761
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
809
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
762
810
  c.arg_name 'BOOLEAN'
763
811
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
764
812
 
@@ -780,7 +828,7 @@ command %i[mark flag] do |c|
780
828
  if options[:tag].nil?
781
829
  search_tags = []
782
830
  else
783
- search_tags = options[:tag].to_tags
831
+ search_tags = options[:tag]
784
832
  end
785
833
 
786
834
  if options[:interactive]
@@ -862,6 +910,9 @@ command :meanwhile do |c|
862
910
  c.arg_name 'TEXT'
863
911
  c.flag %i[n note]
864
912
 
913
+ c.desc 'Prompt for note via multi-line input'
914
+ c.switch %i[ask], negatable: false, default_value: false
915
+
865
916
  c.action do |_global_options, options, args|
866
917
  if options[:back]
867
918
  date = options[:back].chronify(guess: :begin)
@@ -878,10 +929,15 @@ command :meanwhile do |c|
878
929
  end
879
930
  input = ''
880
931
 
932
+ ask_note = options[:ask] ? Doing::Prompt.request_lines(prompt: 'Add a note') : []
933
+
881
934
  if options[:editor]
882
935
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
883
936
  input += date.strftime('%F %R | ')
884
937
  input += args.join(' ') unless args.empty?
938
+ input += "\n#{options[:note]}" if options[:note]
939
+ input += "\n#{ask_note}" unless ask_note.empty?
940
+
885
941
  input = wwid.fork_editor(input).strip
886
942
  elsif !args.empty?
887
943
  input = args.join(' ')
@@ -900,10 +956,9 @@ command :meanwhile do |c|
900
956
  note = []
901
957
  end
902
958
 
903
- if options[:note]
904
- note.push(options[:note])
905
- elsif note.empty?
906
- note = nil
959
+ unless options[:editor]
960
+ note.add(options[:note]) if options[:note]
961
+ note.add(ask_note) unless ask_note.empty?
907
962
  end
908
963
 
909
964
  wwid.stop_start('meanwhile', { new_item: input, back: date, section: section, archive: options[:archive], note: note })
@@ -937,14 +992,18 @@ command :note do |c|
937
992
  c.desc "Replace/Remove last entry's note (default append)"
938
993
  c.switch %i[r remove], negatable: false, default_value: false
939
994
 
940
- c.desc 'Add/remove note from last entry matching tag. Wildcards allowed (*, ?).'
995
+ c.desc 'Add/remove note from last entry matching tag. Wildcards allowed (*, ?)'
941
996
  c.arg_name 'TAG'
942
- c.flag [:tag]
997
+ c.flag [:tag], type: TagArray
943
998
 
944
999
  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
1000
  c.arg_name 'QUERY'
946
1001
  c.flag [:search]
947
1002
 
1003
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1004
+ c.arg_name 'QUERY'
1005
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1006
+
948
1007
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
949
1008
  # c.switch [:fuzzy], default_value: false, negatable: false
950
1009
 
@@ -958,13 +1017,16 @@ command :note do |c|
958
1017
  c.arg_name 'TYPE'
959
1018
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
960
1019
 
961
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
1020
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
962
1021
  c.arg_name 'BOOLEAN'
963
1022
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
964
1023
 
965
1024
  c.desc 'Select item for new note from a menu of matching entries'
966
1025
  c.switch %i[i interactive], negatable: false, default_value: false
967
1026
 
1027
+ c.desc 'Prompt for note via multi-line input'
1028
+ c.switch %i[ask], negatable: false, default_value: false
1029
+
968
1030
  c.action do |_global_options, options, args|
969
1031
  options[:fuzzy] = false
970
1032
  if options[:section]
@@ -991,8 +1053,9 @@ command :note do |c|
991
1053
 
992
1054
  last_note = last_entry.note || Doing::Note.new
993
1055
  new_note = Doing::Note.new
1056
+ ask_note = options[:ask] ? Doing::Prompt.request_lines(prompt: 'Add a note') : ''
994
1057
 
995
- if options[:editor] || (args.empty? && $stdin.stat.size.zero? && !options[:remove])
1058
+ if options[:editor] || (args.empty? && $stdin.stat.size.zero? && !options[:remove] && !options[:ask])
996
1059
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
997
1060
 
998
1061
  input = !args.empty? ? args.join(' ') : ''
@@ -1003,7 +1066,9 @@ command :note do |c|
1003
1066
  prev_input = last_entry.note || Doing::Note.new
1004
1067
  end
1005
1068
 
1069
+
1006
1070
  input = prev_input.add(input)
1071
+ input.add(ask_note) unless ask_note.empty?
1007
1072
 
1008
1073
  input = wwid.fork_editor(prev_input.strip_lines.join("\n"), message: nil).strip
1009
1074
  note = input
@@ -1014,9 +1079,12 @@ command :note do |c|
1014
1079
  elsif $stdin.stat.size.positive?
1015
1080
  new_note.add($stdin.read.strip)
1016
1081
  else
1017
- raise EmptyInput, 'You must provide content when adding a note' unless options[:remove]
1082
+ raise EmptyInput, 'You must provide content when adding a note' unless options[:remove] || !ask_note.empty?
1083
+
1018
1084
  end
1019
1085
 
1086
+ new_note.add(ask_note) unless ask_note.empty?
1087
+
1020
1088
  if last_note.equal?(new_note)
1021
1089
  Doing.logger.debug('Skipped:', 'No note change')
1022
1090
  else
@@ -1062,6 +1130,9 @@ command %i[now next] do |c|
1062
1130
  c.arg_name 'TEXT'
1063
1131
  c.flag %i[n note]
1064
1132
 
1133
+ c.desc 'Prompt for note via multi-line input'
1134
+ c.switch %i[ask], negatable: false, default_value: false
1135
+
1065
1136
  # c.desc "Edit entry with specified app"
1066
1137
  # c.arg_name 'editor_app'
1067
1138
  # # c.flag [:a, :app]
@@ -1081,23 +1152,28 @@ command %i[now next] do |c|
1081
1152
  options[:section] = settings['current_section']
1082
1153
  end
1083
1154
 
1155
+ ask_note = options[:ask] ? Doing::Prompt.request_lines(prompt: 'Add a note') : ''
1156
+
1084
1157
  if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
1085
1158
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
1086
1159
 
1087
1160
  input = date.strftime('%F %R | ')
1088
1161
  input += args.join(' ') unless args.empty?
1162
+ input += "\n#{options[:note]}" if options[:note]
1163
+ input += "\n#{ask_note}" unless ask_note.empty?
1089
1164
  input = wwid.fork_editor(input).strip
1090
1165
 
1091
1166
  raise EmptyInput, 'No content' if input.empty?
1092
1167
 
1093
1168
  date, title, note = wwid.format_input(input)
1094
- note.add(options[:note]) if options[:note]
1169
+
1095
1170
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1096
1171
  wwid.write(wwid.doing_file)
1097
1172
  elsif args.length.positive?
1098
1173
  d, title, note = wwid.format_input(args.join(' '))
1099
1174
  date = d.nil? ? date : d
1100
1175
  note.add(options[:note]) if options[:note]
1176
+ note.add(ask_note) unless ask_note.empty?
1101
1177
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1102
1178
  wwid.write(wwid.doing_file)
1103
1179
  elsif $stdin.stat.size.positive?
@@ -1108,6 +1184,7 @@ command %i[now next] do |c|
1108
1184
  date = d
1109
1185
  end
1110
1186
  note.add(options[:note]) if options[:note]
1187
+ note.add(ask_note) unless ask_note.empty?
1111
1188
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1112
1189
  wwid.write(wwid.doing_file)
1113
1190
  else
@@ -1135,7 +1212,7 @@ command %i[reset begin] do |c|
1135
1212
  c.desc 'Resume entry (remove @done)'
1136
1213
  c.switch %i[r resume], default_value: true
1137
1214
 
1138
- c.desc 'Reset last entry matching tag. Wildcards allowed (*, ?).'
1215
+ c.desc 'Reset last entry matching tag. Wildcards allowed (*, ?)'
1139
1216
  c.arg_name 'TAG'
1140
1217
  c.flag [:tag]
1141
1218
 
@@ -1143,6 +1220,10 @@ command %i[reset begin] do |c|
1143
1220
  c.arg_name 'QUERY'
1144
1221
  c.flag [:search]
1145
1222
 
1223
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1224
+ c.arg_name 'QUERY'
1225
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1226
+
1146
1227
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
1147
1228
  # c.switch [:fuzzy], default_value: false, negatable: false
1148
1229
 
@@ -1156,7 +1237,7 @@ command %i[reset begin] do |c|
1156
1237
  c.arg_name 'TYPE'
1157
1238
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1158
1239
 
1159
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1240
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
1160
1241
  c.arg_name 'BOOLEAN'
1161
1242
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1162
1243
 
@@ -1256,13 +1337,21 @@ command :select do |c|
1256
1337
 
1257
1338
  c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"'
1258
1339
  c.arg_name 'QUERY'
1259
- c.flag %i[q query search]
1340
+ c.flag %i[q query]
1341
+
1342
+ c.desc 'Select from entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
1343
+ c.arg_name 'QUERY'
1344
+ c.flag [:search]
1345
+
1346
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1347
+ c.arg_name 'QUERY'
1348
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1260
1349
 
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.'
1350
+ 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
1351
  c.arg_name 'DATE_STRING'
1263
1352
  c.flag [:before]
1264
1353
 
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.'
1354
+ 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
1355
  c.arg_name 'DATE_STRING'
1267
1356
  c.flag [:after]
1268
1357
 
@@ -1287,7 +1376,7 @@ command :select do |c|
1287
1376
  c.arg_name 'TYPE'
1288
1377
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1289
1378
 
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.'
1379
+ 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
1380
  c.switch %i[menu], negatable: true, default_value: true
1292
1381
 
1293
1382
  c.desc 'Cancel selected items (add @done without timestamp)'
@@ -1305,7 +1394,7 @@ command :select do |c|
1305
1394
  c.desc 'Add flag to selected item(s)'
1306
1395
  c.switch %i[flag], negatable: false, default_value: false
1307
1396
 
1308
- c.desc 'Perform action without confirmation.'
1397
+ c.desc 'Perform action without confirmation'
1309
1398
  c.switch %i[force], negatable: false, default_value: false
1310
1399
 
1311
1400
  c.desc 'Save selected entries to file using --output format'
@@ -1365,6 +1454,10 @@ command :tag do |c|
1365
1454
  c.arg_name 'ORIG_TAG'
1366
1455
  c.flag %i[rename]
1367
1456
 
1457
+ c.desc 'Include a value, e.g. @tag(value)'
1458
+ c.arg_name 'VALUE'
1459
+ c.flag %i[v value]
1460
+
1368
1461
  c.desc 'Don\'t ask permission to tag all entries when count is 0'
1369
1462
  c.switch %i[force], negatable: false, default_value: false
1370
1463
 
@@ -1386,12 +1479,16 @@ command :tag do |c|
1386
1479
  c.desc 'Tag the last X entries containing TAG.
1387
1480
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
1388
1481
  c.arg_name 'TAG'
1389
- c.flag [:tag]
1482
+ c.flag [:tag], type: TagArray
1390
1483
 
1391
1484
  c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
1392
1485
  c.arg_name 'QUERY'
1393
1486
  c.flag [:search]
1394
1487
 
1488
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1489
+ c.arg_name 'QUERY'
1490
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1491
+
1395
1492
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
1396
1493
  # c.switch [:fuzzy], default_value: false, negatable: false
1397
1494
 
@@ -1405,7 +1502,7 @@ command :tag do |c|
1405
1502
  c.arg_name 'TYPE'
1406
1503
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1407
1504
 
1408
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
1505
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
1409
1506
  c.arg_name 'BOOLEAN'
1410
1507
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1411
1508
 
@@ -1428,7 +1525,7 @@ command :tag do |c|
1428
1525
  if options[:tag].nil?
1429
1526
  search_tags = []
1430
1527
  else
1431
- search_tags = options[:tag].to_tags
1528
+ search_tags = options[:tag]
1432
1529
  end
1433
1530
 
1434
1531
  if options[:autotag]
@@ -1528,11 +1625,11 @@ command %i[grep search] do |c|
1528
1625
  c.arg_name 'NAME'
1529
1626
  c.flag %i[s section], default_value: 'All'
1530
1627
 
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.'
1628
+ 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
1629
  c.arg_name 'DATE_STRING'
1533
1630
  c.flag [:before]
1534
1631
 
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.'
1632
+ 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
1633
  c.arg_name 'DATE_STRING'
1537
1634
  c.flag [:after]
1538
1635
 
@@ -1591,6 +1688,13 @@ command %i[grep search] do |c|
1591
1688
  c.desc 'Display an interactive menu of results to perform further operations'
1592
1689
  c.switch %i[i interactive], default_value: false, negatable: false
1593
1690
 
1691
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1692
+ c.arg_name 'QUERY'
1693
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1694
+
1695
+ c.desc 'Combine multiple tags or value queries using AND, OR, or NOT'
1696
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1697
+
1594
1698
  c.action do |_global_options, options, args|
1595
1699
  options[:fuzzy] = false
1596
1700
  raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
@@ -1601,6 +1705,7 @@ command %i[grep search] do |c|
1601
1705
  section = wwid.guess_section(options[:section]) if options[:section]
1602
1706
 
1603
1707
  options[:case] = options[:case].normalize_case
1708
+ options[:bool] = options[:bool].normalize_bool
1604
1709
 
1605
1710
  search = args.join(' ')
1606
1711
  search.sub!(/^'?/, "'") if options[:exact]
@@ -1639,11 +1744,11 @@ command :last do |c|
1639
1744
  c.desc "Delete the last entry"
1640
1745
  c.switch %i[d delete], negatable: false, default_value: false
1641
1746
 
1642
- c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
1747
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?)'
1643
1748
  c.arg_name 'TAG'
1644
- c.flag [:tag]
1749
+ c.flag [:tag], type: TagArray
1645
1750
 
1646
- c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
1751
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans'
1647
1752
  c.arg_name 'BOOLEAN'
1648
1753
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1649
1754
 
@@ -1651,6 +1756,10 @@ command :last do |c|
1651
1756
  c.arg_name 'QUERY'
1652
1757
  c.flag [:search]
1653
1758
 
1759
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1760
+ c.arg_name 'QUERY'
1761
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1762
+
1654
1763
  c.desc 'Show elapsed time if entry is not tagged @done'
1655
1764
  c.switch [:duration]
1656
1765
 
@@ -1674,7 +1783,7 @@ command :last do |c|
1674
1783
  if options[:tag].nil?
1675
1784
  options[:tag] = []
1676
1785
  else
1677
- options[:tag] = options[:tag].to_tags
1786
+ options[:tag] = options[:tag]
1678
1787
  options[:bool] = options[:bool].normalize_bool
1679
1788
  end
1680
1789
 
@@ -1685,12 +1794,14 @@ command :last do |c|
1685
1794
  if options[:editor]
1686
1795
  wwid.edit_last(section: options[:section],
1687
1796
  options: {
1688
- search: search,
1797
+ search: options[:search],
1689
1798
  fuzzy: options[:fuzzy],
1690
1799
  case: options[:case],
1691
- tag: tags,
1800
+ tag: options[:tag],
1692
1801
  tag_bool: options[:bool],
1693
- not: options[:not]
1802
+ not: options[:not],
1803
+ val: options[:val],
1804
+ bool: options[:bool]
1694
1805
  })
1695
1806
  else
1696
1807
  last = wwid.last(times: true, section: options[:section],
@@ -1702,7 +1813,9 @@ command :last do |c|
1702
1813
  negate: options[:not],
1703
1814
  tag: options[:tag],
1704
1815
  tag_bool: options[:bool],
1705
- delete: options[:delete]
1816
+ delete: options[:delete],
1817
+ bool: options[:bool],
1818
+ val: options[:val]
1706
1819
  })
1707
1820
  Doing::Pager::page last.strip if last
1708
1821
  end
@@ -1798,11 +1911,15 @@ command :show do |c|
1798
1911
  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
1912
  c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
1800
1913
 
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.'
1914
+ 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
1915
  c.arg_name 'TAG'
1803
- c.flag [:tag]
1916
+ c.flag [:tag], type: TagArray
1917
+
1918
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1919
+ c.arg_name 'QUERY'
1920
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1804
1921
 
1805
- c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
1922
+ c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans'
1806
1923
  c.arg_name 'BOOLEAN'
1807
1924
  c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1808
1925
 
@@ -1814,11 +1931,11 @@ command :show do |c|
1814
1931
  c.arg_name 'AGE'
1815
1932
  c.flag %i[a age], default_value: 'newest'
1816
1933
 
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.'
1934
+ 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
1935
  c.arg_name 'DATE_STRING'
1819
1936
  c.flag [:before]
1820
1937
 
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.'
1938
+ 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
1939
  c.arg_name 'DATE_STRING'
1823
1940
  c.flag [:after]
1824
1941
 
@@ -1929,7 +2046,7 @@ command :show do |c|
1929
2046
  section ||= 'All'
1930
2047
  end
1931
2048
 
1932
- tags.concat(options[:tag].to_tags) if options[:tag]
2049
+ tags.concat(options[:tag]) if options[:tag]
1933
2050
 
1934
2051
  options[:times] = true if options[:totals]
1935
2052
 
@@ -2012,7 +2129,7 @@ command :tags do |c|
2012
2129
  c.arg_name 'ORDER'
2013
2130
  c.flag %i[o order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
2014
2131
 
2015
- c.desc 'Get tags for entries matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?).'
2132
+ c.desc 'Get tags for entries matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?)'
2016
2133
  c.arg_name 'TAG'
2017
2134
  c.flag [:tag]
2018
2135
 
@@ -2021,6 +2138,10 @@ command :tags do |c|
2021
2138
  c.arg_name 'QUERY'
2022
2139
  c.flag [:search]
2023
2140
 
2141
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
2142
+ c.arg_name 'QUERY'
2143
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
2144
+
2024
2145
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
2025
2146
  # c.switch [:fuzzy], default_value: false, negatable: false
2026
2147
 
@@ -2034,7 +2155,7 @@ command :tags do |c|
2034
2155
  c.arg_name 'TYPE'
2035
2156
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2036
2157
 
2037
- c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans.'
2158
+ c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans'
2038
2159
  c.arg_name 'BOOLEAN'
2039
2160
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2040
2161
 
@@ -2291,11 +2412,15 @@ command :view do |c|
2291
2412
  c.desc 'Include colors in output'
2292
2413
  c.switch [:color], default_value: true, negatable: true
2293
2414
 
2294
- c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
2415
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?)'
2295
2416
  c.arg_name 'TAG'
2296
2417
  c.flag [:tag]
2297
2418
 
2298
- c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
2419
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
2420
+ c.arg_name 'QUERY'
2421
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
2422
+
2423
+ c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans'
2299
2424
  c.arg_name 'BOOLEAN'
2300
2425
  c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2301
2426
 
@@ -2324,11 +2449,11 @@ command :view do |c|
2324
2449
  c.arg_name 'DIRECTION'
2325
2450
  c.flag [:tag_order], must_match: REGEX_SORT_ORDER
2326
2451
 
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.'
2452
+ 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
2453
  c.arg_name 'DATE_STRING'
2329
2454
  c.flag [:before]
2330
2455
 
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.'
2456
+ 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
2457
  c.arg_name 'DATE_STRING'
2333
2458
  c.flag [:after]
2334
2459
 
@@ -2935,11 +3060,11 @@ command %i[archive move] do |c|
2935
3060
  c.desc 'Label moved items with @from(SECTION_NAME)'
2936
3061
  c.switch [:label], default_value: true, negatable: true
2937
3062
 
2938
- c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands.'
3063
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands'
2939
3064
  c.arg_name 'TAG'
2940
- c.flag [:tag]
3065
+ c.flag [:tag], type: TagArray
2941
3066
 
2942
- c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
3067
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans'
2943
3068
  c.arg_name 'BOOLEAN'
2944
3069
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2945
3070
 
@@ -2947,6 +3072,10 @@ command %i[archive move] do |c|
2947
3072
  c.arg_name 'QUERY'
2948
3073
  c.flag [:search]
2949
3074
 
3075
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
3076
+ c.arg_name 'QUERY'
3077
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
3078
+
2950
3079
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
2951
3080
  # c.switch [:fuzzy], default_value: false, negatable: false
2952
3081
 
@@ -2982,7 +3111,7 @@ command %i[archive move] do |c|
2982
3111
 
2983
3112
  raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
2984
3113
 
2985
- tags.concat(options[:tag].to_tags) if options[:tag]
3114
+ tags.concat(options[:tag]) if options[:tag]
2986
3115
 
2987
3116
  search = nil
2988
3117
 
@@ -3042,7 +3171,7 @@ command :import do |c|
3042
3171
 
3043
3172
  c.desc 'Tag all imported entries'
3044
3173
  c.arg_name 'TAGS'
3045
- c.flag :tag
3174
+ c.flag %i[t tag]
3046
3175
 
3047
3176
  c.desc 'Autotag entries'
3048
3177
  c.switch :autotag, negatable: true, default_value: true
@@ -3077,6 +3206,12 @@ command :import do |c|
3077
3206
  options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
3078
3207
  end
3079
3208
 
3209
+ if options[:search]
3210
+ search = options[:search]
3211
+ search.sub!(/^'?/, "'") if options[:exact]
3212
+ options[:search] = search
3213
+ end
3214
+
3080
3215
  if options[:from]
3081
3216
  date_string = options[:from]
3082
3217
  if date_string =~ / (to|through|thru|(un)?til|-+) /
@@ -3085,7 +3220,7 @@ command :import do |c|
3085
3220
  finish = dates[2].chronify(guess: :end)
3086
3221
  else
3087
3222
  start = date_string.chronify(guess: :begin)
3088
- finish = false
3223
+ finish = date_string.chronify(guess: :end)
3089
3224
  end
3090
3225
  raise InvalidTimeExpression, 'Unrecognized date string' unless start
3091
3226
  dates = [start, finish]
@@ -3122,11 +3257,11 @@ command :rotate do |c|
3122
3257
  c.arg_name 'SECTION_NAME'
3123
3258
  c.flag %i[s section], default_value: 'All'
3124
3259
 
3125
- c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands.'
3260
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands'
3126
3261
  c.arg_name 'TAG'
3127
3262
  c.flag [:tag]
3128
3263
 
3129
- c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
3264
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans'
3130
3265
  c.arg_name 'BOOLEAN'
3131
3266
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
3132
3267
 
@@ -3134,6 +3269,10 @@ command :rotate do |c|
3134
3269
  c.arg_name 'QUERY'
3135
3270
  c.flag [:search]
3136
3271
 
3272
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
3273
+ c.arg_name 'QUERY'
3274
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
3275
+
3137
3276
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
3138
3277
  # c.switch [:fuzzy], default_value: false, negatable: false
3139
3278
 
@@ -3357,7 +3496,7 @@ command :undo do |c|
3357
3496
  c.arg_name 'COUNT'
3358
3497
  c.flag %i[p prune], type: Integer
3359
3498
 
3360
- c.desc 'Redo last undo. Note: you cannot undo a redo.'
3499
+ c.desc 'Redo last undo. Note: you cannot undo a redo'
3361
3500
  c.switch %i[r redo]
3362
3501
 
3363
3502
  c.action do |_global_options, options, args|
@@ -3384,7 +3523,7 @@ command :undo do |c|
3384
3523
  end
3385
3524
 
3386
3525
  # @@redo
3387
- long_desc 'Shortcut for `doing undo -r`, reverses the last undo command. You cannot undo a redo.'
3526
+ long_desc 'Shortcut for `doing undo -r`, reverses the last undo command. You cannot undo a redo'
3388
3527
  arg_name 'COUNT'
3389
3528
  command :redo do |c|
3390
3529
  c.desc 'Specify alternate doing file'