doing 2.1.13 → 2.1.17

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +1 -0
  3. data/.yardoc/checksums +14 -12
  4. data/.yardoc/object_types +0 -0
  5. data/.yardoc/objects/root.dat +0 -0
  6. data/CHANGELOG.md +76 -0
  7. data/Gemfile.lock +9 -2
  8. data/README.md +56 -19
  9. data/bin/doing +218 -68
  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 +6 -2
  18. data/docs/doc/Doing/Completion.html +1 -1
  19. data/docs/doc/Doing/Configuration.html +8 -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 +340 -14
  31. data/docs/doc/Doing/Items.html +2 -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 +1 -1
  40. data/docs/doc/Doing/Util.html +1 -1
  41. data/docs/doc/Doing/WWID.html +77 -71
  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 +997 -118
  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 +449 -305
  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 +76 -9
  68. data/example_plugin.rb +2 -4
  69. data/lib/completion/_doing.zsh +17 -17
  70. data/lib/completion/doing.bash +25 -25
  71. data/lib/completion/doing.fish +18 -6
  72. data/lib/doing/array_chronify.rb +57 -0
  73. data/lib/doing/colors.rb +4 -0
  74. data/lib/doing/configuration.rb +6 -2
  75. data/lib/doing/item.rb +108 -0
  76. data/lib/doing/log_adapter.rb +3 -3
  77. data/lib/doing/numeric_chronify.rb +40 -0
  78. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  79. data/lib/doing/plugins/export/json_export.rb +2 -2
  80. data/lib/doing/plugins/export/template_export.rb +49 -90
  81. data/lib/doing/plugins/import/calendar_import.rb +13 -1
  82. data/lib/doing/plugins/import/doing_import.rb +12 -1
  83. data/lib/doing/plugins/import/timing_import.rb +13 -1
  84. data/lib/doing/prompt.rb +65 -1
  85. data/lib/doing/string.rb +137 -33
  86. data/lib/doing/string_chronify.rb +112 -14
  87. data/lib/doing/template_string.rb +1 -1
  88. data/lib/doing/time.rb +6 -6
  89. data/lib/doing/util_backup.rb +1 -1
  90. data/lib/doing/version.rb +1 -1
  91. data/lib/doing/wwid.rb +117 -106
  92. data/lib/doing.rb +36 -31
  93. data/lib/examples/plugins/say_export.rb +1 -4
  94. metadata +46 -2
data/bin/doing CHANGED
@@ -70,6 +70,12 @@ if settings.dig('plugins', 'command_path')
70
70
  commands_from File.expand_path(settings.dig('plugins', 'command_path'))
71
71
  end
72
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
+
73
79
  program_desc 'A CLI for a What Was I Doing system'
74
80
  program_long_desc %(Doing uses a TaskPaper-like formatting to keep a plain text
75
81
  record of what you've been doing, complete with tag-based time tracking. The
@@ -139,7 +145,7 @@ command %i[again resume] do |c|
139
145
 
140
146
  c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?)'
141
147
  c.arg_name 'TAG'
142
- c.flag [:tag]
148
+ c.flag [:tag], type: TagArray
143
149
 
144
150
  c.desc 'Repeat last entry matching search. Surround with
145
151
  slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
@@ -170,16 +176,19 @@ command %i[again resume] do |c|
170
176
  c.desc "Edit duplicated entry with #{Doing::Util.default_editor} before adding"
171
177
  c.switch %i[e editor], negatable: false, default_value: false
172
178
 
173
- c.desc 'Note'
179
+ c.desc 'Add a note'
174
180
  c.arg_name 'TEXT'
175
181
  c.flag %i[n note]
176
182
 
183
+ c.desc 'Prompt for note via multi-line input'
184
+ c.switch %i[ask], negatable: false, default_value: false
185
+
177
186
  c.desc 'Select item to resume from a menu of matching entries'
178
187
  c.switch %i[i interactive], negatable: false, default_value: false
179
188
 
180
189
  c.action do |_global_options, options, _args|
181
190
  options[:fuzzy] = false
182
- tags = options[:tag].nil? ? [] : options[:tag].to_tags
191
+ tags = options[:tag].nil? ? [] : options[:tag]
183
192
 
184
193
  options[:case] = options[:case].normalize_case
185
194
 
@@ -189,6 +198,11 @@ command %i[again resume] do |c|
189
198
  options[:search] = search
