doing 2.0.5.pre → 2.0.9.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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|
|