doing 2.0.5.pre → 2.0.9.pre
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -1
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/bin/doing +446 -111
- data/doing.rdoc +371 -13
- data/generate_completions.sh +1 -0
- data/lib/completion/_doing.zsh +179 -127
- data/lib/completion/doing.bash +60 -27
- data/lib/completion/doing.fish +74 -23
- data/lib/doing/cli_status.rb +4 -0
- data/lib/doing/errors.rb +22 -15
- data/lib/doing/item.rb +18 -12
- data/lib/doing/log_adapter.rb +27 -25
- data/lib/doing/plugin_manager.rb +1 -1
- data/lib/doing/plugins/import/calendar_import.rb +7 -1
- data/lib/doing/plugins/import/doing_import.rb +6 -6
- data/lib/doing/plugins/import/timing_import.rb +7 -1
- data/lib/doing/string.rb +29 -6
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +120 -86
- data/lib/examples/commands/autotag.rb +63 -0
- data/lib/examples/plugins/{templates → wiki_export/templates}/wiki.css +0 -0
- data/lib/examples/plugins/{templates → wiki_export/templates}/wiki.haml +0 -0
- data/lib/examples/plugins/{templates → wiki_export/templates}/wiki_index.haml +0 -0
- data/lib/examples/plugins/{wiki_export.rb → wiki_export/wiki_export.rb} +0 -0
- data/scripts/generate_bash_completions.rb +3 -2
- data/scripts/generate_fish_completions.rb +4 -1
- data/scripts/generate_zsh_completions.rb +44 -39
- metadata +6 -6
- data/doing.fish +0 -278
data/bin/doing
CHANGED
@@ -57,12 +57,15 @@ config = Doing.config
|
|
57
57
|
settings = config.settings
|
58
58
|
wwid.config = settings
|
59
59
|
|
60
|
-
if
|
61
|
-
commands_from File.expand_path(
|
60
|
+
if settings.dig('plugins', 'command_path')
|
61
|
+
commands_from File.expand_path(settings.dig('plugins', 'command_path'))
|
62
62
|
end
|
63
63
|
|
64
64
|
program_desc 'A CLI for a What Was I Doing system'
|
65
|
-
program_long_desc %(Doing uses a TaskPaper-like formatting to keep a plain text
|
65
|
+
program_long_desc %(Doing uses a TaskPaper-like formatting to keep a plain text
|
66
|
+
record of what you've been doing, complete with tag-based time tracking. The
|
67
|
+
command line tool allows you to add entries, annotate with tags and notes, and
|
68
|
+
view your entries with myriad options, with a focus on a "natural" language syntax.)
|
66
69
|
|
67
70
|
default_command :recent
|
68
71
|
# sort_help :manually
|
@@ -137,7 +140,7 @@ command %i[now next] do |c|
|
|
137
140
|
if options[:back]
|
138
141
|
date = wwid.chronify(options[:back], guess: :begin)
|
139
142
|
|
140
|
-
raise
|
143
|
+
raise InvalidTimeExpression.new('unable to parse date string', topic: 'Date parser:') if date.nil?
|
141
144
|
else
|
142
145
|
date = Time.now
|
143
146
|
end
|
@@ -149,13 +152,13 @@ command %i[now next] do |c|
|
|
149
152
|
end
|
150
153
|
|
151
154
|
if options[:e] || (args.empty? && $stdin.stat.size.zero?)
|
152
|
-
raise
|
155
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
153
156
|
|
154
157
|
input = ''
|
155
158
|
input += args.join(' ') unless args.empty?
|
156
159
|
input = wwid.fork_editor(input).strip
|
157
160
|
|
158
|
-
raise
|
161
|
+
raise EmptyInput, 'No content' if input.empty?
|
159
162
|
|
160
163
|
title, note = wwid.format_input(input)
|
161
164
|
note.push(options[:n]) if options[:n]
|
@@ -173,14 +176,14 @@ command %i[now next] do |c|
|
|
173
176
|
wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
|
174
177
|
wwid.write(wwid.doing_file)
|
175
178
|
else
|
176
|
-
raise
|
179
|
+
raise EmptyInput, 'You must provide content when creating a new entry'
|
177
180
|
end
|
178
181
|
end
|
179
182
|
end
|
180
183
|
|
181
184
|
desc 'Reset the start time of an entry'
|
182
185
|
command %i[reset begin] do |c|
|
183
|
-
c.desc '
|
186
|
+
c.desc 'Limit search to section'
|
184
187
|
c.arg_name 'NAME'
|
185
188
|
c.flag %i[s section], default_value: 'All'
|
186
189
|
|
@@ -195,6 +198,16 @@ command %i[reset begin] do |c|
|
|
195
198
|
c.arg_name 'QUERY'
|
196
199
|
c.flag [:search]
|
197
200
|
|
201
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
202
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
203
|
+
|
204
|
+
c.desc 'Reset items that *don\'t* match search/tag filters'
|
205
|
+
c.switch [:not], default_value: false, negatable: false
|
206
|
+
|
207
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
208
|
+
c.arg_name 'TYPE'
|
209
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
210
|
+
|
198
211
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
199
212
|
c.arg_name 'BOOLEAN'
|
200
213
|
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
@@ -207,7 +220,16 @@ command %i[reset begin] do |c|
|
|
207
220
|
options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
|
208
221
|
end
|
209
222
|
|
210
|
-
options[:
|
223
|
+
options[:bool] = options[:bool].normalize_bool
|
224
|
+
|
225
|
+
options[:case] = options[:case].normalize_case
|
226
|
+
|
227
|
+
if options[:search]
|
228
|
+
search = options[:search]
|
229
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
230
|
+
options[:search] = search
|
231
|
+
end
|
232
|
+
|
211
233
|
|
212
234
|
items = wwid.filter_items([], opt: options)
|
213
235
|
|
@@ -248,6 +270,11 @@ long_desc %(
|
|
248
270
|
)
|
249
271
|
arg_name 'NOTE_TEXT'
|
250
272
|
command :note do |c|
|
273
|
+
c.example 'doing note', desc: 'Open the last entry in $EDITOR to append a note'
|
274
|
+
c.example 'doing note "Just a quick annotation"', desc: 'Add a quick note to the last entry'
|
275
|
+
c.example 'doing note --tag done "Keeping it real or something"', desc: 'Add a note to the last item tagged @done'
|
276
|
+
c.example 'doing note --search "late night" -e', desc: 'Open $EDITOR to add a note to the last item containing "late night" (fuzzy matched)'
|
277
|
+
|
251
278
|
c.desc 'Section'
|
252
279
|
c.arg_name 'NAME'
|
253
280
|
c.flag %i[s section], default_value: 'All'
|
@@ -266,6 +293,16 @@ command :note do |c|
|
|
266
293
|
c.arg_name 'QUERY'
|
267
294
|
c.flag [:search]
|
268
295
|
|
296
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
297
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
298
|
+
|
299
|
+
c.desc 'Add note to item that *doesn\'t* match search/tag filters'
|
300
|
+
c.switch [:not], default_value: false, negatable: false
|
301
|
+
|
302
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
303
|
+
c.arg_name 'TYPE'
|
304
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
305
|
+
|
269
306
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
270
307
|
c.arg_name 'BOOLEAN'
|
271
308
|
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
@@ -280,6 +317,15 @@ command :note do |c|
|
|
280
317
|
|
281
318
|
options[:tag_bool] = options[:bool].normalize_bool
|
282
319
|
|
320
|
+
options[:case] = options[:case].normalize_case
|
321
|
+
|
322
|
+
if options[:search]
|
323
|
+
search = options[:search]
|
324
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
325
|
+
options[:search] = search
|
326
|
+
end
|
327
|
+
|
328
|
+
|
283
329
|
last_entry = wwid.last_entry(options)
|
284
330
|
|
285
331
|
unless last_entry
|
@@ -291,7 +337,7 @@ command :note do |c|
|
|
291
337
|
new_note = Doing::Note.new
|
292
338
|
|
293
339
|
if options[:e] || (args.empty? && $stdin.stat.size.zero? && !options[:r])
|
294
|
-
raise
|
340
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
295
341
|
|
296
342
|
input = !args.empty? ? args.join(' ') : ''
|
297
343
|
|
@@ -312,7 +358,7 @@ command :note do |c|
|
|
312
358
|
elsif $stdin.stat.size.positive?
|
313
359
|
new_note.add($stdin.read)
|
314
360
|
else
|
315
|
-
raise
|
361
|
+
raise EmptyInput, 'You must provide content when adding a note' unless options[:remove]
|
316
362
|
end
|
317
363
|
|
318
364
|
if last_note.equal?(new_note)
|
@@ -330,6 +376,11 @@ end
|
|
330
376
|
desc 'Finish any running @meanwhile tasks and optionally create a new one'
|
331
377
|
arg_name 'ENTRY'
|
332
378
|
command :meanwhile do |c|
|
379
|
+
c.example 'doing meanwhile "Long task that will have others after it before it\'s done"', desc: 'Add a new long-running entry, completing any current @meanwhile entry'
|
380
|
+
c.example 'doing meanwhile', desc: 'Finish any open @meanwhile entry'
|
381
|
+
c.example 'doing meanwhile --archive', desc: 'Finish any open @meanwhile entry and archive it'
|
382
|
+
c.example 'doing meanwhile --back 2h "Something I\'ve been working on for a while', desc: 'Add a @meanwhile entry with a start date 2 hours ago'
|
383
|
+
|
333
384
|
c.desc 'Section'
|
334
385
|
c.arg_name 'NAME'
|
335
386
|
c.flag %i[s section]
|
@@ -338,7 +389,7 @@ command :meanwhile do |c|
|
|
338
389
|
c.switch %i[e editor], negatable: false, default_value: false
|
339
390
|
|
340
391
|
c.desc 'Archive previous @meanwhile entry'
|
341
|
-
c.switch %i[a archive], default_value: false
|
392
|
+
c.switch %i[a archive], negatable: false, default_value: false
|
342
393
|
|
343
394
|
c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]'
|
344
395
|
c.arg_name 'DATE_STRING'
|
@@ -352,7 +403,7 @@ command :meanwhile do |c|
|
|
352
403
|
if options[:back]
|
353
404
|
date = wwid.chronify(options[:back], guess: :begin)
|
354
405
|
|
355
|
-
raise
|
406
|
+
raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
356
407
|
else
|
357
408
|
date = Time.now
|
358
409
|
end
|
@@ -365,7 +416,7 @@ command :meanwhile do |c|
|
|
365
416
|
input = ''
|
366
417
|
|
367
418
|
if options[:e]
|
368
|
-
raise
|
419
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
369
420
|
|
370
421
|
input += args.join(' ') unless args.empty?
|
371
422
|
input = wwid.fork_editor(input).strip
|
@@ -396,14 +447,14 @@ end
|
|
396
447
|
desc 'Output HTML, CSS, and Markdown (ERB) templates for customization'
|
397
448
|
long_desc %(
|
398
449
|
Templates are printed to STDOUT for piping to a file.
|
399
|
-
Save them and use them in the configuration file under
|
400
|
-
|
401
|
-
Example `doing template haml > ~/styles/my_doing.haml`
|
450
|
+
Save them and use them in the configuration file under export_templates.
|
402
451
|
)
|
403
452
|
arg_name 'TYPE', must_match: Doing::Plugins.template_regex
|
404
453
|
command :template do |c|
|
454
|
+
c.example 'doing template haml > ~/styles/my_doing.haml', desc: 'Output the haml template and save it to a file'
|
455
|
+
|
405
456
|
c.desc 'List all available templates'
|
406
|
-
c.switch %i[l list]
|
457
|
+
c.switch %i[l list], negatable: false
|
407
458
|
|
408
459
|
c.desc 'List in single column for completion'
|
409
460
|
c.switch %i[c]
|
@@ -424,7 +475,7 @@ command :template do |c|
|
|
424
475
|
type = args[0]
|
425
476
|
end
|
426
477
|
|
427
|
-
raise
|
478
|
+
raise InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type
|
428
479
|
|
429
480
|
$stdout.puts Doing::Plugins.template_for_trigger(type)
|
430
481
|
|
@@ -473,6 +524,16 @@ command :select do |c|
|
|
473
524
|
c.arg_name 'QUERY'
|
474
525
|
c.flag %i[q query search]
|
475
526
|
|
527
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
528
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
529
|
+
|
530
|
+
c.desc 'Select items that *don\'t* match search/tag filters'
|
531
|
+
c.switch [:not], default_value: false, negatable: false
|
532
|
+
|
533
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
534
|
+
c.arg_name 'TYPE'
|
535
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
536
|
+
|
476
537
|
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.'
|
477
538
|
c.switch %i[menu], negatable: true, default_value: true
|
478
539
|
|
@@ -503,12 +564,14 @@ command :select do |c|
|
|
503
564
|
c.flag %i[o output]
|
504
565
|
|
505
566
|
c.desc "Copy selection as a new entry with current time and no @done tag. Only works with single selections. Can be combined with --editor."
|
506
|
-
c.switch %i[again resume]
|
567
|
+
c.switch %i[again resume], negatable: false, default_value: false
|
507
568
|
|
508
569
|
c.action do |_global_options, options, args|
|
509
|
-
raise
|
570
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
510
571
|
|
511
|
-
raise
|
572
|
+
raise InvalidArgument, '--no-menu requires --query' if !options[:menu] && !options[:query]
|
573
|
+
|
574
|
+
options[:case] = options[:case].normalize_case
|
512
575
|
|
513
576
|
wwid.interactive(options)
|
514
577
|
end
|
@@ -517,6 +580,9 @@ end
|
|
517
580
|
desc 'Add an item to the Later section'
|
518
581
|
arg_name 'ENTRY'
|
519
582
|
command :later do |c|
|
583
|
+
c.example 'doing later "Something I\'ll think about tomorrow"', desc: 'Add an entry to the Later section'
|
584
|
+
c.example 'doing later -e', desc: 'Open $EDITOR to create an entry and optional note'
|
585
|
+
|
520
586
|
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
521
587
|
c.switch %i[e editor], negatable: false, default_value: false
|
522
588
|
|
@@ -531,17 +597,17 @@ command :later do |c|
|
|
531
597
|
c.action do |_global_options, options, args|
|
532
598
|
if options[:back]
|
533
599
|
date = wwid.chronify(options[:back], guess: :begin)
|
534
|
-
raise
|
600
|
+
raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
535
601
|
else
|
536
602
|
date = Time.now
|
537
603
|
end
|
538
604
|
|
539
605
|
if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
|
540
|
-
raise
|
606
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
541
607
|
|
542
608
|
input = args.empty? ? '' : args.join(' ')
|
543
609
|
input = wwid.fork_editor(input).strip
|
544
|
-
raise
|
610
|
+
raise EmptyInput, 'No content' unless input && !input.empty?
|
545
611
|
|
546
612
|
title, note = wwid.format_input(input)
|
547
613
|
note.push(options[:n]) if options[:n]
|
@@ -558,7 +624,7 @@ command :later do |c|
|
|
558
624
|
wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
|
559
625
|
wwid.write(wwid.doing_file)
|
560
626
|
else
|
561
|
-
raise
|
627
|
+
raise EmptyInput, 'You must provide content when creating a new entry'
|
562
628
|
end
|
563
629
|
end
|
564
630
|
end
|
@@ -566,6 +632,11 @@ end
|
|
566
632
|
desc 'Add a completed item with @done(date). No argument finishes last entry.'
|
567
633
|
arg_name 'ENTRY'
|
568
634
|
command %i[done did] do |c|
|
635
|
+
c.example 'doing done', desc: 'Tag the last entry @done'
|
636
|
+
c.example 'doing done I already finished this', desc: 'Add a new entry and immediately mark it @done'
|
637
|
+
c.example 'doing done --back 30m This took me half an hour', desc: 'Add an entry with a start date 30 minutes ago and a @done date of right now'
|
638
|
+
c.example 'doing done --at 3pm --took 1h Started and finished this afternoon', desc: 'Add an entry with a @done date of 3pm and a start date of 2pm (3pm - 1h)'
|
639
|
+
|
569
640
|
c.desc 'Remove @done tag'
|
570
641
|
c.switch %i[r remove], negatable: false, default_value: false
|
571
642
|
|
@@ -614,19 +685,19 @@ command %i[done did] do |c|
|
|
614
685
|
|
615
686
|
if options[:took]
|
616
687
|
took = wwid.chronify_qty(options[:took])
|
617
|
-
raise
|
688
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
618
689
|
end
|
619
690
|
|
620
691
|
if options[:back]
|
621
692
|
date = wwid.chronify(options[:back], guess: :begin)
|
622
|
-
raise
|
693
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
|
623
694
|
else
|
624
695
|
date = options[:took] ? Time.now - took : Time.now
|
625
696
|
end
|
626
697
|
|
627
698
|
if options[:at]
|
628
699
|
finish_date = wwid.chronify(options[:at], guess: :begin)
|
629
|
-
raise
|
700
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
|
630
701
|
|
631
702
|
date = options[:took] ? finish_date - took : finish_date
|
632
703
|
elsif options[:took]
|
@@ -651,7 +722,7 @@ command %i[done did] do |c|
|
|
651
722
|
note.add(options[:note]) if options[:note]
|
652
723
|
|
653
724
|
if options[:editor]
|
654
|
-
raise
|
725
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
655
726
|
is_new = false
|
656
727
|
|
657
728
|
if args.empty?
|
@@ -659,7 +730,7 @@ command %i[done did] do |c|
|
|
659
730
|
|
660
731
|
unless last_entry
|
661
732
|
Doing.logger.debug('Skipped:', options[:unfinished] ? 'No unfinished entry' : 'Last entry already @done')
|
662
|
-
raise
|
733
|
+
raise NoResults, 'No results'
|
663
734
|
end
|
664
735
|
|
665
736
|
old_entry = last_entry.dup
|
@@ -671,7 +742,7 @@ command %i[done did] do |c|
|
|
671
742
|
end
|
672
743
|
|
673
744
|
input = wwid.fork_editor(input).strip
|
674
|
-
raise
|
745
|
+
raise EmptyInput, 'No content' unless input && !input.empty?
|
675
746
|
|
676
747
|
title, note = wwid.format_input(input)
|
677
748
|
new_entry = Doing::Item.new(date, title, section, note)
|
@@ -746,7 +817,7 @@ command %i[done did] do |c|
|
|
746
817
|
wwid.write(wwid.doing_file)
|
747
818
|
Doing.logger.info('Entry Added:', new_entry.title)
|
748
819
|
else
|
749
|
-
raise
|
820
|
+
raise EmptyInput, 'You must provide content when creating a new entry'
|
750
821
|
end
|
751
822
|
end
|
752
823
|
end
|
@@ -755,6 +826,9 @@ desc 'End last X entries with no time tracked'
|
|
755
826
|
long_desc 'Adds @done tag without datestamp so no elapsed time is recorded. Alias for `doing finish --no-date`.'
|
756
827
|
arg_name 'COUNT'
|
757
828
|
command :cancel do |c|
|
829
|
+
c.example 'doing cancel', desc: 'Cancel the last entry'
|
830
|
+
c.example 'doing cancel --tag project1 -u 5', desc: 'Cancel the last 5 unfinished entries containing @project1'
|
831
|
+
|
758
832
|
c.desc 'Archive entries'
|
759
833
|
c.switch %i[a archive], negatable: false, default_value: false
|
760
834
|
|
@@ -770,6 +844,20 @@ command :cancel do |c|
|
|
770
844
|
c.arg_name 'BOOLEAN'
|
771
845
|
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
772
846
|
|
847
|
+
c.desc 'Cancel the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
|
848
|
+
c.arg_name 'QUERY'
|
849
|
+
c.flag [:search]
|
850
|
+
|
851
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
852
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
853
|
+
|
854
|
+
c.desc 'Finish items that *don\'t* match search/tag filters'
|
855
|
+
c.switch [:not], default_value: false, negatable: false
|
856
|
+
|
857
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
858
|
+
c.arg_name 'TYPE'
|
859
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
860
|
+
|
773
861
|
c.desc 'Cancel last entry (or entries) not already marked @done'
|
774
862
|
c.switch %i[u unfinished], negatable: false, default_value: false
|
775
863
|
|
@@ -789,9 +877,9 @@ command :cancel do |c|
|
|
789
877
|
tags = options[:tag].to_tags
|
790
878
|
end
|
791
879
|
|
792
|
-
raise
|
880
|
+
raise InvalidArgument, 'Only one argument allowed' if args.length > 1
|
793
881
|
|
794
|
-
raise
|
882
|
+
raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
|
795
883
|
|
796
884
|
if options[:interactive]
|
797
885
|
count = 0
|
@@ -799,7 +887,17 @@ command :cancel do |c|
|
|
799
887
|
count = args[0] ? args[0].to_i : 1
|
800
888
|
end
|
801
889
|
|
890
|
+
search = nil
|
891
|
+
|
892
|
+
if options[:search]
|
893
|
+
search = options[:search]
|
894
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
895
|
+
end
|
896
|
+
|
802
897
|
opts = {
|
898
|
+
search: search,
|
899
|
+
case: options[:case].normalize_case,
|
900
|
+
not: options[:not],
|
803
901
|
archive: options[:a],
|
804
902
|
count: count,
|
805
903
|
date: false,
|
@@ -820,6 +918,10 @@ desc 'Mark last X entries as @done'
|
|
820
918
|
long_desc 'Marks the last X entries with a @done tag and current date. Does not alter already completed entries.'
|
821
919
|
arg_name 'COUNT'
|
822
920
|
command :finish do |c|
|
921
|
+
c.example 'doing finish', desc: 'Mark the last entry @done'
|
922
|
+
c.example 'doing finish --auto --section Later 10', desc: 'Add @done to any unfinished entries in the last 10 in Later, setting the finish time based on the start time of the task after it'
|
923
|
+
c.example 'doing finish --search "a specific entry" --at "yesterday 3pm"', desc: 'Search for an entry containing string and set its @done time to yesterday at 3pm'
|
924
|
+
|
823
925
|
c.desc 'Include date'
|
824
926
|
c.switch [:date], negatable: true, default_value: true
|
825
927
|
|
@@ -844,6 +946,16 @@ command :finish do |c|
|
|
844
946
|
c.arg_name 'QUERY'
|
845
947
|
c.flag [:search]
|
846
948
|
|
949
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
950
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
951
|
+
|
952
|
+
c.desc 'Finish items that *don\'t* match search/tag filters'
|
953
|
+
c.switch [:not], default_value: false, negatable: false
|
954
|
+
|
955
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
956
|
+
c.arg_name 'TYPE'
|
957
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
958
|
+
|
847
959
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
848
960
|
c.arg_name 'BOOLEAN'
|
849
961
|
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
@@ -873,22 +985,22 @@ command :finish do |c|
|
|
873
985
|
unless options[:auto]
|
874
986
|
if options[:took]
|
875
987
|
took = wwid.chronify_qty(options[:took])
|
876
|
-
raise
|
988
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
877
989
|
end
|
878
990
|
|
879
|
-
raise
|
991
|
+
raise InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
|
880
992
|
|
881
|
-
raise
|
993
|
+
raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
882
994
|
|
883
995
|
if options[:at]
|
884
996
|
finish_date = wwid.chronify(options[:at], guess: :begin)
|
885
|
-
raise
|
997
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
|
886
998
|
|
887
999
|
date = options[:took] ? finish_date - took : finish_date
|
888
1000
|
elsif options[:back]
|
889
1001
|
date = wwid.chronify(options[:back])
|
890
1002
|
|
891
|
-
raise
|
1003
|
+
raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
892
1004
|
elsif options[:took]
|
893
1005
|
date = wwid.chronify_qty(options[:took])
|
894
1006
|
else
|
@@ -902,9 +1014,9 @@ command :finish do |c|
|
|
902
1014
|
tags = options[:tag].to_tags
|
903
1015
|
end
|
904
1016
|
|
905
|
-
raise
|
1017
|
+
raise InvalidArgument, 'Only one argument allowed' if args.length > 1
|
906
1018
|
|
907
|
-
raise
|
1019
|
+
raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
|
908
1020
|
|
909
1021
|
if options[:interactive]
|
910
1022
|
count = 0
|
@@ -912,12 +1024,21 @@ command :finish do |c|
|
|
912
1024
|
count = args[0] ? args[0].to_i : 1
|
913
1025
|
end
|
914
1026
|
|
1027
|
+
search = nil
|
1028
|
+
|
1029
|
+
if options[:search]
|
1030
|
+
search = options[:search]
|
1031
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
1032
|
+
end
|
1033
|
+
|
915
1034
|
opts = {
|
916
1035
|
archive: options[:archive],
|
917
1036
|
back: date,
|
918
1037
|
count: count,
|
919
1038
|
date: options[:date],
|
920
|
-
search:
|
1039
|
+
search: search,
|
1040
|
+
case: options[:case].normalize_case,
|
1041
|
+
not: options[:not],
|
921
1042
|
section: options[:section],
|
922
1043
|
sequential: options[:auto],
|
923
1044
|
tag: tags,
|
@@ -951,6 +1072,16 @@ command %i[again resume] do |c|
|
|
951
1072
|
c.arg_name 'QUERY'
|
952
1073
|
c.flag [:search]
|
953
1074
|
|
1075
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
1076
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
1077
|
+
|
1078
|
+
c.desc 'Resume items that *don\'t* match search/tag filters'
|
1079
|
+
c.switch [:not], default_value: false, negatable: false
|
1080
|
+
|
1081
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
1082
|
+
c.arg_name 'TYPE'
|
1083
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
1084
|
+
|
954
1085
|
c.desc 'Boolean used to combine multiple tags'
|
955
1086
|
c.arg_name 'BOOLEAN'
|
956
1087
|
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
@@ -967,7 +1098,17 @@ command %i[again resume] do |c|
|
|
967
1098
|
|
968
1099
|
c.action do |_global_options, options, _args|
|
969
1100
|
tags = options[:tag].nil? ? [] : options[:tag].to_tags
|
1101
|
+
|
1102
|
+
options[:case] = options[:case].normalize_case
|
1103
|
+
|
1104
|
+
if options[:search]
|
1105
|
+
search = options[:search]
|
1106
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
1107
|
+
options[:search] = search
|
1108
|
+
end
|
1109
|
+
|
970
1110
|
opts = options
|
1111
|
+
|
971
1112
|
opts[:tag] = tags
|
972
1113
|
opts[:tag_bool] = options[:bool].normalize_bool
|
973
1114
|
opts[:interactive] = options[:interactive]
|
@@ -1037,6 +1178,16 @@ command :tag do |c|
|
|
1037
1178
|
c.arg_name 'QUERY'
|
1038
1179
|
c.flag [:search]
|
1039
1180
|
|
1181
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
1182
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
1183
|
+
|
1184
|
+
c.desc 'Tag items that *don\'t* match search/tag filters'
|
1185
|
+
c.switch [:not], default_value: false, negatable: false
|
1186
|
+
|
1187
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
1188
|
+
c.arg_name 'TYPE'
|
1189
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
1190
|
+
|
1040
1191
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
1041
1192
|
c.arg_name 'BOOLEAN'
|
1042
1193
|
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
@@ -1045,9 +1196,9 @@ command :tag do |c|
|
|
1045
1196
|
c.switch %i[i interactive], negatable: false, default_value: false
|
1046
1197
|
|
1047
1198
|
c.action do |_global_options, options, args|
|
1048
|
-
raise
|
1199
|
+
raise MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:autotag]
|
1049
1200
|
|
1050
|
-
raise
|
1201
|
+
raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
1051
1202
|
|
1052
1203
|
section = 'All'
|
1053
1204
|
|
@@ -1081,6 +1232,13 @@ command :tag do |c|
|
|
1081
1232
|
count = options[:count].to_i
|
1082
1233
|
end
|
1083
1234
|
|
1235
|
+
options[:case] = options[:case].normalize_case
|
1236
|
+
|
1237
|
+
if options[:search]
|
1238
|
+
search = options[:search]
|
1239
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
1240
|
+
options[:search] = search
|
1241
|
+
end
|
1084
1242
|
|
1085
1243
|
if count.zero? && !options[:force]
|
1086
1244
|
if options[:search]
|
@@ -1104,7 +1262,7 @@ command :tag do |c|
|
|
1104
1262
|
|
1105
1263
|
res = wwid.yn(question, default_response: false)
|
1106
1264
|
|
1107
|
-
|
1265
|
+
raise UserCancelled unless res
|
1108
1266
|
end
|
1109
1267
|
|
1110
1268
|
options[:count] = count
|
@@ -1117,19 +1275,6 @@ command :tag do |c|
|
|
1117
1275
|
end
|
1118
1276
|
end
|
1119
1277
|
|
1120
|
-
# desc 'Autotag last X entries'
|
1121
|
-
# arg_name 'COUNT'
|
1122
|
-
# command :autotag do |c|
|
1123
|
-
# c.action do |global_options, options, args|
|
1124
|
-
# options = {
|
1125
|
-
# autotag: true,
|
1126
|
-
# count: args[0].to_i
|
1127
|
-
# }
|
1128
|
-
# cmd = commands[:tag]
|
1129
|
-
# cmd.action.(global_options, options, [])
|
1130
|
-
# end
|
1131
|
-
# end
|
1132
|
-
|
1133
1278
|
desc 'Mark last entry as flagged'
|
1134
1279
|
command [:mark, :flag] do |c|
|
1135
1280
|
c.example 'doing flag', desc: 'Add @flagged to the last entry created'
|
@@ -1165,6 +1310,16 @@ command [:mark, :flag] do |c|
|
|
1165
1310
|
c.arg_name 'QUERY'
|
1166
1311
|
c.flag [:search]
|
1167
1312
|
|
1313
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
1314
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
1315
|
+
|
1316
|
+
c.desc 'Flag items that *don\'t* match search/tag/date filters'
|
1317
|
+
c.switch [:not], default_value: false, negatable: false
|
1318
|
+
|
1319
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
1320
|
+
c.arg_name 'TYPE'
|
1321
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
1322
|
+
|
1168
1323
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
1169
1324
|
c.arg_name 'BOOLEAN'
|
1170
1325
|
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
@@ -1175,7 +1330,7 @@ command [:mark, :flag] do |c|
|
|
1175
1330
|
c.action do |_global_options, options, _args|
|
1176
1331
|
mark = settings['marker_tag'] || 'flagged'
|
1177
1332
|
|
1178
|
-
raise
|
1333
|
+
raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
1179
1334
|
|
1180
1335
|
section = 'All'
|
1181
1336
|
|
@@ -1196,6 +1351,14 @@ command [:mark, :flag] do |c|
|
|
1196
1351
|
count = options[:count].to_i
|
1197
1352
|
end
|
1198
1353
|
|
1354
|
+
options[:case] = options[:case].normalize_case
|
1355
|
+
|
1356
|
+
if options[:search]
|
1357
|
+
search = options[:search]
|
1358
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
1359
|
+
options[:search] = search
|
1360
|
+
end
|
1361
|
+
|
1199
1362
|
if count.zero? && !options[:force]
|
1200
1363
|
if options[:search]
|
1201
1364
|
section_q = ' matching your search terms'
|
@@ -1270,6 +1433,16 @@ command :show do |c|
|
|
1270
1433
|
c.arg_name 'QUERY'
|
1271
1434
|
c.flag [:search]
|
1272
1435
|
|
1436
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
1437
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
1438
|
+
|
1439
|
+
c.desc 'Show items that *don\'t* match search/tag/date filters'
|
1440
|
+
c.switch [:not], default_value: false, negatable: false
|
1441
|
+
|
1442
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
1443
|
+
c.arg_name 'TYPE'
|
1444
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
1445
|
+
|
1273
1446
|
c.desc 'Sort order (asc/desc)'
|
1274
1447
|
c.arg_name 'ORDER'
|
1275
1448
|
c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
@@ -1307,8 +1480,8 @@ command :show do |c|
|
|
1307
1480
|
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1308
1481
|
c.arg_name 'FORMAT'
|
1309
1482
|
c.flag %i[o output]
|
1310
|
-
c.action do |
|
1311
|
-
raise
|
1483
|
+
c.action do |global_options, options, args|
|
1484
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1312
1485
|
|
1313
1486
|
tag_filter = false
|
1314
1487
|
tags = []
|
@@ -1323,8 +1496,15 @@ command :show do |c|
|
|
1323
1496
|
when /^@/
|
1324
1497
|
section = 'All'
|
1325
1498
|
else
|
1326
|
-
|
1327
|
-
|
1499
|
+
begin
|
1500
|
+
section = wwid.guess_section(args[0])
|
1501
|
+
rescue WrongCommand => exception
|
1502
|
+
cmd = commands[:view]
|
1503
|
+
action = cmd.send(:get_action, nil)
|
1504
|
+
return action.call(global_options, options, args)
|
1505
|
+
end
|
1506
|
+
|
1507
|
+
raise InvalidSection, "No such section: #{args[0]}" unless section
|
1328
1508
|
|
1329
1509
|
args.shift
|
1330
1510
|
end
|
@@ -1359,7 +1539,7 @@ command :show do |c|
|
|
1359
1539
|
start = wwid.chronify(date_string, guess: :begin)
|
1360
1540
|
finish = false
|
1361
1541
|
end
|
1362
|
-
raise
|
1542
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
1363
1543
|
dates = [start, finish]
|
1364
1544
|
end
|
1365
1545
|
|
@@ -1367,6 +1547,14 @@ command :show do |c|
|
|
1367
1547
|
|
1368
1548
|
tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
|
1369
1549
|
|
1550
|
+
options[:case] = options[:case].normalize_case
|
1551
|
+
|
1552
|
+
if options[:search]
|
1553
|
+
search = options[:search]
|
1554
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
1555
|
+
options[:search] = search
|
1556
|
+
end
|
1557
|
+
|
1370
1558
|
opt = options.dup
|
1371
1559
|
|
1372
1560
|
opt[:sort_tags] = options[:tag_sort] =~ /^n/i
|
@@ -1429,20 +1617,35 @@ command %i[grep search] do |c|
|
|
1429
1617
|
c.desc 'Only show items with recorded time intervals'
|
1430
1618
|
c.switch [:only_timed], default_value: false, negatable: false
|
1431
1619
|
|
1620
|
+
c.desc 'Force exact string matching (case sensitive)'
|
1621
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
1622
|
+
|
1623
|
+
c.desc 'Show items that *don\'t* match search string'
|
1624
|
+
c.switch [:not], default_value: false, negatable: false
|
1625
|
+
|
1626
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
1627
|
+
c.arg_name 'TYPE'
|
1628
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
1629
|
+
|
1432
1630
|
c.desc 'Display an interactive menu of results to perform further operations'
|
1433
1631
|
c.switch %i[i interactive], default_value: false, negatable: false
|
1434
1632
|
|
1435
1633
|
c.action do |_global_options, options, args|
|
1436
|
-
raise
|
1634
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1437
1635
|
|
1438
1636
|
tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
|
1439
1637
|
|
1440
1638
|
section = wwid.guess_section(options[:section]) if options[:section]
|
1441
1639
|
|
1640
|
+
options[:case] = options[:case].normalize_case
|
1641
|
+
|
1642
|
+
search = args.join(' ')
|
1643
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
1644
|
+
|
1442
1645
|
options[:times] = true if options[:totals]
|
1443
1646
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1444
1647
|
options[:highlight] = true
|
1445
|
-
options[:search] =
|
1648
|
+
options[:search] = search
|
1446
1649
|
options[:section] = section
|
1447
1650
|
options[:tags_color] = tags_color
|
1448
1651
|
|
@@ -1548,7 +1751,7 @@ command :today do |c|
|
|
1548
1751
|
c.flag [:after]
|
1549
1752
|
|
1550
1753
|
c.action do |_global_options, options, _args|
|
1551
|
-
raise
|
1754
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1552
1755
|
|
1553
1756
|
options[:t] = true if options[:totals]
|
1554
1757
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
@@ -1595,9 +1798,9 @@ command :on do |c|
|
|
1595
1798
|
c.flag %i[o output]
|
1596
1799
|
|
1597
1800
|
c.action do |_global_options, options, args|
|
1598
|
-
raise
|
1801
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1599
1802
|
|
1600
|
-
raise
|
1803
|
+
raise MissingArgument, 'Missing date argument' if args.empty?
|
1601
1804
|
|
1602
1805
|
date_string = args.join(' ')
|
1603
1806
|
|
@@ -1610,7 +1813,7 @@ command :on do |c|
|
|
1610
1813
|
finish = false
|
1611
1814
|
end
|
1612
1815
|
|
1613
|
-
raise
|
1816
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
1614
1817
|
|
1615
1818
|
message = "Date interpreted as #{start}"
|
1616
1819
|
message += " to #{finish}" if finish
|
@@ -1653,9 +1856,9 @@ command :since do |c|
|
|
1653
1856
|
c.flag %i[o output]
|
1654
1857
|
|
1655
1858
|
c.action do |_global_options, options, args|
|
1656
|
-
raise
|
1859
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1657
1860
|
|
1658
|
-
raise
|
1861
|
+
raise MissingArgument, 'Missing date argument' if args.empty?
|
1659
1862
|
|
1660
1863
|
date_string = args.join(' ')
|
1661
1864
|
|
@@ -1665,7 +1868,7 @@ command :since do |c|
|
|
1665
1868
|
start = wwid.chronify(date_string, guess: :begin)
|
1666
1869
|
finish = Time.now
|
1667
1870
|
|
1668
|
-
raise
|
1871
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
1669
1872
|
|
1670
1873
|
Doing.logger.debug("Date interpreted as #{start} through the current time")
|
1671
1874
|
|
@@ -1698,7 +1901,6 @@ command :yesterday do |c|
|
|
1698
1901
|
c.switch [:totals], default_value: false, negatable: false
|
1699
1902
|
|
1700
1903
|
c.desc 'Sort tags by (name|time)'
|
1701
|
-
default = 'time'
|
1702
1904
|
default = settings['tag_sort'] || 'name'
|
1703
1905
|
c.arg_name 'KEY'
|
1704
1906
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
@@ -1716,7 +1918,7 @@ command :yesterday do |c|
|
|
1716
1918
|
c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
1717
1919
|
|
1718
1920
|
c.action do |_global_options, options, _args|
|
1719
|
-
raise
|
1921
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1720
1922
|
|
1721
1923
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1722
1924
|
|
@@ -1761,8 +1963,18 @@ command :last do |c|
|
|
1761
1963
|
c.arg_name 'QUERY'
|
1762
1964
|
c.flag [:search]
|
1763
1965
|
|
1966
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
1967
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
1968
|
+
|
1969
|
+
c.desc 'Show items that *don\'t* match search string or tag filter'
|
1970
|
+
c.switch [:not], default_value: false, negatable: false
|
1971
|
+
|
1972
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
1973
|
+
c.arg_name 'TYPE'
|
1974
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
1975
|
+
|
1764
1976
|
c.action do |global_options, options, _args|
|
1765
|
-
raise
|
1977
|
+
raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
|
1766
1978
|
|
1767
1979
|
if options[:tag].nil?
|
1768
1980
|
tags = []
|
@@ -1779,11 +1991,20 @@ command :last do |c|
|
|
1779
1991
|
|
1780
1992
|
end
|
1781
1993
|
|
1994
|
+
options[:case] = options[:case].normalize_case
|
1995
|
+
|
1996
|
+
search = nil
|
1997
|
+
|
1998
|
+
if options[:search]
|
1999
|
+
search = options[:search]
|
2000
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
2001
|
+
end
|
2002
|
+
|
1782
2003
|
if options[:editor]
|
1783
|
-
wwid.edit_last(section: options[:s], options: { search: options[:
|
2004
|
+
wwid.edit_last(section: options[:s], options: { search: search, case: options[:case], tag: tags, tag_bool: options[:bool], not: options[:not] })
|
1784
2005
|
else
|
1785
2006
|
Doing::Pager::page wwid.last(times: true, section: options[:s],
|
1786
|
-
options: { search: options[:
|
2007
|
+
options: { search: search, case: options[:case], negate: options[:not], tag: tags, tag_bool: options[:bool] }).strip
|
1787
2008
|
end
|
1788
2009
|
end
|
1789
2010
|
end
|
@@ -1791,7 +2012,7 @@ end
|
|
1791
2012
|
desc 'List sections'
|
1792
2013
|
command :sections do |c|
|
1793
2014
|
c.desc 'List in single column'
|
1794
|
-
c.switch %i[c column], default_value: false
|
2015
|
+
c.switch %i[c column], negatable: false, default_value: false
|
1795
2016
|
|
1796
2017
|
c.action do |_global_options, options, _args|
|
1797
2018
|
joiner = options[:c] ? "\n" : "\t"
|
@@ -1814,7 +2035,7 @@ command :add_section do |c|
|
|
1814
2035
|
c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file'
|
1815
2036
|
|
1816
2037
|
c.action do |_global_options, _options, args|
|
1817
|
-
raise
|
2038
|
+
raise InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
|
1818
2039
|
|
1819
2040
|
wwid.add_section(args.join(' ').cap_first)
|
1820
2041
|
wwid.write(wwid.doing_file)
|
@@ -1853,16 +2074,54 @@ command :plugins do |c|
|
|
1853
2074
|
|
1854
2075
|
c.desc 'List plugins of type (import, export)'
|
1855
2076
|
c.arg_name 'TYPE'
|
1856
|
-
c.flag %i[t type], must_match: /^[iea]
|
2077
|
+
c.flag %i[t type], must_match: /^(?:[iea].*)$/i, default_value: 'all'
|
1857
2078
|
|
1858
2079
|
c.desc 'List in single column for completion'
|
1859
|
-
c.switch %i[c column], default_value: false
|
2080
|
+
c.switch %i[c column], negatable: false, default_value: false
|
1860
2081
|
|
1861
2082
|
c.action do |_global_options, options, _args|
|
1862
2083
|
Doing::Plugins.list_plugins(options)
|
1863
2084
|
end
|
1864
2085
|
end
|
1865
2086
|
|
2087
|
+
desc 'Generate shell completion scripts'
|
2088
|
+
command :completion do |c|
|
2089
|
+
c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
|
2090
|
+
c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
|
2091
|
+
c.example 'doing completion --type fish --file ~/.config/fish/completions/doing.fish', desc: 'Output fish completions to file'
|
2092
|
+
c.example 'doing completion --type bash --file ~/.bash_it/completion/enabled/doing.bash', desc: 'Output bash completions to file'
|
2093
|
+
|
2094
|
+
c.desc 'Shell to generate for (bash, zsh, fish)'
|
2095
|
+
c.arg_name 'SHELL'
|
2096
|
+
c.flag %i[t type], must_match: /^[bzf](?:[ai]?sh)?$/i, default_value: 'zsh'
|
2097
|
+
|
2098
|
+
c.desc 'File to write output to'
|
2099
|
+
c.arg_name 'PATH'
|
2100
|
+
c.flag %i[f file], default_value: 'stdout'
|
2101
|
+
|
2102
|
+
c.action do |_global_options, options, _args|
|
2103
|
+
script_dir = File.join(File.dirname(__FILE__), '..', 'scripts')
|
2104
|
+
|
2105
|
+
case options[:type]
|
2106
|
+
when /^b/
|
2107
|
+
result = `ruby #{File.join(script_dir, 'generate_bash_completions.rb')}`
|
2108
|
+
when /^z/
|
2109
|
+
result = `ruby #{File.join(script_dir, 'generate_zsh_completions.rb')}`
|
2110
|
+
when /^f/
|
2111
|
+
result = `ruby #{File.join(script_dir, 'generate_fish_completions.rb')}`
|
2112
|
+
end
|
2113
|
+
|
2114
|
+
if options[:file] =~ /^stdout$/i
|
2115
|
+
$stdout.puts result
|
2116
|
+
else
|
2117
|
+
File.open(File.expand_path(options[:file]), 'w') do |f|
|
2118
|
+
f.puts result
|
2119
|
+
end
|
2120
|
+
Doing.logger.warn('File written:', "#{options[:type]} completions written to #{options[:file]}")
|
2121
|
+
end
|
2122
|
+
end
|
2123
|
+
end
|
2124
|
+
|
1866
2125
|
desc 'Display a user-created view'
|
1867
2126
|
long_desc 'Command line options override view configuration'
|
1868
2127
|
arg_name 'VIEW_NAME'
|
@@ -1903,6 +2162,16 @@ command :view do |c|
|
|
1903
2162
|
c.arg_name 'QUERY'
|
1904
2163
|
c.flag [:search]
|
1905
2164
|
|
2165
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
2166
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
2167
|
+
|
2168
|
+
c.desc 'Show items that *don\'t* match search string'
|
2169
|
+
c.switch [:not], default_value: false, negatable: false
|
2170
|
+
|
2171
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
2172
|
+
c.arg_name 'TYPE'
|
2173
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
2174
|
+
|
1906
2175
|
c.desc 'Sort tags by (name|time)'
|
1907
2176
|
c.arg_name 'KEY'
|
1908
2177
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i
|
@@ -1925,15 +2194,22 @@ command :view do |c|
|
|
1925
2194
|
c.desc 'Select from a menu of matching entries to perform additional operations'
|
1926
2195
|
c.switch %i[i interactive], negatable: false, default_value: false
|
1927
2196
|
|
1928
|
-
c.action do |
|
1929
|
-
raise
|
2197
|
+
c.action do |global_options, options, args|
|
2198
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1930
2199
|
|
1931
|
-
raise
|
2200
|
+
raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
|
1932
2201
|
|
1933
2202
|
title = if args.empty?
|
1934
2203
|
wwid.choose_view
|
1935
2204
|
else
|
1936
|
-
|
2205
|
+
begin
|
2206
|
+
wwid.guess_view(args[0])
|
2207
|
+
rescue WrongCommand => exception
|
2208
|
+
cmd = commands[:show]
|
2209
|
+
options[:sort] = 'asc'
|
2210
|
+
action = cmd.send(:get_action, nil)
|
2211
|
+
return action.call(global_options, options, args)
|
2212
|
+
end
|
1937
2213
|
end
|
1938
2214
|
|
1939
2215
|
if options[:section]
|
@@ -2023,11 +2299,21 @@ command :view do |c|
|
|
2023
2299
|
start = wwid.chronify(date_string, guess: :begin)
|
2024
2300
|
finish = false
|
2025
2301
|
end
|
2026
|
-
raise
|
2302
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
2027
2303
|
dates = [start, finish]
|
2028
2304
|
end
|
2029
2305
|
|
2030
|
-
|
2306
|
+
options[:case] = options[:case].normalize_case
|
2307
|
+
|
2308
|
+
search = nil
|
2309
|
+
|
2310
|
+
if options[:search]
|
2311
|
+
search = options[:search]
|
2312
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
2313
|
+
end
|
2314
|
+
|
2315
|
+
opts = options.dup
|
2316
|
+
opts[:search] = search
|
2031
2317
|
opts[:output] = output_format
|
2032
2318
|
opts[:count] = count
|
2033
2319
|
opts[:format] = date_format
|
@@ -2046,9 +2332,9 @@ command :view do |c|
|
|
2046
2332
|
|
2047
2333
|
Doing::Pager.page wwid.list_section(opts)
|
2048
2334
|
elsif title.instance_of?(FalseClass)
|
2049
|
-
|
2335
|
+
raise UserCancelled, 'Cancelled' unless res
|
2050
2336
|
else
|
2051
|
-
raise
|
2337
|
+
raise InvalidView, "View #{title} not found in config"
|
2052
2338
|
end
|
2053
2339
|
end
|
2054
2340
|
end
|
@@ -2100,6 +2386,16 @@ command %i[archive move] do |c|
|
|
2100
2386
|
c.arg_name 'QUERY'
|
2101
2387
|
c.flag [:search]
|
2102
2388
|
|
2389
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
2390
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
2391
|
+
|
2392
|
+
c.desc 'Show items that *don\'t* match search string'
|
2393
|
+
c.switch [:not], default_value: false, negatable: false
|
2394
|
+
|
2395
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
2396
|
+
c.arg_name 'TYPE'
|
2397
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
2398
|
+
|
2103
2399
|
c.desc 'Archive entries older than date
|
2104
2400
|
(Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
|
2105
2401
|
c.arg_name 'DATE_STRING'
|
@@ -2119,11 +2415,21 @@ command %i[archive move] do |c|
|
|
2119
2415
|
tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
|
2120
2416
|
end
|
2121
2417
|
|
2122
|
-
raise
|
2418
|
+
raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
|
2123
2419
|
|
2124
2420
|
tags.concat(options[:tag].to_tags) if options[:tag]
|
2125
2421
|
|
2126
|
-
|
2422
|
+
search = nil
|
2423
|
+
|
2424
|
+
options[:case] = options[:case].normalize_case
|
2425
|
+
|
2426
|
+
if options[:search]
|
2427
|
+
search = options[:search]
|
2428
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
2429
|
+
end
|
2430
|
+
|
2431
|
+
opts = options.dup
|
2432
|
+
opts[:search] = search
|
2127
2433
|
opts[:bool] = options[:bool].normalize_bool
|
2128
2434
|
opts[:destination] = options[:to]
|
2129
2435
|
opts[:tags] = tags
|
@@ -2158,6 +2464,16 @@ command :rotate do |c|
|
|
2158
2464
|
c.arg_name 'QUERY'
|
2159
2465
|
c.flag [:search]
|
2160
2466
|
|
2467
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
2468
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
2469
|
+
|
2470
|
+
c.desc 'Rotate items that *don\'t* match search string or tag filter'
|
2471
|
+
c.switch [:not], default_value: false, negatable: false
|
2472
|
+
|
2473
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
2474
|
+
c.arg_name 'TYPE'
|
2475
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
2476
|
+
|
2161
2477
|
c.desc 'Rotate entries older than date
|
2162
2478
|
(Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
|
2163
2479
|
c.arg_name 'DATE_STRING'
|
@@ -2170,6 +2486,16 @@ command :rotate do |c|
|
|
2170
2486
|
|
2171
2487
|
options[:bool] = options[:bool].normalize_bool
|
2172
2488
|
|
2489
|
+
options[:case] = options[:case].normalize_case
|
2490
|
+
|
2491
|
+
search = nil
|
2492
|
+
|
2493
|
+
if options[:search]
|
2494
|
+
search = options[:search]
|
2495
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
2496
|
+
options[:search] = search
|
2497
|
+
end
|
2498
|
+
|
2173
2499
|
wwid.rotate(options)
|
2174
2500
|
end
|
2175
2501
|
end
|
@@ -2208,7 +2534,7 @@ command :open do |c|
|
|
2208
2534
|
system %(open "#{File.expand_path(wwid.doing_file)}")
|
2209
2535
|
end
|
2210
2536
|
else
|
2211
|
-
raise
|
2537
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
2212
2538
|
|
2213
2539
|
system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
|
2214
2540
|
end
|
@@ -2236,7 +2562,7 @@ command :config do |c|
|
|
2236
2562
|
c.flag %i[e editor], default_value: nil
|
2237
2563
|
|
2238
2564
|
c.desc 'Show a config key value based on arguments. Separate key paths with colons or dots, e.g. "export_templates.html". Empty arguments outputs the entire config.'
|
2239
|
-
c.switch %i[d dump]
|
2565
|
+
c.switch %i[d dump], negatable: false
|
2240
2566
|
|
2241
2567
|
c.desc 'Format for --dump (json|yaml|raw)'
|
2242
2568
|
c.arg_name 'FORMAT'
|
@@ -2261,7 +2587,6 @@ command :config do |c|
|
|
2261
2587
|
c.action do |_global_options, options, args|
|
2262
2588
|
if options[:update]
|
2263
2589
|
config.configure({rewrite: true, ignore_local: true})
|
2264
|
-
# Doing.logger.warn("Config file rewritten: #{config.config_file}")
|
2265
2590
|
return
|
2266
2591
|
end
|
2267
2592
|
|
@@ -2276,7 +2601,6 @@ command :config do |c|
|
|
2276
2601
|
when /^r/
|
2277
2602
|
cfg
|
2278
2603
|
else
|
2279
|
-
# cfg = { last_key => cfg } unless last_key.nil?
|
2280
2604
|
YAML.dump(cfg)
|
2281
2605
|
end
|
2282
2606
|
else
|
@@ -2291,7 +2615,7 @@ command :config do |c|
|
|
2291
2615
|
choices.concat(config.additional_configs)
|
2292
2616
|
res = wwid.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to edit > ')
|
2293
2617
|
|
2294
|
-
raise
|
2618
|
+
raise UserCancelled, 'Cancelled' unless res
|
2295
2619
|
|
2296
2620
|
config_file = res.strip || config.config_file
|
2297
2621
|
else
|
@@ -2308,7 +2632,7 @@ command :config do |c|
|
|
2308
2632
|
`open -a "#{editor}" "#{config_file}"`
|
2309
2633
|
end
|
2310
2634
|
else
|
2311
|
-
raise
|
2635
|
+
raise InvalidArgument, 'No viable editor found in config or environment.'
|
2312
2636
|
end
|
2313
2637
|
elsif options[:a] || options[:b]
|
2314
2638
|
if options[:a]
|
@@ -2319,7 +2643,7 @@ command :config do |c|
|
|
2319
2643
|
else
|
2320
2644
|
editor = options[:e] || Doing::Util.find_default_editor('config')
|
2321
2645
|
|
2322
|
-
raise
|
2646
|
+
raise MissingEditor, 'No viable editor defined in config or environment' unless editor
|
2323
2647
|
|
2324
2648
|
if Doing::Util.exec_available(editor)
|
2325
2649
|
system %(#{editor} "#{config_file}")
|
@@ -2329,7 +2653,7 @@ command :config do |c|
|
|
2329
2653
|
end
|
2330
2654
|
else
|
2331
2655
|
editor = options[:e] || Doing::Util.default_editor
|
2332
|
-
raise
|
2656
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
|
2333
2657
|
|
2334
2658
|
system %(#{editor} "#{config_file}")
|
2335
2659
|
end
|
@@ -2360,6 +2684,16 @@ command :import do |c|
|
|
2360
2684
|
c.arg_name 'QUERY'
|
2361
2685
|
c.flag [:search]
|
2362
2686
|
|
2687
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
2688
|
+
c.switch %i[x exact], default_value: false, negatable: false
|
2689
|
+
|
2690
|
+
c.desc 'Import items that *don\'t* match search/tag/date filters'
|
2691
|
+
c.switch [:not], default_value: false, negatable: false
|
2692
|
+
|
2693
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
|
2694
|
+
c.arg_name 'TYPE'
|
2695
|
+
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
2696
|
+
|
2363
2697
|
c.desc 'Only import items with recorded time intervals'
|
2364
2698
|
c.switch [:only_timed], default_value: false, negatable: false
|
2365
2699
|
|
@@ -2413,17 +2747,19 @@ command :import do |c|
|
|
2413
2747
|
start = wwid.chronify(date_string, guess: :begin)
|
2414
2748
|
finish = false
|
2415
2749
|
end
|
2416
|
-
raise
|
2750
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
2417
2751
|
dates = [start, finish]
|
2418
2752
|
end
|
2419
2753
|
|
2754
|
+
options[:case] = options[:case].normalize_case
|
2755
|
+
|
2420
2756
|
if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
|
2421
2757
|
options[:no_overlap] = !options[:overlap]
|
2422
2758
|
options[:date_filter] = dates
|
2423
2759
|
wwid.import(args, options)
|
2424
2760
|
wwid.write(wwid.doing_file)
|
2425
2761
|
else
|
2426
|
-
raise
|
2762
|
+
raise InvalidPluginType, "Invalid import type: #{options[:type]}"
|
2427
2763
|
end
|
2428
2764
|
end
|
2429
2765
|
end
|
@@ -2448,14 +2784,13 @@ pre do |global, _command, _options, _args|
|
|
2448
2784
|
end
|
2449
2785
|
|
2450
2786
|
on_error do |exception|
|
2451
|
-
|
2452
|
-
|
2453
|
-
|
2454
|
-
|
2455
|
-
|
2456
|
-
|
2457
|
-
|
2458
|
-
false
|
2787
|
+
if exception.kind_of?(SystemExit)
|
2788
|
+
false
|
2789
|
+
else
|
2790
|
+
# Doing.logger.error('Fatal:', exception)
|
2791
|
+
Doing.logger.output_results
|
2792
|
+
true
|
2793
|
+
end
|
2459
2794
|
end
|
2460
2795
|
|
2461
2796
|
post do |global, _command, _options, _args|
|