190
199
  end
191
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
+
192
206
  opts = options.dup
193
207
 
194
208
  opts[:tag] = tags
@@ -216,7 +230,7 @@ command :cancel do |c|
216
230
 
217
231
  c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2). Wildcards allowed (*, ?)'
218
232
  c.arg_name 'TAG'
219
- c.flag [:tag]
233
+ c.flag [:tag], type: TagArray
220
234
 
221
235
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
222
236
  c.arg_name 'BOOLEAN'
@@ -260,7 +274,7 @@ command :cancel do |c|
260
274
  if options[:tag].nil?
261
275
  tags = []
262
276
  else
263
- tags = options[:tag].to_tags
277
+ tags = options[:tag]
264
278
  end
265
279
 
266
280
  raise InvalidArgument, 'Only one argument allowed' if args.length > 1
@@ -324,19 +338,26 @@ command %i[done did] do |c|
324
338
  c.switch %i[a archive], negatable: false, default_value: false
325
339
 
326
340
  c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm).
327
- If used, ignores --back. Used with --took, backdates start date)
341
+ Used with --took, backdates start date)
328
342
  c.arg_name 'DATE_STRING'
329
- c.flag [:at]
343
+ c.flag %i[at finished]
330
344
 
331
345
  c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
332
346
  c.arg_name 'DATE_STRING'
333
347
  c.flag %i[b back started]
334
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
+
335
356
  c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM).
336
357
  If used without the --back option, the start date will be moved back to allow
337
358
  the completion date to be the current time.)
338
359
  c.arg_name 'INTERVAL'
339
- c.flag %i[t took]
360
+ c.flag %i[t took for]
340
361
 
341
362
  c.desc 'Section'
342
363
  c.arg_name 'NAME'
@@ -349,6 +370,9 @@ command %i[done did] do |c|
349
370
  c.arg_name 'TEXT'
350
371
  c.flag %i[n note]
351
372
 
373
+ c.desc 'Prompt for note via multi-line input'
374
+ c.switch %i[ask], negatable: false, default_value: false
375
+
352
376
  c.desc 'Finish last entry not already marked @done'
353
377
  c.switch %i[u unfinished], negatable: false, default_value: false
354
378
 
@@ -360,30 +384,41 @@ command %i[done did] do |c|
360
384
  took = 0
361
385
  donedate = nil
362
386
 
363
- if options[:took]
364
- took = options[:took].chronify_qty
365
- raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
366
- end
367
-
368
- if options[:back]
369
- date = options[:back].chronify(guess: :begin)
370
- 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
371
390
  else
372
- date = options[:took] ? Time.now - took : Time.now
373
- 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
374
395
 
375
- if options[:at]
376
- finish_date = options[:at].chronify(guess: :begin)
377
- 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
378
402
 
379
- date = options[:took] ? finish_date - took : finish_date
380
- elsif options[:took]
381
- finish_date = date + took
382
- else
383
- 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
384
417
  end
385
418
 
386
419
  if options[:date]
420
+ finish_date = wwid.verify_duration(date, finish_date) unless options[:took] || options[:from]
421
+
387
422
  donedate = finish_date.strftime('%F %R')
388
423
  end
389
424
 
@@ -393,9 +428,14 @@ command %i[done did] do |c|
393
428
  section = settings['current_section']
394
429
  end
395
430
 
431
+
396
432
  note = Doing::Note.new
397
433
  note.add(options[:note]) if options[:note]
398
434
 
435
+ if options[:ask] && !options[:editor]
436
+ note.add(Doing::Prompt.read_lines(prompt: 'Add a note'))
437
+ end
438
+
399
439
  if options[:editor]
400
440
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
401
441
  is_new = false
@@ -420,6 +460,12 @@ command %i[done did] do |c|
420
460
  raise EmptyInput, 'No content' unless input && !input.empty?
421
461
 
422
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
+
423
469
  date = d.nil? ? date : d
424
470
  new_entry = Doing::Item.new(date, title, section, note)
425
471
  if new_entry.should_finish?
@@ -449,7 +495,6 @@ command %i[done did] do |c|
449
495
  if options[:remove]
450
496
  wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
451
497
  else
