doing 2.1.13 → 2.1.17

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