doing 1.0.73 → 1.0.78
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +8 -6
- data/bin/doing +179 -65
- data/lib/doing/helpers.rb +1 -1
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +204 -41
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 64076f13651c82428d05f490870a3a8a2507dc783cfaeebe7658538523aaef25
|
4
|
+
data.tar.gz: 691df494543c3ff71911a4ca87135adba9504fb445d51180e9c5914372c5d35b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be005b1f54f3e170030f88a6df19567478985965b56bb50a842fe3d5c87d7afb750e165d4888da45466f3dba16205a94d1baf5cbfe723f0a18dffe960d944d00
|
7
|
+
data.tar.gz: 48e5cbec09157db3c974b5d0c242c394884a77f47ce5cf4e009b7dedf47daccba581709aefb534266415884ab71fad03c0a4d22385e3abc9d3e33576f3aaad17
|
data/README.md
CHANGED
@@ -27,7 +27,7 @@ If there's something I want to look at later but doesn't need to be added to a t
|
|
27
27
|
|
28
28
|
## Installation
|
29
29
|
|
30
|
-
The current version of `doing` is <!--VER-->1.0.
|
30
|
+
The current version of `doing` is <!--VER-->1.0.76<!--END VER-->.
|
31
31
|
|
32
32
|
$ [sudo] gem install doing
|
33
33
|
|
@@ -268,7 +268,7 @@ You can add additional custom views. Just nest them under the `views` key (inden
|
|
268
268
|
count: 5
|
269
269
|
wrap_width: 0
|
270
270
|
date_format: '%F %_I:%M%P'
|
271
|
-
template: '%date | %title%note'
|
271
|
+
template: '%date | %title%note'
|
272
272
|
|
273
273
|
The `section` key is the default section to pull entries from. Count and section can be overridden at runtime with the `-c` and `-s` flags. Setting `section` to `All` will combine all sections in the output.
|
274
274
|
|
@@ -505,7 +505,7 @@ You can include a `transform` section in the autotag config which contains pairs
|
|
505
505
|
transform:
|
506
506
|
- (\w+)-\d+:$1
|
507
507
|
|
508
|
-
This creates a search pattern looking for a string of word characters followed by a hyphen and one or more digits, e.g. `@projecttag-12`. Do not include the @ symbol in the pattern. The replacement (`$
|
508
|
+
This creates a search pattern looking for a string of word characters followed by a hyphen and one or more digits, e.g. `@projecttag-12`. Do not include the @ symbol in the pattern. The replacement (`$l`) indicates that the first matched group (in parenthesis) should be used to generate the new tag, resulting in `@projecttag` being added to the entry.
|
509
509
|
|
510
510
|
##### Annotating
|
511
511
|
|
@@ -536,7 +536,7 @@ If you have a use for it, you can use `-o csv` on the show or view commands to o
|
|
536
536
|
|
537
537
|
`doing yesterday` is great for stand-ups (thanks to [Sean Collins](https://github.com/sc68cal) for that!). Note that you can show yesterday's activity from an alternate section by using the section name as an argument (e.g. `doing yesterday archive`).
|
538
538
|
|
539
|
-
`doing on` allows for full date ranges and filtering. `doing on saturday`, or `doing on one month to today` will give you ranges. You can use the same terms with the `show` command by adding the `-f` or `--from` flag. `doing show @done --from "monday to friday"` will give you all of your completed items for the last week (assuming it's the weekend).
|
539
|
+
`doing on` allows for full date ranges and filtering. `doing on saturday`, or `doing on one month to today` will give you ranges. You can use the same terms with the `show` command by adding the `-f` or `--from` flag. `doing show @done --from "monday to friday"` will give you all of your completed items for the last week (assuming it's the weekend). There's also `doing since` a simple alias for `doing on PAST_DATE to now`, e.g. `doing since monday`.
|
540
540
|
|
541
541
|
You can also show entries matching a search string with `doing grep` (synonym `doing search`). If you want to search with regular expressions or for an exact match, surround your search query with forward slashes, e.g. `doing search /project name/`. If you pass a search string without slashes, it's treated as a fuzzy search string, meaning matches can be found as long as the characters in the search string are in order and with no more than three other characters between each. By default searches are across all sections, but you can limit it to one with the `-s SECTION_NAME` flag. Searches can be displayed with the default template, or output as HTML, CSV, or JSON.
|
542
542
|
|
@@ -621,9 +621,9 @@ Now you can run `doing import --type timing -s SECTION PATH`, where SECTION is t
|
|
621
621
|
|
622
622
|
If you have `fzf` installed (<https://github.com/junegunn/fzf>), you can use `doing select` to get a menu of all your items (or items in a given section) which can be searched with fuzzy matching. The menu allows multiple selections to be acted on directly.
|
623
623
|
|
624
|
-
To use the menu, type a search string or use the arrow keys to navigate up and down. Press tab on an entry you'd like to perform an action on. A marker will show up on the left indicating the entry is selected. Repeat the process and select as many entries as needed. When you hit Return, the selection will be passed back to doing.
|
624
|
+
To use the menu, type a search string or use the arrow keys to navigate up and down. Press tab on an entry you'd like to perform an action on. A marker will show up on the left indicating the entry is selected. Repeat the process and select as many entries as needed. When you hit Return, the selection will be passed back to doing. Use Control-A to select all visible entries.
|
625
625
|
|
626
|
-
Doing can perform several functions with this menu. Not all of doing's features are available, but the core functionality you'd need is there, plus you can open
|
626
|
+
Doing can perform several functions with this menu. Not all of doing's features are available, but the core functionality you'd need is there, plus you can open the selected entries on one page in your text editor, make changes to them, and when you save and close the entries are updated accordingly. This allows editing of everything from timestamps to tags to notes.
|
627
627
|
|
628
628
|
Run `doing help select` for a list of options:
|
629
629
|
|
@@ -641,6 +641,8 @@ For example, `doing select -d -a` would present the menu, and then mark selected
|
|
641
641
|
|
642
642
|
Multiple actions can be performed at once by combining options. You can also combine the `--editor` switch with any other options. Other actions will be performed first, then the entries --- with any modifications performed --- will be presented in the editor for tweaking.
|
643
643
|
|
644
|
+
**Note:** when using the `--editor` flag to open selections in your text editor, entries will be separated by `---` lines. These must remain in place for doing to track the changes. You can do anything you want to the entries, modify dates, change text, add notes, etc., as long as you leave the dividers in place. You can even delete an entry entirely, leaving the dividers around the missing line and the entry will be removed from your doing file when you save and exit the editor.
|
645
|
+
|
644
646
|
---
|
645
647
|
|
646
648
|
## Extras
|
data/bin/doing
CHANGED
@@ -79,7 +79,7 @@ command %i[now next] do |c|
|
|
79
79
|
if options[:back]
|
80
80
|
date = wwid.chronify(options[:back])
|
81
81
|
|
82
|
-
|
82
|
+
exit_now! 'Unable to parse date string' if date.nil?
|
83
83
|
else
|
84
84
|
date = Time.now
|
85
85
|
end
|
@@ -87,13 +87,13 @@ command %i[now next] do |c|
|
|
87
87
|
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
88
88
|
|
89
89
|
if options[:e] || (args.empty? && $stdin.stat.size.zero?)
|
90
|
-
|
90
|
+
exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
|
91
91
|
|
92
92
|
input = ''
|
93
93
|
input += args.join(' ') unless args.empty?
|
94
94
|
input = wwid.fork_editor(input).strip
|
95
95
|
|
96
|
-
|
96
|
+
exit_now! 'No content' if input.empty?
|
97
97
|
|
98
98
|
title, note = wwid.format_input(input)
|
99
99
|
note.push(options[:n]) if options[:n]
|
@@ -111,7 +111,7 @@ command %i[now next] do |c|
|
|
111
111
|
wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
|
112
112
|
wwid.write(wwid.doing_file)
|
113
113
|
else
|
114
|
-
|
114
|
+
exit_now! 'You must provide content when creating a new entry'
|
115
115
|
end
|
116
116
|
end
|
117
117
|
end
|
@@ -138,7 +138,7 @@ command :note do |c|
|
|
138
138
|
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
139
139
|
|
140
140
|
if options[:e] || (args.empty? && $stdin.stat.size.zero? && !options[:r])
|
141
|
-
|
141
|
+
exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
|
142
142
|
|
143
143
|
input = !args.empty? ? args.join(' ') : ''
|
144
144
|
|
@@ -147,11 +147,11 @@ command :note do |c|
|
|
147
147
|
input = prev_input + input
|
148
148
|
|
149
149
|
input = wwid.fork_editor(input).strip
|
150
|
-
|
150
|
+
exit_now! 'No content, cancelled' unless input
|
151
151
|
|
152
152
|
_title, note = wwid.format_input(input)
|
153
153
|
|
154
|
-
|
154
|
+
exit_now! 'No note content' unless note
|
155
155
|
|
156
156
|
wwid.note_last(section, note, replace: true)
|
157
157
|
elsif !args.empty?
|
@@ -165,7 +165,7 @@ command :note do |c|
|
|
165
165
|
elsif options[:r]
|
166
166
|
wwid.note_last(section, [], replace: true)
|
167
167
|
else
|
168
|
-
|
168
|
+
exit_now! 'You must provide content when adding a note'
|
169
169
|
end
|
170
170
|
wwid.write(wwid.doing_file)
|
171
171
|
end
|
@@ -196,7 +196,7 @@ command :meanwhile do |c|
|
|
196
196
|
if options[:back]
|
197
197
|
date = wwid.chronify(options[:back])
|
198
198
|
|
199
|
-
|
199
|
+
exit_now! 'Unable to parse date string' if date.nil?
|
200
200
|
else
|
201
201
|
date = Time.now
|
202
202
|
end
|
@@ -205,7 +205,7 @@ command :meanwhile do |c|
|
|
205
205
|
input = ''
|
206
206
|
|
207
207
|
if options[:e]
|
208
|
-
|
208
|
+
exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
|
209
209
|
|
210
210
|
input += args.join(' ') unless args.empty?
|
211
211
|
input = wwid.fork_editor(input).strip
|
@@ -243,7 +243,7 @@ long_desc %(
|
|
243
243
|
arg_name 'TYPE', must_match: /^(?:html|haml|css)/i
|
244
244
|
command :template do |c|
|
245
245
|
c.action do |_global_options, options, args|
|
246
|
-
|
246
|
+
exit_now! 'No type specified, use `doing template [HAML|CSS]`' if args.empty?
|
247
247
|
|
248
248
|
case args[0]
|
249
249
|
when /html|haml/i
|
@@ -251,15 +251,15 @@ command :template do |c|
|
|
251
251
|
when /css/i
|
252
252
|
$stdout.puts wwid.css_template
|
253
253
|
else
|
254
|
-
|
254
|
+
exit_now! 'Invalid type specified, must be HAML or CSS'
|
255
255
|
end
|
256
256
|
end
|
257
257
|
end
|
258
258
|
|
259
259
|
desc 'Display an interactive menu to perform operations (requires fzf)'
|
260
|
-
long_desc '
|
260
|
+
long_desc 'List all entries and select with typeahead fuzzy matching.
|
261
261
|
|
262
|
-
|
262
|
+
Multiple selections are allowed, hit tab to add the highlighted entry to the selection. Return processes the selected entries.'
|
263
263
|
command :select do |c|
|
264
264
|
c.desc 'Select from a specific section'
|
265
265
|
c.arg_name 'SECTION'
|
@@ -269,6 +269,9 @@ command :select do |c|
|
|
269
269
|
c.arg_name 'TAG'
|
270
270
|
c.flag %i[t tag]
|
271
271
|
|
272
|
+
c.desc 'Reverse -c, -f, --flag, and -t (remove instead of adding)'
|
273
|
+
c.switch %i[r remove], negatable: false
|
274
|
+
|
272
275
|
# c.desc 'Add @done to selected item(s), using start time of next item as the finish time'
|
273
276
|
# c.switch %i[a auto], negatable: false, default_value: false
|
274
277
|
|
@@ -279,6 +282,10 @@ command :select do |c|
|
|
279
282
|
c.arg_name 'SECTION'
|
280
283
|
c.flag %i[m move]
|
281
284
|
|
285
|
+
c.desc 'Initial search query for filtering'
|
286
|
+
c.arg_name 'QUERY'
|
287
|
+
c.flag %i[q query]
|
288
|
+
|
282
289
|
c.desc 'Cancel selected items (add @done without timestamp)'
|
283
290
|
c.switch %i[c cancel], negatable: false, default_value: false
|
284
291
|
|
@@ -294,9 +301,15 @@ command :select do |c|
|
|
294
301
|
c.desc 'Add flag to selected item(s)'
|
295
302
|
c.switch %i[flag], negatable: false, default_value: false
|
296
303
|
|
304
|
+
c.desc 'Save selected entries to file using --output format'
|
305
|
+
c.arg_name 'FILE'
|
306
|
+
c.flag %i[save_to]
|
307
|
+
|
308
|
+
c.desc 'Output format for export (doing|taskpaper|csv|html|json|template|timeline)'
|
309
|
+
c.arg_name 'FORMAT'
|
310
|
+
c.flag %i[o output], must_match: /^(?:doing|taskpaper|html|csv|json|template|timeline)$/i
|
311
|
+
|
297
312
|
c.action do |_global_options, options, args|
|
298
|
-
section = options[:section] || 'All'
|
299
|
-
edit = options[:editor]
|
300
313
|
wwid.interactive(options)
|
301
314
|
end
|
302
315
|
end
|
@@ -322,17 +335,17 @@ command :later do |c|
|
|
322
335
|
c.action do |_global_options, options, args|
|
323
336
|
if options[:back]
|
324
337
|
date = wwid.chronify(options[:back])
|
325
|
-
|
338
|
+
exit_now! 'Unable to parse date string' if date.nil?
|
326
339
|
else
|
327
340
|
date = Time.now
|
328
341
|
end
|
329
342
|
|
330
343
|
if options[:e] || (args.empty? && $stdin.stat.size.zero?)
|
331
|
-
|
344
|
+
exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
|
332
345
|
|
333
346
|
input = args.empty? ? '' : args.join(' ')
|
334
347
|
input = wwid.fork_editor(input).strip
|
335
|
-
|
348
|
+
exit_now! 'No content' unless input && !input.empty?
|
336
349
|
|
337
350
|
title, note = wwid.format_input(input)
|
338
351
|
note.push(options[:n]) if options[:n]
|
@@ -349,7 +362,7 @@ command :later do |c|
|
|
349
362
|
wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
|
350
363
|
wwid.write(wwid.doing_file)
|
351
364
|
else
|
352
|
-
|
365
|
+
exit_now! 'You must provide content when creating a new entry'
|
353
366
|
end
|
354
367
|
end
|
355
368
|
end
|
@@ -397,19 +410,19 @@ command %i[done did] do |c|
|
|
397
410
|
|
398
411
|
if options[:took]
|
399
412
|
took = wwid.chronify_qty(options[:took])
|
400
|
-
|
413
|
+
exit_now! 'Unable to parse date string for --took' if took.nil?
|
401
414
|
end
|
402
415
|
|
403
416
|
if options[:back]
|
404
417
|
date = wwid.chronify(options[:back])
|
405
|
-
|
418
|
+
exit_now! 'Unable to parse date string for --back' if date.nil?
|
406
419
|
else
|
407
420
|
date = options[:took] ? Time.now - took : Time.now
|
408
421
|
end
|
409
422
|
|
410
423
|
if options[:at]
|
411
424
|
finish_date = wwid.chronify(options[:at])
|
412
|
-
|
425
|
+
exit_now! 'Unable to parse date string for --at' if finish_date.nil?
|
413
426
|
|
414
427
|
date = options[:took] ? finish_date - took : finish_date
|
415
428
|
elsif options[:took]
|
@@ -427,12 +440,12 @@ command %i[done did] do |c|
|
|
427
440
|
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
428
441
|
|
429
442
|
if options[:e]
|
430
|
-
|
443
|
+
exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
|
431
444
|
|
432
445
|
input = ''
|
433
446
|
input += args.join(' ') unless args.empty?
|
434
447
|
input = wwid.fork_editor(input).strip
|
435
|
-
|
448
|
+
exit_now! 'No content' unless input && !input.empty?
|
436
449
|
|
437
450
|
title, note = wwid.format_input(input)
|
438
451
|
title += " @done#{donedate}"
|
@@ -467,7 +480,7 @@ command %i[done did] do |c|
|
|
467
480
|
wwid.add_item(title.cap_first, section.cap_first, { note: note, back: date })
|
468
481
|
wwid.write(wwid.doing_file)
|
469
482
|
else
|
470
|
-
|
483
|
+
exit_now! 'You must provide content when creating a new entry'
|
471
484
|
end
|
472
485
|
end
|
473
486
|
end
|
@@ -513,9 +526,9 @@ command :cancel do |c|
|
|
513
526
|
end
|
514
527
|
end
|
515
528
|
|
516
|
-
|
529
|
+
exit_now! 'Only one argument allowed' if args.length > 1
|
517
530
|
|
518
|
-
|
531
|
+
exit_now! 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
|
519
532
|
|
520
533
|
count = args[0] ? args[0].to_i : 1
|
521
534
|
opts = {
|
@@ -580,14 +593,14 @@ command :finish do |c|
|
|
580
593
|
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
581
594
|
|
582
595
|
unless options[:auto]
|
583
|
-
|
596
|
+
exit_now! '--back and --took cannot be used together' if options[:back] && options[:took]
|
584
597
|
|
585
|
-
|
598
|
+
exit_now! '--search and --tag cannot be used together' if options[:search] && options[:tag]
|
586
599
|
|
587
600
|
if options[:back]
|
588
601
|
date = wwid.chronify(options[:back])
|
589
602
|
|
590
|
-
|
603
|
+
exit_now! 'Unable to parse date string' if date.nil?
|
591
604
|
elsif options[:took]
|
592
605
|
date = wwid.chronify_qty(options[:took])
|
593
606
|
else
|
@@ -611,9 +624,9 @@ command :finish do |c|
|
|
611
624
|
end
|
612
625
|
end
|
613
626
|
|
614
|
-
|
627
|
+
exit_now! 'Only one argument allowed' if args.length > 1
|
615
628
|
|
616
|
-
|
629
|
+
exit_now! 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
|
617
630
|
|
618
631
|
count = args[0] ? args[0].to_i : 1
|
619
632
|
opts = {
|
@@ -695,6 +708,9 @@ command :tag do |c|
|
|
695
708
|
c.arg_name 'COUNT'
|
696
709
|
c.flag %i[c count], default_value: 1
|
697
710
|
|
711
|
+
c.desc 'Don\'t ask permission to tag all entries when count is 0'
|
712
|
+
c.switch %i[force], negatable: false, default_value: false
|
713
|
+
|
698
714
|
c.desc 'Include current date/time with tag'
|
699
715
|
c.switch %i[d date], negatable: false, default_value: false
|
700
716
|
|
@@ -707,12 +723,48 @@ command :tag do |c|
|
|
707
723
|
c.desc 'Autotag entries based on autotag configuration in ~/.doingrc'
|
708
724
|
c.switch %i[a autotag], negatable: false, default_value: false
|
709
725
|
|
726
|
+
c.desc 'Tag the last X entries containing TAG.
|
727
|
+
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
|
728
|
+
c.arg_name 'TAG'
|
729
|
+
c.flag [:tag]
|
730
|
+
|
731
|
+
c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/")'
|
732
|
+
c.arg_name 'QUERY'
|
733
|
+
c.flag [:search]
|
734
|
+
|
735
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
736
|
+
c.arg_name 'BOOLEAN'
|
737
|
+
c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
|
738
|
+
|
710
739
|
c.action do |_global_options, options, args|
|
711
|
-
|
740
|
+
exit_now! 'You must specify at least one tag' if args.empty? && !options[:a]
|
712
741
|
|
713
|
-
|
742
|
+
exit_now! '--search and --tag cannot be used together' if options[:search] && options[:tag]
|
714
743
|
|
715
|
-
|
744
|
+
section = 'All'
|
745
|
+
|
746
|
+
if options[:section]
|
747
|
+
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
748
|
+
end
|
749
|
+
|
750
|
+
|
751
|
+
if options[:tag].nil?
|
752
|
+
search_tags = []
|
753
|
+
else
|
754
|
+
search_tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
|
755
|
+
options[:bool] = case options[:bool]
|
756
|
+
when /(and|all)/i
|
757
|
+
'AND'
|
758
|
+
when /(any|or)/i
|
759
|
+
'OR'
|
760
|
+
when /(not|none)/i
|
761
|
+
'NOT'
|
762
|
+
else
|
763
|
+
'AND'
|
764
|
+
end
|
765
|
+
end
|
766
|
+
|
767
|
+
if options[:autotag]
|
716
768
|
tags = []
|
717
769
|
else
|
718
770
|
tags = if args.join('') =~ /,/
|
@@ -724,10 +776,19 @@ command :tag do |c|
|
|
724
776
|
tags.map! { |tag| tag.sub(/^@/, '').strip }
|
725
777
|
end
|
726
778
|
|
727
|
-
count = options[:
|
779
|
+
count = options[:count].to_i
|
780
|
+
|
781
|
+
if count.zero? && !options[:force]
|
782
|
+
if options[:search]
|
783
|
+
section_q = ' matching your search terms'
|
784
|
+
elsif options[:tag]
|
785
|
+
section_q = ' matching your tag search'
|
786
|
+
elsif section == 'All'
|
787
|
+
section_q = ''
|
788
|
+
else
|
789
|
+
section_q = " in section #{section}"
|
790
|
+
end
|
728
791
|
|
729
|
-
if count.zero?
|
730
|
-
section_q = section == 'All' ? '' : " in section #{section}"
|
731
792
|
|
732
793
|
question = if options[:a]
|
733
794
|
"Are you sure you want to autotag all records#{section_q}"
|
@@ -739,14 +800,18 @@ command :tag do |c|
|
|
739
800
|
|
740
801
|
res = wwid.yn(question, default_response: false)
|
741
802
|
|
742
|
-
|
803
|
+
exit_now! 'Cancelled' unless res
|
743
804
|
end
|
805
|
+
|
744
806
|
opts = {
|
745
807
|
autotag: options[:a],
|
746
808
|
count: count,
|
747
809
|
date: options[:date],
|
748
810
|
remove: options[:r],
|
811
|
+
search: options[:search],
|
749
812
|
section: section,
|
813
|
+
tag: search_tags,
|
814
|
+
tag_bool: options[:bool],
|
750
815
|
tags: tags,
|
751
816
|
unfinished: options[:unfinished]
|
752
817
|
}
|
@@ -813,10 +878,10 @@ command :show do |c|
|
|
813
878
|
c.flag %i[f from]
|
814
879
|
|
815
880
|
c.desc 'Show time intervals on @done tasks'
|
816
|
-
c.switch %i[t times], default_value: true
|
881
|
+
c.switch %i[t times], default_value: true, negatable: true
|
817
882
|
|
818
883
|
c.desc 'Show intervals with totals at the end of output'
|
819
|
-
c.switch [:totals], default_value: false, negatable:
|
884
|
+
c.switch [:totals], default_value: false, negatable: false
|
820
885
|
|
821
886
|
c.desc 'Sort tags by (name|time)'
|
822
887
|
default = 'time'
|
@@ -845,7 +910,7 @@ command :show do |c|
|
|
845
910
|
section = 'All'
|
846
911
|
else
|
847
912
|
section = wwid.guess_section(args[0])
|
848
|
-
|
913
|
+
exit_now! "No such section: #{args[0]}" unless section
|
849
914
|
|
850
915
|
args.shift
|
851
916
|
end
|
@@ -935,10 +1000,10 @@ command [:grep, :search] do |c|
|
|
935
1000
|
c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
|
936
1001
|
|
937
1002
|
c.desc 'Show time intervals on @done tasks'
|
938
|
-
c.switch %i[t times], default_value: true
|
1003
|
+
c.switch %i[t times], default_value: true, negatable: true
|
939
1004
|
|
940
1005
|
c.desc 'Show intervals with totals at the end of output'
|
941
|
-
c.switch [:totals], default_value: false, negatable:
|
1006
|
+
c.switch [:totals], default_value: false, negatable: false
|
942
1007
|
|
943
1008
|
c.desc 'Sort tags by (name|time)'
|
944
1009
|
default = 'time'
|
@@ -982,10 +1047,10 @@ command :recent do |c|
|
|
982
1047
|
c.flag %i[s section], default_value: 'All'
|
983
1048
|
|
984
1049
|
c.desc 'Show time intervals on @done tasks'
|
985
|
-
c.switch %i[t times], default_value: true
|
1050
|
+
c.switch %i[t times], default_value: true, negatable: true
|
986
1051
|
|
987
1052
|
c.desc 'Show intervals with totals at the end of output'
|
988
|
-
c.switch [:totals], default_value: false, negatable:
|
1053
|
+
c.switch [:totals], default_value: false, negatable: false
|
989
1054
|
|
990
1055
|
c.desc 'Sort tags by (name|time)'
|
991
1056
|
default = 'time'
|
@@ -1022,10 +1087,10 @@ command :today do |c|
|
|
1022
1087
|
c.flag %i[s section], default_value: 'All'
|
1023
1088
|
|
1024
1089
|
c.desc 'Show time intervals on @done tasks'
|
1025
|
-
c.switch %i[t times], default_value: true
|
1090
|
+
c.switch %i[t times], default_value: true, negatable: true
|
1026
1091
|
|
1027
1092
|
c.desc 'Show time totals at the end of output'
|
1028
|
-
c.switch [:totals], default_value: false, negatable:
|
1093
|
+
c.switch [:totals], default_value: false, negatable: false
|
1029
1094
|
|
1030
1095
|
c.desc 'Sort tags by (name|time)'
|
1031
1096
|
default = 'time'
|
@@ -1057,10 +1122,10 @@ command :on do |c|
|
|
1057
1122
|
c.flag %i[s section], default_value: 'All'
|
1058
1123
|
|
1059
1124
|
c.desc 'Show time intervals on @done tasks'
|
1060
|
-
c.switch %i[t times], default_value: true
|
1125
|
+
c.switch %i[t times], default_value: true, negatable: true
|
1061
1126
|
|
1062
1127
|
c.desc 'Show time totals at the end of output'
|
1063
|
-
c.switch [:totals], default_value: false, negatable:
|
1128
|
+
c.switch [:totals], default_value: false, negatable: false
|
1064
1129
|
|
1065
1130
|
c.desc 'Sort tags by (name|time)'
|
1066
1131
|
default = 'time'
|
@@ -1100,6 +1165,55 @@ command :on do |c|
|
|
1100
1165
|
end
|
1101
1166
|
end
|
1102
1167
|
|
1168
|
+
desc 'List entries since a date'
|
1169
|
+
long_desc %(Date argument can be natural language and are always interpreted as being in the past. "thursday" would be interpreted as "last thursday,"
|
1170
|
+
and "2d" would be interpreted as "two days ago.")
|
1171
|
+
arg_name 'DATE_STRING'
|
1172
|
+
command :since do |c|
|
1173
|
+
c.desc 'Section'
|
1174
|
+
c.arg_name 'NAME'
|
1175
|
+
c.flag %i[s section], default_value: 'All'
|
1176
|
+
|
1177
|
+
c.desc 'Show time intervals on @done tasks'
|
1178
|
+
c.switch %i[t times], default_value: true, negatable: true
|
1179
|
+
|
1180
|
+
c.desc 'Show time totals at the end of output'
|
1181
|
+
c.switch [:totals], default_value: false, negatable: false
|
1182
|
+
|
1183
|
+
c.desc 'Sort tags by (name|time)'
|
1184
|
+
default = 'time'
|
1185
|
+
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
1186
|
+
c.arg_name 'KEY'
|
1187
|
+
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1188
|
+
|
1189
|
+
c.desc 'Output to export format (csv|html|json|template|timeline)'
|
1190
|
+
c.arg_name 'FORMAT'
|
1191
|
+
c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
|
1192
|
+
|
1193
|
+
c.action do |_global_options, options, args|
|
1194
|
+
exit_now! 'Missing date argument' if args.empty?
|
1195
|
+
|
1196
|
+
date_string = args.join(' ')
|
1197
|
+
|
1198
|
+
date_string += ' at midnight' unless date_string =~ /(\d:|\d *[ap]m?|midnight|noon)/i
|
1199
|
+
date_string.sub!(/(day) (\d)/, '\1 at \2') if date_string =~ /day \d/
|
1200
|
+
|
1201
|
+
start = wwid.chronify(date_string)
|
1202
|
+
finish = Time.now
|
1203
|
+
|
1204
|
+
exit_now! 'Unrecognized date string' unless start
|
1205
|
+
|
1206
|
+
message = "Date interpreted as #{start} through the current time"
|
1207
|
+
wwid.results.push(message)
|
1208
|
+
|
1209
|
+
options[:t] = true if options[:totals]
|
1210
|
+
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1211
|
+
|
1212
|
+
puts wwid.list_date([start, finish], options[:s], options[:t], options[:output],
|
1213
|
+
{ totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
|
1214
|
+
end
|
1215
|
+
end
|
1216
|
+
|
1103
1217
|
desc 'List entries from yesterday'
|
1104
1218
|
command :yesterday do |c|
|
1105
1219
|
c.desc 'Specify a section'
|
@@ -1111,10 +1225,10 @@ command :yesterday do |c|
|
|
1111
1225
|
c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
|
1112
1226
|
|
1113
1227
|
c.desc 'Show time intervals on @done tasks'
|
1114
|
-
c.switch %i[t times], default_value: true
|
1228
|
+
c.switch %i[t times], default_value: true, negatable: true
|
1115
1229
|
|
1116
1230
|
c.desc 'Show time totals at the end of output'
|
1117
|
-
c.switch [:totals], default_value: false, negatable:
|
1231
|
+
c.switch [:totals], default_value: false, negatable: false
|
1118
1232
|
|
1119
1233
|
c.desc 'Sort tags by (name|time)'
|
1120
1234
|
default = 'time'
|
@@ -1151,7 +1265,7 @@ command :last do |c|
|
|
1151
1265
|
c.flag [:search]
|
1152
1266
|
|
1153
1267
|
c.action do |_global_options, options, _args|
|
1154
|
-
|
1268
|
+
exit_now! '--tag and --search cannot be used together' if options[:tag] && options[:search]
|
1155
1269
|
|
1156
1270
|
if options[:tag].nil?
|
1157
1271
|
tags = []
|
@@ -1200,7 +1314,7 @@ desc 'Add a new section to the "doing" file'
|
|
1200
1314
|
arg_name 'SECTION_NAME'
|
1201
1315
|
command :add_section do |c|
|
1202
1316
|
c.action do |_global_options, _options, args|
|
1203
|
-
|
1317
|
+
exit_now! "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
|
1204
1318
|
|
1205
1319
|
wwid.add_section(args[0].cap_first)
|
1206
1320
|
wwid.write(wwid.doing_file)
|
@@ -1241,10 +1355,10 @@ command :view do |c|
|
|
1241
1355
|
c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
|
1242
1356
|
|
1243
1357
|
c.desc 'Show time intervals on @done tasks'
|
1244
|
-
c.switch %i[t times], default_value: true
|
1358
|
+
c.switch %i[t times], default_value: true, negatable: true
|
1245
1359
|
|
1246
1360
|
c.desc 'Show intervals with totals at the end of output'
|
1247
|
-
c.switch [:totals], default_value: false, negatable:
|
1361
|
+
c.switch [:totals], default_value: false, negatable: false
|
1248
1362
|
|
1249
1363
|
c.desc 'Include colors in output'
|
1250
1364
|
c.switch [:color], default_value: true, negatable: true
|
@@ -1256,7 +1370,7 @@ command :view do |c|
|
|
1256
1370
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1257
1371
|
|
1258
1372
|
c.desc 'Only show items with recorded time intervals'
|
1259
|
-
c.switch [:only_timed], default_value: false, negatable:
|
1373
|
+
c.switch [:only_timed], default_value: false, negatable: false
|
1260
1374
|
|
1261
1375
|
c.action do |_global_options, options, args|
|
1262
1376
|
title = if args.empty?
|
@@ -1328,7 +1442,7 @@ command :view do |c|
|
|
1328
1442
|
elsif title.instance_of?(FalseClass)
|
1329
1443
|
exit_now! 'Cancelled'
|
1330
1444
|
else
|
1331
|
-
|
1445
|
+
exit_now! "View #{title} not found in config"
|
1332
1446
|
end
|
1333
1447
|
end
|
1334
1448
|
end
|
@@ -1385,7 +1499,7 @@ command :archive do |c|
|
|
1385
1499
|
tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
|
1386
1500
|
end
|
1387
1501
|
|
1388
|
-
|
1502
|
+
exit_now! '--keep and --count can\'t be used together' if options[:keep] && options[:count]
|
1389
1503
|
|
1390
1504
|
tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag]
|
1391
1505
|
|
@@ -1436,7 +1550,7 @@ command :open do |c|
|
|
1436
1550
|
elsif options[:b]
|
1437
1551
|
system %(open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}")
|
1438
1552
|
elsif options[:e]
|
1439
|
-
|
1553
|
+
exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
|
1440
1554
|
|
1441
1555
|
system %($EDITOR "#{File.expand_path(wwid.doing_file)}")
|
1442
1556
|
elsif wwid.config.key?('editor_app') && !wwid.config['editor_app'].nil?
|
@@ -1446,7 +1560,7 @@ command :open do |c|
|
|
1446
1560
|
end
|
1447
1561
|
|
1448
1562
|
else
|
1449
|
-
|
1563
|
+
exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
|
1450
1564
|
|
1451
1565
|
system %($EDITOR "#{File.expand_path(wwid.doing_file)}")
|
1452
1566
|
end
|
@@ -1483,13 +1597,13 @@ command :config do |c|
|
|
1483
1597
|
`open -b #{options[:b]} "#{wwid.config_file}"`
|
1484
1598
|
end
|
1485
1599
|
else
|
1486
|
-
|
1600
|
+
exit_now! 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
|
1487
1601
|
|
1488
1602
|
editor = options[:e].nil? ? ENV['EDITOR'] : options[:e]
|
1489
1603
|
system %(#{editor} "#{wwid.config_file}")
|
1490
1604
|
end
|
1491
1605
|
else
|
1492
|
-
|
1606
|
+
exit_now! 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
|
1493
1607
|
|
1494
1608
|
editor = options[:e].nil? ? ENV['EDITOR'] : options[:e]
|
1495
1609
|
system %(#{editor} "#{wwid.config_file}")
|
@@ -1542,7 +1656,7 @@ command :import do |c|
|
|
1542
1656
|
wwid.write(wwid.doing_file)
|
1543
1657
|
end
|
1544
1658
|
else
|
1545
|
-
|
1659
|
+
exit_now! 'Invalid import type'
|
1546
1660
|
end
|
1547
1661
|
end
|
1548
1662
|
end
|
data/lib/doing/helpers.rb
CHANGED
@@ -92,7 +92,7 @@ class ::String
|
|
92
92
|
def link_urls(opt = {})
|
93
93
|
opt[:format] ||= :html
|
94
94
|
if opt[:format] == :html
|
95
|
-
gsub(%r{(?mi)((http|https)://)
|
95
|
+
gsub(%r{(?mi)((http|https)://)([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&:/~+#]*[\w\-@^=%&/~+#])?}) do |_match|
|
96
96
|
m = Regexp.last_match
|
97
97
|
proto = m[1].nil? ? 'http://' : ''
|
98
98
|
%(<a href="#{proto}#{m[0]}" title="Link to #{m[0]}">[#{m[3]}]</a>)
|
data/lib/doing/version.rb
CHANGED
data/lib/doing/wwid.rb
CHANGED
@@ -73,7 +73,7 @@ class WWID
|
|
73
73
|
rescue StandardError
|
74
74
|
@config = {}
|
75
75
|
@local_config = {}
|
76
|
-
#
|
76
|
+
# exit_now! "error reading config"
|
77
77
|
end
|
78
78
|
end
|
79
79
|
|
@@ -158,7 +158,7 @@ class WWID
|
|
158
158
|
|
159
159
|
# if ENV['DOING_DEBUG'].to_i == 3
|
160
160
|
# if @config['default_tags'].length > 0
|
161
|
-
#
|
161
|
+
# exit_now! "DEFAULT CONFIG CHANGED"
|
162
162
|
# end
|
163
163
|
# end
|
164
164
|
|
@@ -293,14 +293,14 @@ class WWID
|
|
293
293
|
if $?.exitstatus == 0
|
294
294
|
input = IO.read(tmpfile.path)
|
295
295
|
else
|
296
|
-
|
296
|
+
exit_now! 'Cancelled'
|
297
297
|
end
|
298
298
|
ensure
|
299
299
|
tmpfile.close
|
300
300
|
tmpfile.unlink
|
301
301
|
end
|
302
302
|
|
303
|
-
input.split(/\n/).delete_if {|line| line =~ /^#/ }.join("\n")
|
303
|
+
input.split(/\n/).delete_if {|line| line =~ /^#/ }.join("\n")
|
304
304
|
end
|
305
305
|
|
306
306
|
#
|
@@ -311,11 +311,11 @@ class WWID
|
|
311
311
|
# @param input (String) The string to parse
|
312
312
|
#
|
313
313
|
def format_input(input)
|
314
|
-
|
314
|
+
exit_now! 'No content in entry' if input.nil? || input.strip.empty?
|
315
315
|
|
316
316
|
input_lines = input.split(/[\n\r]+/).delete_if {|line| line =~ /^#/ || line =~ /^\s*$/ }
|
317
317
|
title = input_lines[0]&.strip
|
318
|
-
|
318
|
+
exit_now! 'No content in first line' if title.nil? || title.strip.empty?
|
319
319
|
|
320
320
|
note = input_lines.length > 1 ? input_lines[1..-1] : []
|
321
321
|
# If title line ends in a parenthetical, use that as the note
|
@@ -346,7 +346,7 @@ class WWID
|
|
346
346
|
#
|
347
347
|
def chronify(input)
|
348
348
|
now = Time.now
|
349
|
-
|
349
|
+
exit_now! "Invalid time expression #{input.inspect}" if input.to_s.strip == ''
|
350
350
|
|
351
351
|
secs_ago = if input.match(/^(\d+)$/)
|
352
352
|
# plain number, assume minutes
|
@@ -438,7 +438,7 @@ class WWID
|
|
438
438
|
end
|
439
439
|
unless section || guessed
|
440
440
|
alt = guess_view(frag, true)
|
441
|
-
|
441
|
+
exit_now! "Did you mean `doing view #{alt}`?" if alt
|
442
442
|
|
443
443
|
res = yn("Section #{frag} not found, create it", default_response: false)
|
444
444
|
|
@@ -448,7 +448,7 @@ class WWID
|
|
448
448
|
return frag.cap_first
|
449
449
|
end
|
450
450
|
|
451
|
-
|
451
|
+
exit_now! "Unknown section: #{frag}"
|
452
452
|
end
|
453
453
|
section ? section.cap_first : guessed
|
454
454
|
end
|
@@ -462,7 +462,7 @@ class WWID
|
|
462
462
|
## @return (Bool) yes or no
|
463
463
|
##
|
464
464
|
def yn(question, default_response: false)
|
465
|
-
default = default_response ?
|
465
|
+
default = default_response ? default_response : 'n'
|
466
466
|
|
467
467
|
# if this isn't an interactive shell, answer default
|
468
468
|
return default.downcase == 'y' unless $stdout.isatty
|
@@ -518,9 +518,9 @@ class WWID
|
|
518
518
|
unless view || guessed
|
519
519
|
alt = guess_section(frag, guessed: true)
|
520
520
|
if alt
|
521
|
-
|
521
|
+
exit_now! "Did you mean `doing show #{alt}`?"
|
522
522
|
else
|
523
|
-
|
523
|
+
exit_now! "Unknown view: #{frag}"
|
524
524
|
end
|
525
525
|
end
|
526
526
|
view
|
@@ -631,7 +631,7 @@ class WWID
|
|
631
631
|
|
632
632
|
add_tags = opt[:tag] ? opt[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '@') }.join(' ') : ''
|
633
633
|
prefix = opt[:prefix] ? opt[:prefix] : '[Timing.app]'
|
634
|
-
|
634
|
+
exit_now! "File not found" unless File.exist?(File.expand_path(path))
|
635
635
|
|
636
636
|
data = JSON.parse(IO.read(File.expand_path(path)))
|
637
637
|
new_items = []
|
@@ -688,7 +688,7 @@ class WWID
|
|
688
688
|
section = combined['items'].dup.sort_by { |item| item['date'] }.reverse[0]['section']
|
689
689
|
end
|
690
690
|
|
691
|
-
|
691
|
+
exit_now! "Section #{section} not found" unless @content.key?(section)
|
692
692
|
|
693
693
|
last_item = @content[section]['items'].dup.sort_by { |item| item['date'] }.reverse[0]
|
694
694
|
warn "Editing note for #{last_item['title']}"
|
@@ -766,6 +766,22 @@ class WWID
|
|
766
766
|
all_items.max_by { |item| item['date'] }
|
767
767
|
end
|
768
768
|
|
769
|
+
##
|
770
|
+
## @brief Generate a menu of options and allow user selection
|
771
|
+
##
|
772
|
+
## @return (String) The selected option
|
773
|
+
##
|
774
|
+
def choose_from(options, prompt)
|
775
|
+
puts prompt
|
776
|
+
options.each_with_index do |section, i|
|
777
|
+
puts format('% 3d: %s', i + 1, section)
|
778
|
+
end
|
779
|
+
print "#{colors['green']}> #{colors['default']}"
|
780
|
+
num = STDIN.gets
|
781
|
+
return false if num =~ /^[a-z ]*$/i
|
782
|
+
|
783
|
+
options[num.to_i - 1]
|
784
|
+
end
|
769
785
|
|
770
786
|
##
|
771
787
|
## @brief Display an interactive menu of entries
|
@@ -773,7 +789,7 @@ class WWID
|
|
773
789
|
## @param opt (Hash) Additional options
|
774
790
|
##
|
775
791
|
def interactive(opt = {})
|
776
|
-
|
792
|
+
fzf = File.join(File.dirname(__FILE__), '../helpers/fuzzyfilefinder')
|
777
793
|
|
778
794
|
section = opt[:section] ? guess_section(opt[:section]) : 'All'
|
779
795
|
|
@@ -807,7 +823,7 @@ class WWID
|
|
807
823
|
out.join('')
|
808
824
|
end
|
809
825
|
|
810
|
-
res = `echo #{Shellwords.escape(options.join("\n"))}
|
826
|
+
res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} -m --bind ctrl-a:select-all -q "#{opt[:query]}"`
|
811
827
|
selected = []
|
812
828
|
res.split(/\n/).each do |item|
|
813
829
|
idx = item.match(/^(\d+)\)/)[1].to_i
|
@@ -819,6 +835,59 @@ class WWID
|
|
819
835
|
return
|
820
836
|
end
|
821
837
|
|
838
|
+
unless opt[:delete] || opt[:flag] || opt[:finish] || opt[:cancel] || opt[:tag] || opt[:archive] || opt[:output] || opt[:save_to]
|
839
|
+
action = choose_from(
|
840
|
+
[
|
841
|
+
"add tag",
|
842
|
+
"remove tag",
|
843
|
+
"archive",
|
844
|
+
"cancel",
|
845
|
+
"delete",
|
846
|
+
"edit",
|
847
|
+
"finish",
|
848
|
+
"flag",
|
849
|
+
"move",
|
850
|
+
"output format"
|
851
|
+
],
|
852
|
+
'What do you want to do with the selected items?')
|
853
|
+
case action
|
854
|
+
when /(add|remove) tag/
|
855
|
+
print "Enter tag: "
|
856
|
+
tag = STDIN.gets
|
857
|
+
return if tag =~ /^ *$/
|
858
|
+
opt[:tag] = tag.strip
|
859
|
+
opt[:remove] = true if action =~ /remove tag/
|
860
|
+
when /output format/
|
861
|
+
output_format = choose_from(%w[doing taskpaper json timeline html csv], 'Which output format?')
|
862
|
+
return if tag =~ /^ *$/
|
863
|
+
opt[:output] = output_format.strip
|
864
|
+
res = yn("Save to file?", default_response: 'n')
|
865
|
+
if res
|
866
|
+
print "File path/name: "
|
867
|
+
filename = STDIN.gets.strip
|
868
|
+
return if filename.empty?
|
869
|
+
opt[:save_to] = filename
|
870
|
+
end
|
871
|
+
when /archive/
|
872
|
+
opt[:archive] = true
|
873
|
+
when /delete/
|
874
|
+
opt[:delete] = true
|
875
|
+
when /edit/
|
876
|
+
opt[:editor] = true
|
877
|
+
when /finish/
|
878
|
+
opt[:finish] = true
|
879
|
+
when /cancel/
|
880
|
+
opt[:cancel] = true
|
881
|
+
when /move/
|
882
|
+
section = choose_section.strip
|
883
|
+
return if section =~ /^ *$/
|
884
|
+
opt[:move] = section.strip
|
885
|
+
when /flag/
|
886
|
+
opt[:flag] = true
|
887
|
+
end
|
888
|
+
end
|
889
|
+
|
890
|
+
|
822
891
|
if opt[:delete]
|
823
892
|
res = yn("Delete #{selected.size} items?", default_response: 'y')
|
824
893
|
if res
|
@@ -830,17 +899,35 @@ class WWID
|
|
830
899
|
|
831
900
|
if opt[:flag]
|
832
901
|
tag = @config['marker_tag'] || 'flagged'
|
833
|
-
selected.map!
|
902
|
+
selected.map! do |item|
|
903
|
+
if opt[:remove]
|
904
|
+
untag_item(item, tag)
|
905
|
+
else
|
906
|
+
tag_item(item, tag, date: false)
|
907
|
+
end
|
908
|
+
end
|
834
909
|
end
|
835
910
|
|
836
911
|
if opt[:finish] || opt[:cancel]
|
837
912
|
tag = 'done'
|
838
|
-
selected.map!
|
913
|
+
selected.map! do |item|
|
914
|
+
if opt[:remove]
|
915
|
+
untag_item(item, tag)
|
916
|
+
else
|
917
|
+
tag_item(item, tag, date: !opt[:cancel])
|
918
|
+
end
|
919
|
+
end
|
839
920
|
end
|
840
921
|
|
841
922
|
if opt[:tag]
|
842
923
|
tag = opt[:tag]
|
843
|
-
selected.map!
|
924
|
+
selected.map! do |item|
|
925
|
+
if opt[:remove]
|
926
|
+
untag_item(item, tag)
|
927
|
+
else
|
928
|
+
tag_item(item, tag, date: false)
|
929
|
+
end
|
930
|
+
end
|
844
931
|
end
|
845
932
|
|
846
933
|
if opt[:archive] || opt[:move]
|
@@ -860,13 +947,17 @@ class WWID
|
|
860
947
|
editable += "\n#{old_note}" unless old_note.nil?
|
861
948
|
editable_items << editable
|
862
949
|
end
|
950
|
+
divider = "\n-----------\n"
|
951
|
+
input = editable_items.map(&:strip).join(divider) + "\n\n# You may delete entries, but leave all divider lines in place"
|
863
952
|
|
864
|
-
new_items = fork_editor(
|
953
|
+
new_items = fork_editor(input).split(/#{divider}/)
|
865
954
|
|
866
955
|
new_items.each_with_index do |new_item, i|
|
956
|
+
|
867
957
|
input_lines = new_item.split(/[\n\r]+/).delete_if {|line| line =~ /^#/ || line =~ /^\s*$/ }
|
868
958
|
title = input_lines[0]&.strip
|
869
|
-
|
959
|
+
|
960
|
+
if title.nil? || title =~ /^#{divider.strip}$/ || title.strip.empty?
|
870
961
|
delete_item(selected[i])
|
871
962
|
else
|
872
963
|
note = input_lines.length > 1 ? input_lines[1..-1] : []
|
@@ -880,13 +971,47 @@ class WWID
|
|
880
971
|
item = selected[i].dup
|
881
972
|
item['title'] = title
|
882
973
|
item['note'] = note
|
883
|
-
item['date'] = Time.parse(date)
|
974
|
+
item['date'] = Time.parse(date) || selected[i]['date']
|
884
975
|
update_item(selected[i], item)
|
885
976
|
end
|
886
977
|
end
|
887
978
|
|
888
979
|
write(@doing_file)
|
889
980
|
end
|
981
|
+
|
982
|
+
if opt[:output]
|
983
|
+
selected.map! do |item|
|
984
|
+
item['title'] = "#{item['title']} @project(#{item['section']})"
|
985
|
+
item
|
986
|
+
end
|
987
|
+
|
988
|
+
@content = {'Export' => {'original' => 'Export:', 'items' => selected}}
|
989
|
+
options = {section: 'Export'}
|
990
|
+
|
991
|
+
if opt[:output] !~ /(doing|taskpaper)/
|
992
|
+
options[:output] = opt[:output]
|
993
|
+
else
|
994
|
+
options[:template] = '- %date | %title%note'
|
995
|
+
end
|
996
|
+
|
997
|
+
output = list_section(options)
|
998
|
+
|
999
|
+
if opt[:save_to]
|
1000
|
+
file = File.expand_path(opt[:save_to])
|
1001
|
+
if File.exist?(file)
|
1002
|
+
# Create a backup copy for the undo command
|
1003
|
+
FileUtils.cp(file, "#{file}~")
|
1004
|
+
end
|
1005
|
+
|
1006
|
+
File.open(file, 'w+') do |f|
|
1007
|
+
f.puts output
|
1008
|
+
end
|
1009
|
+
|
1010
|
+
@results.push("Export saved to #{file}")
|
1011
|
+
else
|
1012
|
+
puts output
|
1013
|
+
end
|
1014
|
+
end
|
890
1015
|
end
|
891
1016
|
|
892
1017
|
##
|
@@ -895,7 +1020,7 @@ class WWID
|
|
895
1020
|
## @param opt (Hash) Additional Options
|
896
1021
|
##
|
897
1022
|
def tag_last(opt = {})
|
898
|
-
opt[:section] ||=
|
1023
|
+
opt[:section] ||= nil
|
899
1024
|
opt[:count] ||= 1
|
900
1025
|
opt[:archive] ||= false
|
901
1026
|
opt[:tags] ||= ['done']
|
@@ -910,7 +1035,11 @@ class WWID
|
|
910
1035
|
sec_arr = []
|
911
1036
|
|
912
1037
|
if opt[:section].nil?
|
913
|
-
|
1038
|
+
if opt[:search] || opt[:tag]
|
1039
|
+
sec_arr = sections
|
1040
|
+
else
|
1041
|
+
sec_arr = [@current_section]
|
1042
|
+
end
|
914
1043
|
elsif opt[:section].instance_of?(String)
|
915
1044
|
if opt[:section] =~ /^all$/i
|
916
1045
|
if opt[:count] == 1
|
@@ -921,7 +1050,11 @@ class WWID
|
|
921
1050
|
items = combined['items'].dup.sort_by { |item| item['date'] }.reverse
|
922
1051
|
sec_arr.push(items[0]['section'])
|
923
1052
|
elsif opt[:count] > 1
|
924
|
-
|
1053
|
+
if opt[:search] || opt[:tag]
|
1054
|
+
sec_arr = sections
|
1055
|
+
else
|
1056
|
+
exit_now! 'A count greater than one requires a section to be specified'
|
1057
|
+
end
|
925
1058
|
else
|
926
1059
|
sec_arr = sections
|
927
1060
|
end
|
@@ -1021,7 +1154,7 @@ class WWID
|
|
1021
1154
|
@results.push('Archiving is skipped when operating on all entries') if (opt[:count]).zero?
|
1022
1155
|
end
|
1023
1156
|
else
|
1024
|
-
|
1157
|
+
exit_now! "Section not found: #{section}"
|
1025
1158
|
end
|
1026
1159
|
end
|
1027
1160
|
|
@@ -1060,6 +1193,28 @@ class WWID
|
|
1060
1193
|
@content[section]['items'] = section_items
|
1061
1194
|
end
|
1062
1195
|
|
1196
|
+
##
|
1197
|
+
## @brief Remove a tag on an item from the index
|
1198
|
+
##
|
1199
|
+
## @param old_item (Item) The item to tag
|
1200
|
+
## @param tag (string) The tag to remove
|
1201
|
+
##
|
1202
|
+
def untag_item(old_item, tag)
|
1203
|
+
title = old_item['title'].dup
|
1204
|
+
|
1205
|
+
if title =~ /@#{tag}/
|
1206
|
+
title.chomp!
|
1207
|
+
title.gsub!(/ +@#{tag}(\(.*?\))?/, '')
|
1208
|
+
new_item = old_item.dup
|
1209
|
+
new_item['title'] = title
|
1210
|
+
update_item(old_item, new_item)
|
1211
|
+
return new_item
|
1212
|
+
else
|
1213
|
+
@results.push(%(Item isn't tagged @#{tag}: "#{title}" in #{old_item['section']}))
|
1214
|
+
return old_item
|
1215
|
+
end
|
1216
|
+
end
|
1217
|
+
|
1063
1218
|
##
|
1064
1219
|
## @brief Tag an item from the index
|
1065
1220
|
##
|
@@ -1067,7 +1222,7 @@ class WWID
|
|
1067
1222
|
## @param tag (string) The tag to apply
|
1068
1223
|
## @param date (Boolean) Include timestamp?
|
1069
1224
|
##
|
1070
|
-
def tag_item(old_item, tag, date: false)
|
1225
|
+
def tag_item(old_item, tag, remove: false, date: false)
|
1071
1226
|
title = old_item['title'].dup
|
1072
1227
|
done_date = Time.now
|
1073
1228
|
if title !~ /@#{tag}/
|
@@ -1191,7 +1346,7 @@ class WWID
|
|
1191
1346
|
section = combined['items'].dup.sort_by { |item| item['date'] }.reverse[0]['section']
|
1192
1347
|
end
|
1193
1348
|
|
1194
|
-
|
1349
|
+
exit_now! "Section #{section} not found" unless @content.key?(section)
|
1195
1350
|
|
1196
1351
|
# sort_section(opt[:section])
|
1197
1352
|
items = @content[section]['items'].dup.sort_by { |item| item['date'] }.reverse
|
@@ -1297,10 +1452,10 @@ class WWID
|
|
1297
1452
|
if File.exist?(file)
|
1298
1453
|
# Create a backup copy for the undo command
|
1299
1454
|
FileUtils.cp(file, "#{file}~")
|
1455
|
+
end
|
1300
1456
|
|
1301
|
-
|
1302
|
-
|
1303
|
-
end
|
1457
|
+
File.open(file, 'w+') do |f|
|
1458
|
+
f.puts output
|
1304
1459
|
end
|
1305
1460
|
|
1306
1461
|
if @config.key?('run_after')
|
@@ -1427,7 +1582,7 @@ class WWID
|
|
1427
1582
|
end
|
1428
1583
|
end
|
1429
1584
|
|
1430
|
-
|
1585
|
+
exit_now! 'Invalid section object' unless opt[:section].instance_of? Hash
|
1431
1586
|
|
1432
1587
|
items = opt[:section]['items'].sort_by { |item| item['date'] }
|
1433
1588
|
|
@@ -1477,7 +1632,7 @@ class WWID
|
|
1477
1632
|
|
1478
1633
|
out = ''
|
1479
1634
|
|
1480
|
-
|
1635
|
+
exit_now! 'Unknown output format' if opt[:output] && (opt[:output] !~ /^(template|html|csv|json|timeline)$/i)
|
1481
1636
|
|
1482
1637
|
case opt[:output]
|
1483
1638
|
when /^csv$/i
|
@@ -1514,13 +1669,16 @@ class WWID
|
|
1514
1669
|
note ||= ''
|
1515
1670
|
|
1516
1671
|
tags = []
|
1672
|
+
attributes = {}
|
1517
1673
|
skip_tags = %w[meanwhile done cancelled flagged]
|
1518
1674
|
i['title'].scan(/@([^(\s]+)(?:\((.*?)\))?/).each do |tag|
|
1519
1675
|
tags.push(tag[0]) unless skip_tags.include?(tag[0])
|
1676
|
+
attributes[tag[0]] = tag[1] if tag[1]
|
1520
1677
|
end
|
1678
|
+
|
1521
1679
|
if opt[:output] == 'json'
|
1522
1680
|
|
1523
|
-
|
1681
|
+
i = {
|
1524
1682
|
date: i['date'],
|
1525
1683
|
end_date: end_date,
|
1526
1684
|
title: title.strip, #+ " #{note}"
|
@@ -1529,6 +1687,10 @@ class WWID
|
|
1529
1687
|
tags: tags
|
1530
1688
|
}
|
1531
1689
|
|
1690
|
+
attributes.each { |attr, val| i[attr.to_sym] = val }
|
1691
|
+
|
1692
|
+
items_out << i
|
1693
|
+
|
1532
1694
|
elsif opt[:output] == 'timeline'
|
1533
1695
|
new_item = {
|
1534
1696
|
'id' => index + 1,
|
@@ -1632,7 +1794,7 @@ class WWID
|
|
1632
1794
|
|
1633
1795
|
totals = opt[:totals] ? tag_times('html', opt[:sort_tags]) : ''
|
1634
1796
|
engine = Haml::Engine.new(template)
|
1635
|
-
|
1797
|
+
out = engine.render(Object.new,
|
1636
1798
|
{ :@items => items_out, :@page_title => page_title, :@style => style, :@totals => totals })
|
1637
1799
|
else
|
1638
1800
|
items.each do |item|
|
@@ -1644,11 +1806,12 @@ class WWID
|
|
1644
1806
|
reset = ''
|
1645
1807
|
end
|
1646
1808
|
|
1647
|
-
if (item.
|
1809
|
+
if (item.key?('note') && !item['note'].empty?) && @config[:include_notes]
|
1648
1810
|
note_lines = item['note'].delete_if do |line|
|
1649
|
-
|
1650
|
-
|
1651
|
-
|
1811
|
+
line =~ /^\s*$/
|
1812
|
+
end
|
1813
|
+
note_lines.map! { |line| "\t\t#{line.sub(/^\t*/, '').sub(/^-/, '—')} " }
|
1814
|
+
if opt[:wrap_width]&.positive?
|
1652
1815
|
width = opt[:wrap_width]
|
1653
1816
|
note_lines.map! do |line|
|
1654
1817
|
line.strip.gsub(/(.{1,#{width}})(\s+|\Z)/, "\t\\1\n")
|
@@ -1661,7 +1824,7 @@ class WWID
|
|
1661
1824
|
output = opt[:template].dup
|
1662
1825
|
|
1663
1826
|
output.gsub!(/%[a-z]+/) do |m|
|
1664
|
-
if colors.
|
1827
|
+
if colors.key?(m.sub(/^%/, ''))
|
1665
1828
|
colors[m.sub(/^%/, '')]
|
1666
1829
|
else
|
1667
1830
|
m
|
@@ -1750,7 +1913,7 @@ class WWID
|
|
1750
1913
|
do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label] })
|
1751
1914
|
write(doing_file)
|
1752
1915
|
else
|
1753
|
-
|
1916
|
+
exit_now! 'Either source or destination does not exist'
|
1754
1917
|
end
|
1755
1918
|
end
|
1756
1919
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: doing
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.78
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brett Terpstra
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-09-
|
11
|
+
date: 2021-09-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -36,14 +36,14 @@ dependencies:
|
|
36
36
|
requirements:
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version: 6.
|
39
|
+
version: 6.3.1
|
40
40
|
type: :development
|
41
41
|
prerelease: false
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: 6.
|
46
|
+
version: 6.3.1
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: aruba
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -62,16 +62,16 @@ dependencies:
|
|
62
62
|
name: test-unit
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
65
|
-
- - "
|
65
|
+
- - "~>"
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version:
|
67
|
+
version: 3.4.4
|
68
68
|
type: :development
|
69
69
|
prerelease: false
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
|
-
- - "
|
72
|
+
- - "~>"
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version:
|
74
|
+
version: 3.4.4
|
75
75
|
- !ruby/object:Gem::Dependency
|
76
76
|
name: gli
|
77
77
|
requirement: !ruby/object:Gem::Requirement
|