452
- note = options[:note] ? Doing::Note.new(options[:note]) : nil
453
498
  opt = {
454
499
  archive: options[:archive],
455
500
  back: finish_date,
@@ -464,12 +509,13 @@ command %i[done did] do |c|
464
509
  wwid.tag_last(opt)
465
510
  end
466
511
  elsif !args.empty?
467
- note = Doing::Note.new(options[:note])
468
512
  d, title, new_note = wwid.format_input([args.join(' '), note.strip_lines.join("\n")].join("\n"))
469
513
  date = d.nil? ? date : d
514
+ new_note.add(options[:note])
470
515
  title.chomp!
471
516
  section = 'Archive' if options[:archive]
472
517
  new_entry = Doing::Item.new(date, title, section, new_note)
518
+
473
519
  if new_entry.should_finish?
474
520
  if new_entry.should_time?
475
521
  new_entry.tag('done', value: donedate)
@@ -477,12 +523,14 @@ command %i[done did] do |c|
477
523
  new_entry.tag('done')
478
524
  end
479
525
  end
526
+
480
527
  Doing::Hooks.trigger :pre_entry_add, wwid, new_entry
481
528
  wwid.content.push(new_entry)
482
529
  Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
483
530
  wwid.write(wwid.doing_file)
484
531
  Doing.logger.info('Entry Added:', new_entry.title)
485
532
  elsif $stdin.stat.size.positive?
533
+ note = Doing::Note.new(options[:note])
486
534
  d, title, note = wwid.format_input($stdin.read.strip)
487
535
  unless d.nil?
488
536
  Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
@@ -530,7 +578,7 @@ command :finish do |c|
530
578
 
531
579
  c.desc 'Set the completed date to the start date plus XX[hmd]'
532
580
  c.arg_name 'INTERVAL'
533
- c.flag %i[t took]
581
+ c.flag %i[t took for]
534
582
 
535
583
  c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.)
536
584
  c.arg_name 'DATE_STRING'
@@ -539,7 +587,7 @@ command :finish do |c|
539
587
  c.desc 'Finish the last X entries containing TAG.
540
588
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
541
589
  c.arg_name 'TAG'
542
- c.flag [:tag]
590
+ c.flag [:tag], type: TagArray
543
591
 
544
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")'
545
593
  c.arg_name 'QUERY'
@@ -618,7 +666,7 @@ command :finish do |c|
618
666
  if options[:tag].nil?
619
667
  tags = []
620
668
  else
621
- tags = options[:tag].to_tags
669
+ tags = options[:tag]
622
670
  end
623
671
 
624
672
  raise InvalidArgument, 'Only one argument allowed' if args.length > 1
@@ -681,6 +729,9 @@ command :later do |c|
681
729
  c.arg_name 'TEXT'
682
730
  c.flag %i[n note]
683
731
 
732
+ c.desc 'Prompt for note via multi-line input'
733
+ c.switch %i[ask], negatable: false, default_value: false
734
+
684
735
  c.action do |_global_options, options, args|
685
736
  if options[:back]
686
737
  date = options[:back].chronify(guess: :begin)
@@ -689,23 +740,36 @@ command :later do |c|
689
740
  date = Time.now
690
741
  end
691
742
 
692
- 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]
693
746
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
694
747
 
748
+ input = ''
695
749
  input += date.strftime('%F %R | ')
696
750
  input += args.empty? ? '' : args.join(' ')
751
+ input += "\n#{options[:note]}" if options[:note]
752
+ input += "\n#{ask_note}" unless ask_note.empty?
753
+
697
754
  input = wwid.fork_editor(input).strip
698
- raise EmptyInput, 'No content' unless input && !input.empty?
699
755
 
700
756
  d, title, note = wwid.format_input(input)
701
- date = d.nil? ? date : d
757
+ raise EmptyInput, 'No content' if title.empty?
758
+
702
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
703
766
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
704
767
  wwid.write(wwid.doing_file)
705
768
  elsif !args.empty?
706
769
  d, title, note = wwid.format_input(args.join(' '))
707
770
  date = d.nil? ? date : d
708
771
  note.add(options[:note]) if options[:note]
772
+ note.add(ask_note) unless ask_note.empty?
709
773
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
710
774
  wwid.write(wwid.doing_file)
711
775
  elsif $stdin.stat.size.positive?
@@ -715,10 +779,20 @@ command :later do |c|
715
779
  date = d
716
780
  end
717
781
  note.add(options[:note]) if options[:note]
782
+ note.add(ask_note) unless ask_note.empty?
718
783
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
719
784
  wwid.write(wwid.doing_file)
720
785
  else
721
- 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)
722
796
  end
723
797
  end
724
798
  end
@@ -754,7 +828,7 @@ command %i[mark flag] do |c|
754
828
  c.desc 'Flag the last entry containing TAG.
755
829
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
756
830
  c.arg_name 'TAG'
757
- c.flag [:tag]
831
+ c.flag [:tag], type: TagArray
758
832
 
759
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")'
760
834
  c.arg_name 'QUERY'
@@ -799,7 +873,7 @@ command %i[mark flag] do |c|
799
873
  if options[:tag].nil?
800
874
  search_tags = []
801
875
  else
802
- search_tags = options[:tag].to_tags
876
+ search_tags = options[:tag]
803
877
  end
804
878
 
805
879
  if options[:interactive]
@@ -881,6 +955,9 @@ command :meanwhile do |c|
881
955
  c.arg_name 'TEXT'
882
956
  c.flag %i[n note]
883
957
 
958
+ c.desc 'Prompt for note via multi-line input'
959
+ c.switch %i[ask], negatable: false, default_value: false
960
+
884
961
  c.action do |_global_options, options, args|
885
962
  if options[:back]
886
963
  date = options[:back].chronify(guess: :begin)
@@ -897,10 +974,15 @@ command :meanwhile do |c|
897
974
  end
898
975
  input = ''
899
976
 
977
+ ask_note = options[:ask] ? Doing::Prompt.read_lines(prompt: 'Add a note') : []
978
+
900
979
  if options[:editor]
901
980
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
902
981
  input += date.strftime('%F %R | ')
903
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
+
904
986
  input = wwid.fork_editor(input).strip
905
987
  elsif !args.empty?
906
988
  input = args.join(' ')
@@ -919,10 +1001,9 @@ command :meanwhile do |c|
919
1001
  note = []
920
1002
  end
921
1003
 
922
- if options[:note]
923
- note.push(options[:note])
924
- elsif note.empty?
925
- note = nil
1004
+ unless options[:editor]
1005
+ note.add(options[:note]) if options[:note]
1006
+ note.add(ask_note) unless ask_note.empty?
926
1007
  end
927
1008
 
928
1009
  wwid.stop_start('meanwhile', { new_item: input, back: date, section: section, archive: options[:archive], note: note })
@@ -958,7 +1039,7 @@ command :note do |c|
958
1039
 
959
1040
  c.desc 'Add/remove note from last entry matching tag. Wildcards allowed (*, ?)'
960
1041
  c.arg_name 'TAG'
961
- c.flag [:tag]
1042
+ c.flag [:tag], type: TagArray
962
1043
 
963
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")'
964
1045
  c.arg_name 'QUERY'
@@ -988,6 +1069,9 @@ command :note do |c|
988
1069
  c.desc 'Select item for new note from a menu of matching entries'
989
1070
  c.switch %i[i interactive], negatable: false, default_value: false
990
1071
 
1072
+ c.desc 'Prompt for note via multi-line input'
1073
+ c.switch %i[ask], negatable: false, default_value: false
1074
+
991
1075
  c.action do |_global_options, options, args|
992
1076
  options[:fuzzy] = false
993
1077
  if options[:section]
@@ -1014,8 +1098,9 @@ command :note do |c|
1014
1098
 
1015
1099
  last_note = last_entry.note || Doing::Note.new
1016
1100
  new_note = Doing::Note.new
1101
+ ask_note = options[:ask] ? Doing::Prompt.read_lines(prompt: 'Add a note') : ''
1017
1102
 
1018
- 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])
1019
1104
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
1020
1105
 
1021
1106
  input = !args.empty? ? args.join(' ') : ''
@@ -1026,7 +1111,9 @@ command :note do |c|
1026
1111
  prev_input = last_entry.note || Doing::Note.new
1027
1112
  end
1028
1113
 
1114
+
1029
1115
  input = prev_input.add(input)
1116
+ input.add(ask_note) unless ask_note.empty?
1030
1117
 
1031
1118
  input = wwid.fork_editor(prev_input.strip_lines.join("\n"), message: nil).strip
1032
1119
  note = input
@@ -1037,9 +1124,12 @@ command :note do |c|
1037
1124
  elsif $stdin.stat.size.positive?
1038
1125
  new_note.add($stdin.read.strip)
1039
1126
  else
1040
- 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
+
1041
1129
  end
1042
1130
 
1131
+ new_note.add(ask_note) unless ask_note.empty?
1132
+
1043
1133
  if last_note.equal?(new_note)
1044
1134
  Doing.logger.debug('Skipped:', 'No note change')
1045
1135
  else
@@ -1058,10 +1148,13 @@ long_desc %(Record what you're starting now, or backdate the start time using na
1058
1148
 
1059
1149
  A parenthetical at the end of the entry will be converted to a note.
1060
1150
 
1061
- 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}.)
1062
1154
  arg_name 'ENTRY'
1063
1155
  command %i[now next] do |c|
1064
- 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"
1065
1158
  c.example 'doing now working on a new project', desc: 'Add a new entry at the current time'
1066
1159
  c.example 'doing now debugging @project2', desc: 'Add an entry with a tag'
1067
1160
  c.example 'doing now adding an entry (with a note)', desc: 'Parenthetical at end is converted to note'
@@ -1085,6 +1178,9 @@ command %i[now next] do |c|
1085
1178
  c.arg_name 'TEXT'
1086
1179
  c.flag %i[n note]
1087
1180
 
1181
+ c.desc 'Prompt for note via multi-line input'
1182
+ c.switch %i[ask], negatable: false, default_value: false
1183
+
1088
1184
  # c.desc "Edit entry with specified app"
1089
1185
  # c.arg_name 'editor_app'
1090
1186
  # # c.flag [:a, :app]
@@ -1104,23 +1200,32 @@ command %i[now next] do |c|
1104
1200
  options[:section] = settings['current_section']
1105
1201
  end
1106
1202
 
1107
- 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]
1108
1206
  raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
1109
1207
 
1110
1208
  input = date.strftime('%F %R | ')
1111
1209
  input += args.join(' ') unless args.empty?
1210
+ input += "\n#{options[:note]}" if options[:note]
1211
+ input += "\n#{ask_note}" unless ask_note.empty?
1112
1212
  input = wwid.fork_editor(input).strip
1113
1213
 
1114
- raise EmptyInput, 'No content' if input.empty?
1115
-
1116
1214
  date, title, note = wwid.format_input(input)
1117
- 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
+
1118
1222
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1119
1223
  wwid.write(wwid.doing_file)
1120
1224
  elsif args.length.positive?
1121
1225
  d, title, note = wwid.format_input(args.join(' '))
1122
1226
  date = d.nil? ? date : d
1123
1227
  note.add(options[:note]) if options[:note]
1228
+ note.add(ask_note) unless ask_note.empty?
1124
1229
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1125
1230
  wwid.write(wwid.doing_file)
1126
1231
  elsif $stdin.stat.size.positive?
@@ -1131,10 +1236,26 @@ command %i[now next] do |c|
1131
1236
  date = d
1132
1237
  end
1133
1238
  note.add(options[:note]) if options[:note]
1239
+ if ask_note.empty? && options[:ask]
1240
+ ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
1241
+ note.add(ask_note) unless ask_note.empty?
1242
+ end
1134
1243
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1135
1244
  wwid.write(wwid.doing_file)
1136
1245
  else
1137
- raise EmptyInput, 'You must provide content when creating a new entry'
1246
+ tags = wwid.all_tags(wwid.content)
1247
+ $stderr.puts Doing::Color.boldgreen("Add a new entry. Tab will autocomplete known tags. Ctrl-c to cancel.")
1248
+ title = Doing::Prompt.read_line(prompt: 'Entry content', completions: tags)
1249
+ raise EmptyInput, 'You must provide content when creating a new entry' if title.strip.empty?
1250
+
1251
+ note = Doing::Note.new
1252
+ note.add(options[:note]) if options[:note]
1253
+ res = Doing::Prompt.yn('Add a note', default_response: false)
1254
+ ask_note = res ? Doing::Prompt.read_lines(prompt: 'Enter note') : []
1255
+ note.add(ask_note)
1256
+
1257
+ wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1258
+ wwid.write(wwid.doing_file)
1138
1259
  end
1139
1260
  end
1140
1261
  end
@@ -1283,7 +1404,11 @@ command :select do |c|
1283
1404
 
1284
1405
  c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"'
1285
1406
  c.arg_name 'QUERY'
1286
- c.flag %i[q query search]
1407
+ c.flag %i[q query]
1408
+
1409
+ c.desc 'Select from entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
1410
+ c.arg_name 'QUERY'
1411
+ c.flag [:search]
1287
1412
 
1288
1413
  c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1289
1414
  c.arg_name 'QUERY'
@@ -1421,7 +1546,7 @@ command :tag do |c|
1421
1546
  c.desc 'Tag the last X entries containing TAG.
1422
1547
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
1423
1548
  c.arg_name 'TAG'
1424
- c.flag [:tag]
1549
+ c.flag [:tag], type: TagArray
1425
1550
 
1426
1551
  c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
1427
1552
  c.arg_name 'QUERY'
@@ -1453,7 +1578,7 @@ command :tag do |c|
1453
1578
 
1454
1579
  c.action do |_global_options, options, args|
1455
1580
  options[:fuzzy] = false
1456
- raise MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:autotag]
1581
+ # raise MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:autotag]
1457
1582
 
1458
1583
  raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1459
1584
 
@@ -1467,17 +1592,21 @@ command :tag do |c|
1467
1592
  if options[:tag].nil?
1468
1593
  search_tags = []
1469
1594
  else
1470
- search_tags = options[:tag].to_tags
1595
+ search_tags = options[:tag]
1471
1596
  end
1472
1597
 
1473
1598
  if options[:autotag]
1474
1599
  tags = []
1475
1600
  else
1476
- tags = if args.join('') =~ /,/
1477
- args.join('').split(/,/)
1478
- else
1479
- args.join(' ').split(' ') # in case tags are quoted as one arg
1480
- end
1601
+ if args.empty?
1602
+ tags = []
1603
+ else
1604
+ tags = if args.join('') =~ /,/
1605
+ args.join('').split(/ *, */)
1606
+ else
1607
+ args.join(' ').split(' ') # in case tags are quoted as one arg
1608
+ end
1609
+ end
1481
1610
 
1482
1611
  tags.map! { |tag| tag.sub(/^@/, '').strip }
1483
1612
  end
@@ -1621,6 +1750,9 @@ command %i[grep search] do |c|
1621
1750
  c.arg_name 'TYPE'
1622
1751
  c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1623
1752
 
1753
+ c.desc "Highlight search matches in output. Only affects command line output"
1754
+ c.switch %i[h hilite], default_value: settings.dig('search', 'highlight')
1755
+
1624
1756
  c.desc "Edit matching entries with #{Doing::Util.default_editor}"
1625
1757
  c.switch %i[e editor], negatable: false, default_value: false
1626
1758
 
@@ -1688,7 +1820,7 @@ command :last do |c|
1688
1820
 
1689
1821
  c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?)'
1690
1822
  c.arg_name 'TAG'
1691
- c.flag [:tag]
1823
+ c.flag [:tag], type: TagArray
1692
1824
 
1693
1825
  c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans'
1694
1826
  c.arg_name 'BOOLEAN'
@@ -1698,6 +1830,9 @@ command :last do |c|
1698
1830
  c.arg_name 'QUERY'
1699
1831
  c.flag [:search]
1700
1832
 
1833
+ c.desc "Highlight search matches in output. Only affects command line output"
1834
+ c.switch %i[h hilite], default_value: settings.dig('search', 'highlight')
1835
+
1701
1836
  c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1702
1837
  c.arg_name 'QUERY'
1703
1838
  c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
@@ -1725,7 +1860,7 @@ command :last do |c|
1725
1860
  if options[:tag].nil?
1726
1861
  options[:tag] = []
1727
1862
  else
1728
- options[:tag] = options[:tag].to_tags
1863
+ options[:tag] = options[:tag]
1729
1864
  options[:bool] = options[:bool].normalize_bool
1730
1865
  end
1731
1866
 
@@ -1752,6 +1887,7 @@ command :last do |c|
1752
1887
  search: options[:search],
1753
1888
  fuzzy: options[:fuzzy],
1754
1889
  case: options[:case],
1890
+ hilite: options[:hilite],
1755
1891
  negate: options[:not],
1756
1892
  tag: options[:tag],
1757
1893
  tag_bool: options[:bool],
@@ -1855,7 +1991,7 @@ command :show do |c|
1855
1991
 
1856
1992
  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'
1857
1993
  c.arg_name 'TAG'
1858
- c.flag [:tag]
1994
+ c.flag [:tag], type: TagArray
1859
1995
 
1860
1996
  c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1861
1997
  c.arg_name 'QUERY'
@@ -1897,6 +2033,9 @@ command :show do |c|
1897
2033
  c.arg_name 'QUERY'
1898
2034
  c.flag [:search]
1899
2035
 
2036
+ c.desc "Highlight search matches in output. Only affects command line output"
2037
+ c.switch %i[h hilite], default_value: settings.dig('search', 'highlight')
2038
+
1900
2039
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
1901
2040
  # c.switch [:fuzzy], default_value: false, negatable: false
1902
2041
 
@@ -1988,7 +2127,7 @@ command :show do |c|
1988
2127
  section ||= 'All'
1989
2128
  end
1990
2129
 
1991
- tags.concat(options[:tag].to_tags) if options[:tag]
2130
+ tags.concat(options[:tag]) if options[:tag]
1992
2131
 
1993
2132
  options[:times] = true if options[:totals]
1994
2133
 
@@ -2044,6 +2183,7 @@ command :show do |c|
2044
2183
  opt[:sort_tags] = options[:tag_sort] =~ /^n/i
2045
2184
  opt[:count] = options[:count].to_i
2046
2185
  opt[:highlight] = true
2186
+ opt[:hilite] = options[:hilite]
2047
2187
  opt[:order] = options[:sort].normalize_order
2048
2188
  opt[:tag] = nil
2049
2189
  opt[:tag_order] = options[:tag_order].normalize_order
@@ -2370,6 +2510,9 @@ command :view do |c|
2370
2510
  c.arg_name 'QUERY'
2371
2511
  c.flag [:search]
2372
2512
 
2513
+ c.desc "Highlight search matches in output. Only affects command line output"
2514
+ c.switch %i[h hilite], default_value: settings.dig('search', 'highlight')
2515
+
2373
2516
  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
2374
2517
  # c.switch [:fuzzy], default_value: false, negatable: false
2375
2518
 
@@ -2530,6 +2673,7 @@ command :view do |c|
2530
2673
  opts[:count] = count
2531
2674
  opts[:format] = date_format
2532
2675
  opts[:highlight] = options[:color]
2676
+ opts[:hilite] = options[:hilite]
2533
2677
  opts[:only_timed] = only_timed
2534
2678
  opts[:order] = order
2535
2679
  opts[:output] = options[:interactive] ? nil : options[:output]
@@ -3004,7 +3148,7 @@ command %i[archive move] do |c|
3004
3148
 
3005
3149
  c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands'
3006
3150
  c.arg_name 'TAG'
3007
- c.flag [:tag]
3151
+ c.flag [:tag], type: TagArray
3008
3152
 
3009
3153
  c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans'
3010
3154
  c.arg_name 'BOOLEAN'
@@ -3053,7 +3197,7 @@ command %i[archive move] do |c|
3053
3197
 
3054
3198
  raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
3055
3199
 
3056
- tags.concat(options[:tag].to_tags) if options[:tag]
3200
+ tags.concat(options[:tag]) if options[:tag]
3057
3201
 
3058
3202
  search = nil
3059
3203
 
@@ -3113,7 +3257,7 @@ command :import do |c|
3113
3257
 
3114
3258
  c.desc 'Tag all imported entries'
3115
3259
  c.arg_name 'TAGS'
3116
- c.flag :tag
3260
+ c.flag %i[t tag]
3117
3261
 
3118
3262
  c.desc 'Autotag entries'
3119
3263
  c.switch :autotag, negatable: true, default_value: true
@@ -3148,6 +3292,12 @@ command :import do |c|
3148
3292
  options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
3149
3293
  end
3150
3294
 
3295
+ if options[:search]
3296
+ search = options[:search]
3297
+ search.sub!(/^'?/, "'") if options[:exact]
3298
+ options[:search] = search
3299
+ end
3300
+
3151
3301
  if options[:from]
3152
3302
  date_string = options[:from]
3153
3303
  if date_string =~ / (to|through|thru|(un)?til|-+) /
@@ -3156,7 +3306,7 @@ command :import do |c|
3156
3306
  finish = dates[2].chronify(guess: :end)
3157
3307
  else
3158
3308
  start = date_string.chronify(guess: :begin)
3159
- finish = false
3309
+ finish = date_string.chronify(guess: :end)
3160
3310
  end
3161
3311
  raise InvalidTimeExpression, 'Unrecognized date string' unless start
3162
3312
  dates = [start, finish]