doing 1.0.62 → 1.0.66
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +50 -8
- data/bin/doing +129 -37
- data/lib/doing.rb +1 -0
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +112 -126
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6e02c8453f718dfa428e578ab1b125528aa07ffee1390bbe8222fb425abd63fa
|
4
|
+
data.tar.gz: 333707321b7e92312ee8a2874292b45df782c83eff2ddcdb5279b648b14730ff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 187ce7735035b86ae1233d08dd154f502c7b2def0fc1fd11532de5256647c8cce57bd4ea73ca3fa704c2dfb4a307b94f339222a476e3fd30ba991fea77daa190
|
7
|
+
data.tar.gz: 29f7123aa51cb8a5754bca68854dbbb606f69533d8cdbd90a7a2187ca1cc29dd09131aed0787f72fd680e2d04fe9e47a92555cf90c1327b74cba23cc8a2fab2b
|
data/README.md
CHANGED
@@ -29,7 +29,7 @@ _Side note:_ I actually use the library behind this utility as part of another s
|
|
29
29
|
|
30
30
|
## Installation
|
31
31
|
|
32
|
-
The current version of `doing` is <!--VER-->1.0.
|
32
|
+
The current version of `doing` is <!--VER-->1.0.65<!--END VER-->.
|
33
33
|
|
34
34
|
$ [sudo] gem install doing
|
35
35
|
|
@@ -394,24 +394,41 @@ Note that you can include a tag with synonyms in the whitelist as well to tag it
|
|
394
394
|
|
395
395
|
#### Adding entries:
|
396
396
|
|
397
|
-
now,
|
397
|
+
now, next - Add an entry
|
398
398
|
later - Add an item to the Later section
|
399
|
-
done
|
399
|
+
done, did - Add a completed item with @done(date). No argument finishes last entry.
|
400
400
|
meanwhile - Finish any @meanwhile tasks and optionally create a new one
|
401
401
|
again, resume - Duplicate the last entry as new entry (without @done tag)
|
402
402
|
|
403
|
-
|
403
|
+
##### now
|
404
404
|
|
405
|
-
|
405
|
+
The `doing now` command adds an entry to the "Currently" section by default. It accepts `-s section_name` to send the new entry straight to a non-default section. It also accepts `--back=AMOUNT` to let you specify a start date in the past using "natural language." For example, `doing now --back=25m ENTRY` or `doing now --back="yesterday 3:30pm" ENTRY`.
|
406
406
|
|
407
407
|
You can finish the last unfinished task when starting a new one using `doing now` with the `-f` switch. It will look for the last task not marked _@done_ and add the _@done_ tag with the start time of the new task (either the current time or what you specified with `--back`).
|
408
408
|
|
409
|
-
|
409
|
+
> __Examples:__
|
410
|
+
>
|
411
|
+
> - `doing now Working on project X` --- Add a new entry at the current time
|
412
|
+
> - `doing now --section=Projects Working on @projectX` --- Add a new entry to a section titled "Projects"
|
413
|
+
> - `doing now --back 30m` --- Add a new entry with a backdated start time, indicating you've been working on it for 30 minutes already
|
414
|
+
> - `doing now --back 8am` --- Backdate a new entry to a specific time
|
415
|
+
> - `doing now -f Starting the next thing` --- Add a new entry at the current time, and add a @done tag to the previous item with the current time as its completion date
|
416
|
+
> - `doing now -f --back 30m Working on something new` --- Add a new entry and complete the last entry, but use a timestamp from 30 minutes ago for both.
|
410
417
|
|
411
|
-
|
418
|
+
##### done/finish
|
419
|
+
|
420
|
+
`doing done` is used to add an entry that you've already completed. Like `now`, you can specify a section with `-s section_name`. You can also skip straight to the Archive with `-a`.
|
421
|
+
|
422
|
+
`doing done` can also backdate entries using natural language with `--back 15m` or `--back "3/15 3pm"`. That will modify the starting timestamp of the entry. Used on its own, it will set the start date, and the finish date will be the current time. You can also use `--took 1h20m` or `--took 1:20` to set the finish date based on a "natural language" time interval. If `--took` is used without `--back`, then the start date is adjusted (`--took` interval is subtracted) so that the completion date is the current time.
|
412
423
|
|
413
424
|
When used with `doing done`, `--back` and `--took` allow time intervals to be accurately counted when entering items after the fact. `--took` is also available for the `doing finish` command, but cannot be used in conjunction with `--back`. (In `finish` they both set the end date, and neither has priority. `--back` allows specific days/times, `--took` uses time intervals.)
|
414
425
|
|
426
|
+
> __Examples:__
|
427
|
+
>
|
428
|
+
> - `doing done --back 1h The thing I just did` --- Add an entry for a completed task that you started an hour ago and just finished
|
429
|
+
> - `doing done --back 1h --took 20m The thing I just finished` --- Record an entry you started an hour ago and finished 20 minutes later
|
430
|
+
> - `doing done --took 20m` --- Finish the last task but change the finish date so that the total elapsed time is only 20 minutes (if the finish date would be in the future, start date will be adjusted accordingly)
|
431
|
+
|
415
432
|
All of these commands accept a `-e` argument. This opens your command line editor (as defined in the environment variable `$EDITOR`). Add your entry, save the temp file, and close it. The new entry is added. Anything after the first line is included as a note on the entry.
|
416
433
|
|
417
434
|
`doing again` (or `doing resume`) will duplicate the last @done entry (most recently completed) with a new start date (and without the @done tag). To resume the last entry matching specific tags, use `--tag=TAG`. You can specify multiple tags by separating with a comma. Multiple tags are combined with 'AND' by default (all tags must exist on the entry to match), but you can use `--bool=` to set it to 'OR' or 'NOT'. By default the new entry will be added to the same section as the matching entry, but you can specify a section with `--in=SECTION`.
|
@@ -429,7 +446,7 @@ See `doing help meanwhile` for more options.
|
|
429
446
|
|
430
447
|
##### Finishing
|
431
448
|
|
432
|
-
`doing finish` by itself is the same as `doing done` by itself. It adds _@done(timestamp)_ to the last entry. It also accepts a numeric argument to complete X number of tasks back in history. Add `-a` to also archive the affected entries.
|
449
|
+
`doing finish` by itself is the same as `doing done` by itself. It adds _@done(timestamp)_ to the last entry. It also accepts a numeric argument to complete X number of tasks back in history (0 affects all entries). Add `-a` to also archive the affected entries.
|
433
450
|
|
434
451
|
`doing finish` also provides an `--auto` flag, which you can use to set the end time of any entry to 1 minute before the start time of the next. Running a command such as `doing finish --auto 10` will go through the last 10 entries and sequentially update any without a _@done_ tag with one set to the time just before the next entry in the list.
|
435
452
|
|
@@ -445,6 +462,8 @@ You can change the boolean using `--bool=OR` (last entry containing any of the s
|
|
445
462
|
|
446
463
|
You can also include a `--no-date` switch to add `@done` without a finish date, meaning no time is tracked for the task. `doing cancel` is an alias for this. Like `finish`, `cancel` accepts a count to act on the last X entries, as well as `--archive` and `--section` options. `cancel` also accepts the `--tag` and `--bool` flags for tag filtering.
|
447
464
|
|
465
|
+
By default `doing finish` works on a single entry, the last entry or the most recent entry matching a `--tag` or `--search` query. Specifying `doing finish 10` would finish any unfinished entries within the last 10 entries. In the case of `--tag` or `--search` queries, the count serves as the maximum number of matches doing will act on, sorted in reverse date order (most recent first). A count of 0 will disable the limit entirely, acting on all matching entries.
|
466
|
+
|
448
467
|
|
449
468
|
##### Tagging and Autotagging
|
450
469
|
|
@@ -576,6 +595,29 @@ Example: Archive all Currently items for _@client_ that are marked _@done_
|
|
576
595
|
|
577
596
|
doing archive @client @done
|
578
597
|
|
598
|
+
##### Importing
|
599
|
+
|
600
|
+
Doing can currently only import tasks from Timing.app reports. If you want to sync up your Doing file with Timing's tracking:
|
601
|
+
|
602
|
+
1. Open Timing and go to Reports
|
603
|
+
2. Set the date span you want to import into doing
|
604
|
+
3. Group by Project, Then by None
|
605
|
+
4. Include Tasks with Title, (not as subgroup), Timespan, and Notes
|
606
|
+
5. Uncheck App Usage
|
607
|
+
6. Set File Format to JSON and Duration format to "XX:YY:ZZ"
|
608
|
+
7. Include short entries if desired
|
609
|
+
8. Export the report to a new file
|
610
|
+
|
611
|
+
Now you can run `doing import --type timing -s SECTION PATH`, where SECTION is the name of the section you want to import the entries to (defauts to Currently), and PATH is the path to the JSON file. You can also add a tag (or tags) to all entries, or a custom prefix.
|
612
|
+
|
613
|
+
(`--type timing` is the only option right now, so it doesn't need to be included)
|
614
|
+
|
615
|
+
# Import entries to Projects section and add @timing to all new entries
|
616
|
+
doing import -s Projects --tag=timing "~/Desktop/All Activities.json"
|
617
|
+
|
618
|
+
# Import to default section (Currently) and prefix entries with '[Imported]'
|
619
|
+
doing import --prefix="[Imported]" "~/Desktop/All Activities.json"
|
620
|
+
|
579
621
|
---
|
580
622
|
|
581
623
|
## Extras
|
data/bin/doing
CHANGED
@@ -52,7 +52,7 @@ flag %i[f doing_file]
|
|
52
52
|
|
53
53
|
desc 'Add an entry'
|
54
54
|
arg_name 'ENTRY'
|
55
|
-
command [
|
55
|
+
command %i[now next] do |c|
|
56
56
|
c.desc 'Section'
|
57
57
|
c.arg_name 'NAME'
|
58
58
|
c.flag %i[s section], default_value: wwid.current_section
|
@@ -105,7 +105,8 @@ command [:now, :next] do |c|
|
|
105
105
|
wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
|
106
106
|
wwid.write(wwid.doing_file)
|
107
107
|
elsif $stdin.stat.size.positive?
|
108
|
-
|
108
|
+
input = $stdin.read
|
109
|
+
title, note = wwid.format_input(input)
|
109
110
|
note.push(options[:n]) if options[:n]
|
110
111
|
wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
|
111
112
|
wwid.write(wwid.doing_file)
|
@@ -239,7 +240,7 @@ long_desc %(
|
|
239
240
|
|
240
241
|
Example `doing template HAML > ~/styles/my_doing.haml`
|
241
242
|
)
|
242
|
-
arg_name 'TYPE', must_match: /^(html|haml|css)/i
|
243
|
+
arg_name 'TYPE', must_match: /^(?:html|haml|css)/i
|
243
244
|
command :template do |c|
|
244
245
|
c.action do |_global_options, options, args|
|
245
246
|
raise 'No type specified, use `doing template [HAML|CSS]`' if args.empty?
|
@@ -374,11 +375,12 @@ command %i[done did] do |c|
|
|
374
375
|
finish_date = Time.now
|
375
376
|
end
|
376
377
|
|
377
|
-
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
378
378
|
if finish_date
|
379
379
|
donedate = options[:date] ? "(#{finish_date.strftime('%F %R')})" : ''
|
380
380
|
end
|
381
381
|
|
382
|
+
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
383
|
+
|
382
384
|
if options[:e]
|
383
385
|
raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
|
384
386
|
|
@@ -442,7 +444,7 @@ command :cancel do |c|
|
|
442
444
|
|
443
445
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
444
446
|
c.arg_name 'BOOLEAN'
|
445
|
-
c.flag [:bool], must_match:
|
447
|
+
c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
|
446
448
|
|
447
449
|
c.action do |_global_options, options, args|
|
448
450
|
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
@@ -451,7 +453,16 @@ command :cancel do |c|
|
|
451
453
|
tags = []
|
452
454
|
else
|
453
455
|
tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
|
454
|
-
options[:bool] =
|
456
|
+
options[:bool] = case options[:bool]
|
457
|
+
when /(and|all)/i
|
458
|
+
'AND'
|
459
|
+
when /(any|or)/i
|
460
|
+
'OR'
|
461
|
+
when /(not|none)/i
|
462
|
+
'NOT'
|
463
|
+
else
|
464
|
+
'AND'
|
465
|
+
end
|
455
466
|
end
|
456
467
|
|
457
468
|
raise 'Only one argument allowed' if args.length > 1
|
@@ -499,7 +510,7 @@ command :finish do |c|
|
|
499
510
|
|
500
511
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
501
512
|
c.arg_name 'BOOLEAN'
|
502
|
-
c.flag [:bool], must_match:
|
513
|
+
c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
|
503
514
|
|
504
515
|
c.desc %(Auto-generate finish dates from next entry's start time.
|
505
516
|
Automatically generate completion dates 1 minute before next start date.
|
@@ -536,7 +547,16 @@ command :finish do |c|
|
|
536
547
|
tags = []
|
537
548
|
else
|
538
549
|
tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
|
539
|
-
options[:bool] =
|
550
|
+
options[:bool] = case options[:bool]
|
551
|
+
when /(and|all)/i
|
552
|
+
'AND'
|
553
|
+
when /(any|or)/i
|
554
|
+
'OR'
|
555
|
+
when /(not|none)/i
|
556
|
+
'NOT'
|
557
|
+
else
|
558
|
+
'AND'
|
559
|
+
end
|
540
560
|
end
|
541
561
|
|
542
562
|
raise 'Only one argument allowed' if args.length > 1
|
@@ -581,7 +601,7 @@ command [:again, :resume] do |c|
|
|
581
601
|
|
582
602
|
c.desc 'Boolean used to combine multiple tags'
|
583
603
|
c.arg_name 'BOOLEAN'
|
584
|
-
c.flag [:bool], must_match:
|
604
|
+
c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
|
585
605
|
|
586
606
|
c.desc 'Note'
|
587
607
|
c.arg_name 'TEXT'
|
@@ -589,6 +609,16 @@ command [:again, :resume] do |c|
|
|
589
609
|
|
590
610
|
c.action do |_global_options, options, _args|
|
591
611
|
tags = options[:tag].nil? ? [] : options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }
|
612
|
+
options[:bool] = case options[:bool]
|
613
|
+
when /(and|all)/i
|
614
|
+
'AND'
|
615
|
+
when /(any|or)/i
|
616
|
+
'OR'
|
617
|
+
when /(not|none)/i
|
618
|
+
'NOT'
|
619
|
+
else
|
620
|
+
'AND'
|
621
|
+
end
|
592
622
|
opts = {
|
593
623
|
in: options[:in],
|
594
624
|
note: options[:n],
|
@@ -693,9 +723,9 @@ command :show do |c|
|
|
693
723
|
c.arg_name 'TAG'
|
694
724
|
c.flag [:tag]
|
695
725
|
|
696
|
-
c.desc 'Tag boolean (AND,OR,
|
726
|
+
c.desc 'Tag boolean (AND,OR,NOT)'
|
697
727
|
c.arg_name 'BOOLEAN'
|
698
|
-
c.flag %i[b bool], must_match:
|
728
|
+
c.flag %i[b bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'OR'
|
699
729
|
|
700
730
|
c.desc 'Max count to show'
|
701
731
|
c.arg_name 'MAX'
|
@@ -707,7 +737,7 @@ command :show do |c|
|
|
707
737
|
|
708
738
|
c.desc 'Sort order (asc/desc)'
|
709
739
|
c.arg_name 'ORDER'
|
710
|
-
c.flag %i[s sort], must_match: /^(a|d)/i, default_value: 'ASC'
|
740
|
+
c.flag %i[s sort], must_match: /^(?:a|d)/i, default_value: 'ASC'
|
711
741
|
|
712
742
|
c.desc %(
|
713
743
|
Date range to show, or a single day to filter date on.
|
@@ -727,14 +757,14 @@ command :show do |c|
|
|
727
757
|
default = 'time'
|
728
758
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
729
759
|
c.arg_name 'KEY'
|
730
|
-
c.flag [:tag_sort], must_match: /^(name|time)/i, default_value: default
|
760
|
+
c.flag [:tag_sort], must_match: /^(?:name|time)/i, default_value: default
|
731
761
|
|
732
762
|
c.desc 'Only show items with recorded time intervals'
|
733
763
|
c.switch [:only_timed], default_value: false, negatable: false
|
734
764
|
|
735
765
|
c.desc 'Output to export format (csv|html|json|template|timeline)'
|
736
766
|
c.arg_name 'FORMAT'
|
737
|
-
c.flag %i[o output], must_match: /^(template|html|csv|json|timeline)$/i
|
767
|
+
c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
|
738
768
|
c.action do |_global_options, options, args|
|
739
769
|
tag_filter = false
|
740
770
|
tags = []
|
@@ -756,13 +786,9 @@ command :show do |c|
|
|
756
786
|
end
|
757
787
|
if args.length.positive?
|
758
788
|
args.each do |arg|
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
end
|
763
|
-
else
|
764
|
-
tags.push(arg.strip.sub(/^@/, ''))
|
765
|
-
end
|
789
|
+
arg.split(/,/).each do |tag|
|
790
|
+
tags.push(tag.strip.sub(/^@/, ''))
|
791
|
+
end
|
766
792
|
end
|
767
793
|
end
|
768
794
|
else
|
@@ -770,11 +796,21 @@ command :show do |c|
|
|
770
796
|
end
|
771
797
|
|
772
798
|
tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag]
|
799
|
+
options[:bool] = case options[:bool]
|
800
|
+
when /(and|all)/i
|
801
|
+
'AND'
|
802
|
+
when /(any|or)/i
|
803
|
+
'OR'
|
804
|
+
when /(not|none)/i
|
805
|
+
'NOT'
|
806
|
+
else
|
807
|
+
'AND'
|
808
|
+
end
|
773
809
|
|
774
810
|
unless tags.empty?
|
775
811
|
tag_filter = {
|
776
812
|
'tags' => tags,
|
777
|
-
'bool' => options[:
|
813
|
+
'bool' => options[:bool]
|
778
814
|
}
|
779
815
|
end
|
780
816
|
|
@@ -831,7 +867,7 @@ command [:grep, :search] do |c|
|
|
831
867
|
|
832
868
|
c.desc 'Output to export format (csv|html|json|template|timeline)'
|
833
869
|
c.arg_name 'FORMAT'
|
834
|
-
c.flag %i[o output], must_match: /^(template|html|csv|json|timeline)$/i
|
870
|
+
c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
|
835
871
|
|
836
872
|
c.desc 'Show time intervals on @done tasks'
|
837
873
|
c.switch %i[t times], default_value: true
|
@@ -843,7 +879,7 @@ command [:grep, :search] do |c|
|
|
843
879
|
default = 'time'
|
844
880
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
845
881
|
c.arg_name 'KEY'
|
846
|
-
c.flag [:tag_sort], must_match: /^(name|time)$/i, default_value: default
|
882
|
+
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
847
883
|
|
848
884
|
c.desc 'Only show items with recorded time intervals'
|
849
885
|
c.switch [:only_timed], default_value: false, negatable: false
|
@@ -890,7 +926,7 @@ command :recent do |c|
|
|
890
926
|
default = 'time'
|
891
927
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
892
928
|
c.arg_name 'KEY'
|
893
|
-
c.flag [:tag_sort], must_match: /^(name|time)$/i, default_value: default
|
929
|
+
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
894
930
|
|
895
931
|
c.action do |global_options, options, args|
|
896
932
|
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
@@ -930,11 +966,11 @@ command :today do |c|
|
|
930
966
|
default = 'time'
|
931
967
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
932
968
|
c.arg_name 'KEY'
|
933
|
-
c.flag [:tag_sort], must_match: /^(name|time)$/i, default_value: default
|
969
|
+
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
934
970
|
|
935
971
|
c.desc 'Output to export format (csv|html|json|template|timeline)'
|
936
972
|
c.arg_name 'FORMAT'
|
937
|
-
c.flag %i[o output], must_match: /^(template|html|csv|json|timeline)$/i
|
973
|
+
c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
|
938
974
|
|
939
975
|
c.action do |_global_options, options, _args|
|
940
976
|
options[:t] = true if options[:totals]
|
@@ -965,11 +1001,11 @@ command :on do |c|
|
|
965
1001
|
default = 'time'
|
966
1002
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
967
1003
|
c.arg_name 'KEY'
|
968
|
-
c.flag [:tag_sort], must_match: /^(name|time)$/i, default_value: default
|
1004
|
+
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
969
1005
|
|
970
1006
|
c.desc 'Output to export format (csv|html|json|template|timeline)'
|
971
1007
|
c.arg_name 'FORMAT'
|
972
|
-
c.flag %i[o output], must_match: /^(template|html|csv|json|timeline)$/i
|
1008
|
+
c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
|
973
1009
|
|
974
1010
|
c.action do |_global_options, options, args|
|
975
1011
|
exit_now! 'Missing date argument' if args.empty?
|
@@ -1007,7 +1043,7 @@ command :yesterday do |c|
|
|
1007
1043
|
|
1008
1044
|
c.desc 'Output to export format (csv|html|json|template|timeline)'
|
1009
1045
|
c.arg_name 'FORMAT'
|
1010
|
-
c.flag %i[o output], must_match: /^(template|html|csv|json|timeline)$/i
|
1046
|
+
c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
|
1011
1047
|
|
1012
1048
|
c.desc 'Show time intervals on @done tasks'
|
1013
1049
|
c.switch %i[t times], default_value: true
|
@@ -1019,7 +1055,7 @@ command :yesterday do |c|
|
|
1019
1055
|
default = 'time'
|
1020
1056
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
1021
1057
|
c.arg_name 'KEY'
|
1022
|
-
c.flag [:tag_sort], must_match: /^(name|time)$/i, default_value: default
|
1058
|
+
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1023
1059
|
|
1024
1060
|
c.action do |_global_options, options, _args|
|
1025
1061
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
@@ -1043,7 +1079,7 @@ command :last do |c|
|
|
1043
1079
|
|
1044
1080
|
c.desc 'Tag boolean'
|
1045
1081
|
c.arg_name 'BOOLEAN'
|
1046
|
-
c.flag [:bool], must_match: /(and|or|not)/i, default_value: 'AND'
|
1082
|
+
c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
|
1047
1083
|
|
1048
1084
|
c.desc 'Search filter, surround with slashes for regex (/query/)'
|
1049
1085
|
c.arg_name 'QUERY'
|
@@ -1056,7 +1092,15 @@ command :last do |c|
|
|
1056
1092
|
tags = []
|
1057
1093
|
else
|
1058
1094
|
tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
|
1059
|
-
options[:bool] =
|
1095
|
+
options[:bool] = case options[:bool]
|
1096
|
+
when /(any|or)/i
|
1097
|
+
:or
|
1098
|
+
when /(not|none)/i
|
1099
|
+
:not
|
1100
|
+
else
|
1101
|
+
:and
|
1102
|
+
end
|
1103
|
+
|
1060
1104
|
end
|
1061
1105
|
|
1062
1106
|
if options[:e]
|
@@ -1129,7 +1173,7 @@ command :view do |c|
|
|
1129
1173
|
|
1130
1174
|
c.desc 'Output to export format (csv|html|json|template|timeline)'
|
1131
1175
|
c.arg_name 'FORMAT'
|
1132
|
-
c.flag %i[o output], must_match: /^(template|html|csv|json|timeline)$/i
|
1176
|
+
c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
|
1133
1177
|
|
1134
1178
|
c.desc 'Show time intervals on @done tasks'
|
1135
1179
|
c.switch %i[t times], default_value: true
|
@@ -1144,7 +1188,7 @@ command :view do |c|
|
|
1144
1188
|
default = 'time'
|
1145
1189
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
1146
1190
|
c.arg_name 'KEY'
|
1147
|
-
c.flag [:tag_sort], must_match: /^(name|time)$/i, default_value: default
|
1191
|
+
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1148
1192
|
|
1149
1193
|
c.desc 'Only show items with recorded time intervals'
|
1150
1194
|
c.switch [:only_timed], default_value: false, negatable: true
|
@@ -1177,7 +1221,7 @@ command :view do |c|
|
|
1177
1221
|
else
|
1178
1222
|
view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
|
1179
1223
|
end
|
1180
|
-
tag_filter['bool'] = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].
|
1224
|
+
tag_filter['bool'] = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :or
|
1181
1225
|
end
|
1182
1226
|
|
1183
1227
|
# If the -o/--output flag was specified, override any default in the view template
|
@@ -1256,7 +1300,7 @@ command :archive do |c|
|
|
1256
1300
|
|
1257
1301
|
c.desc 'Tag boolean (AND|OR|NOT)'
|
1258
1302
|
c.arg_name 'BOOLEAN'
|
1259
|
-
c.flag [:bool], must_match: /(and|or|not)/i, default_value: 'AND'
|
1303
|
+
c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
|
1260
1304
|
|
1261
1305
|
c.desc 'Search filter'
|
1262
1306
|
c.arg_name 'QUERY'
|
@@ -1280,6 +1324,16 @@ command :archive do |c|
|
|
1280
1324
|
|
1281
1325
|
tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag]
|
1282
1326
|
|
1327
|
+
options[:bool] = case options[:bool]
|
1328
|
+
when /(and|all)/i
|
1329
|
+
'AND'
|
1330
|
+
when /(any|or)/i
|
1331
|
+
'OR'
|
1332
|
+
when /(not|none)/i
|
1333
|
+
'NOT'
|
1334
|
+
else
|
1335
|
+
'AND'
|
1336
|
+
end
|
1283
1337
|
opts = {
|
1284
1338
|
bool: options[:bool],
|
1285
1339
|
destination: options[:to],
|
@@ -1390,6 +1444,44 @@ command :undo do |c|
|
|
1390
1444
|
end
|
1391
1445
|
end
|
1392
1446
|
|
1447
|
+
desc 'Import entries from an external source'
|
1448
|
+
long_desc 'Imports entries from other sources. Currently only handles JSON reports exported from Timing.app.'
|
1449
|
+
arg_name 'PATH'
|
1450
|
+
command :import do |c|
|
1451
|
+
c.desc 'Import type'
|
1452
|
+
c.arg_name 'TYPE'
|
1453
|
+
c.flag :type, default_value: 'timing'
|
1454
|
+
|
1455
|
+
c.desc 'Target section'
|
1456
|
+
c.arg_name 'NAME'
|
1457
|
+
c.flag %i[s section], default_value: wwid.current_section
|
1458
|
+
|
1459
|
+
c.desc 'Tag all imported entries'
|
1460
|
+
c.arg_name 'TAGS'
|
1461
|
+
c.flag :tag
|
1462
|
+
|
1463
|
+
c.desc 'Prefix entries with'
|
1464
|
+
c.arg_name 'PREFIX'
|
1465
|
+
c.flag :prefix
|
1466
|
+
|
1467
|
+
c.desc 'Allow entries that overlap existing times'
|
1468
|
+
c.switch [:overlap], negatable: true
|
1469
|
+
|
1470
|
+
c.action do |_global_options, options, args|
|
1471
|
+
|
1472
|
+
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
1473
|
+
|
1474
|
+
if options[:type] =~ /^tim/i
|
1475
|
+
args.each do |path|
|
1476
|
+
wwid.import_timing(path, { section: section, tag: options[:tag], prefix: options[:prefix], no_overlap: !options[:overlap] })
|
1477
|
+
wwid.write(wwid.doing_file)
|
1478
|
+
end
|
1479
|
+
else
|
1480
|
+
raise 'Invalid import type'
|
1481
|
+
end
|
1482
|
+
end
|
1483
|
+
end
|
1484
|
+
|
1393
1485
|
pre do |global, _command, _options, _args|
|
1394
1486
|
if global[:config_file] && global[:config_file] != wwid.config_file
|
1395
1487
|
wwid.config_file = global[:config_file]
|
data/lib/doing.rb
CHANGED
data/lib/doing/version.rb
CHANGED
data/lib/doing/wwid.rb
CHANGED
@@ -4,92 +4,6 @@ require 'deep_merge'
|
|
4
4
|
require 'open3'
|
5
5
|
require 'pp'
|
6
6
|
|
7
|
-
##
|
8
|
-
## @brief Hash helpers
|
9
|
-
##
|
10
|
-
class Hash
|
11
|
-
def has_tags?(tags, bool = 'AND')
|
12
|
-
tags = tags.split(/ *, */) if tags.is_a? String
|
13
|
-
item = self
|
14
|
-
case bool
|
15
|
-
when 'AND'
|
16
|
-
result = true
|
17
|
-
tags.each do |tag|
|
18
|
-
unless item['title'] =~ /@#{tag}/
|
19
|
-
result = false
|
20
|
-
break
|
21
|
-
end
|
22
|
-
end
|
23
|
-
result
|
24
|
-
when 'NOT'
|
25
|
-
result = true
|
26
|
-
tags.each do |tag|
|
27
|
-
if item['title'] =~ /@#{tag}/
|
28
|
-
result = false
|
29
|
-
break
|
30
|
-
end
|
31
|
-
end
|
32
|
-
result
|
33
|
-
else
|
34
|
-
result = false
|
35
|
-
tags.each do |tag|
|
36
|
-
if item['title'] =~ /@#{tag}/
|
37
|
-
result = true
|
38
|
-
break
|
39
|
-
end
|
40
|
-
end
|
41
|
-
result
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def matches_search?(search)
|
46
|
-
item = self
|
47
|
-
text = item['note'] ? item['title'] + item['note'].join(' ') : item['title']
|
48
|
-
pattern = if search.strip =~ %r{^/.*?/$}
|
49
|
-
search.sub(%r{/(.*?)/}, '\1')
|
50
|
-
else
|
51
|
-
search.split('').join('.{0,3}')
|
52
|
-
end
|
53
|
-
text =~ /#{pattern}/i ? true : false
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
##
|
58
|
-
## @brief String helpers
|
59
|
-
##
|
60
|
-
class String
|
61
|
-
def cap_first
|
62
|
-
sub(/^\w/) do |m|
|
63
|
-
m.upcase
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
##
|
68
|
-
## @brief Turn raw urls into HTML links
|
69
|
-
##
|
70
|
-
## @param opt (Hash) Additional Options
|
71
|
-
##
|
72
|
-
def link_urls(opt = {})
|
73
|
-
opt[:format] ||= :html
|
74
|
-
if opt[:format] == :html
|
75
|
-
gsub(%r{(?mi)((http|https)://)?([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&:/~+#]*[\w\-@^=%&/~+#])?}) do |_match|
|
76
|
-
m = Regexp.last_match
|
77
|
-
proto = m[1].nil? ? 'http://' : ''
|
78
|
-
%(<a href="#{proto}#{m[0]}" title="Link to #{m[0]}">[#{m[3]}]</a>)
|
79
|
-
end.gsub(/<(\w+:.*?)>/) do |match|
|
80
|
-
m = Regexp.last_match
|
81
|
-
if m[1] =~ /<a href/
|
82
|
-
match
|
83
|
-
else
|
84
|
-
%(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
else
|
88
|
-
self
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
7
|
##
|
94
8
|
## @brief Main "What Was I Doing" methods
|
95
9
|
##
|
@@ -310,7 +224,7 @@ class WWID
|
|
310
224
|
else
|
311
225
|
@content[section]['items'][current - 1]['note'] = [] unless @content[section]['items'][current - 1].key? 'note'
|
312
226
|
|
313
|
-
@content[section]['items'][current - 1]['note'].push(line.
|
227
|
+
@content[section]['items'][current - 1]['note'].push(line.chomp)
|
314
228
|
# end
|
315
229
|
end
|
316
230
|
end
|
@@ -667,6 +581,96 @@ class WWID
|
|
667
581
|
@results.push(%(Added "#{entry['title']}" to #{section}))
|
668
582
|
end
|
669
583
|
|
584
|
+
def same_time?(item_a, item_b)
|
585
|
+
item_a['date'] == item_b['date'] ? get_interval(item_a, false) == get_interval(item_b, false) : false
|
586
|
+
end
|
587
|
+
|
588
|
+
def overlapping_time?(item_a, item_b)
|
589
|
+
return true if same_time?(item_a, item_b)
|
590
|
+
|
591
|
+
start_a = item_a['date']
|
592
|
+
interval = get_interval(item_a, false)
|
593
|
+
end_a = interval ? start_a + interval.to_i : start_a
|
594
|
+
start_b = item_b['date']
|
595
|
+
interval = get_interval(item_b, false)
|
596
|
+
end_b = interval ? start_b + interval.to_i : start_b
|
597
|
+
(start_a >= start_b && start_a <= end_b) || (end_a >= start_b && end_a <= end_b) || (start_a < start_b && end_a > end_b)
|
598
|
+
end
|
599
|
+
|
600
|
+
def dedup(items, no_overlap = false)
|
601
|
+
|
602
|
+
combined = []
|
603
|
+
@content.each do |_k, v|
|
604
|
+
combined += v['items']
|
605
|
+
end
|
606
|
+
|
607
|
+
items.delete_if do |item|
|
608
|
+
duped = false
|
609
|
+
combined.each do |comp|
|
610
|
+
duped = no_overlap ? overlapping_time?(item, comp) : same_time?(item, comp)
|
611
|
+
break if duped
|
612
|
+
end
|
613
|
+
# warn "Skipping overlapping entry: #{item['title']}" if duped
|
614
|
+
duped
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
##
|
619
|
+
## @brief Imports a Timing report
|
620
|
+
##
|
621
|
+
## @param path (String) Path to JSON report file
|
622
|
+
## @param section (String) The section to add to
|
623
|
+
## @param opt (Hash) Additional Options
|
624
|
+
##
|
625
|
+
def import_timing(path, opt = {})
|
626
|
+
section = opt[:section] || @current_section
|
627
|
+
opt[:no_overlap] ||= false
|
628
|
+
|
629
|
+
add_section(section) unless @content.has_key?(section)
|
630
|
+
|
631
|
+
add_tags = opt[:tag] ? opt[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '@') }.join(' ') : ''
|
632
|
+
prefix = opt[:prefix] ? opt[:prefix] : '[Timing.app]'
|
633
|
+
raise "File not found" unless File.exist?(File.expand_path(path))
|
634
|
+
|
635
|
+
data = JSON.parse(IO.read(File.expand_path(path)))
|
636
|
+
new_items = []
|
637
|
+
data.each do |entry|
|
638
|
+
# Only process task entries
|
639
|
+
next if entry.key?('activityType') && entry['activityType'] != 'Task'
|
640
|
+
# Only process entries with a start and end date
|
641
|
+
next unless entry.key?('startDate') && entry.key?('endDate')
|
642
|
+
|
643
|
+
# Round down seconds and convert UTC to local time
|
644
|
+
start_time = Time.parse(entry['startDate'].sub(/:\d\dZ$/, ':00Z')).getlocal
|
645
|
+
end_time = Time.parse(entry['endDate'].sub(/:\d\dZ$/, ':00Z')).getlocal
|
646
|
+
next unless start_time && end_time
|
647
|
+
|
648
|
+
tags = entry['project'].split(/ ▸ /).map {|proj| proj.gsub(/[^a-z0-9]+/i, '').downcase }
|
649
|
+
title = "#{prefix} "
|
650
|
+
title += entry.key?('activityTitle') && entry['activityTitle'] != '(Untitled Task)' ? entry['activityTitle'] : 'Working on'
|
651
|
+
tags.each do |tag|
|
652
|
+
if title =~ /\b#{tag}\b/i
|
653
|
+
title.sub!(/\b#{tag}\b/i, "@#{tag}")
|
654
|
+
else
|
655
|
+
title += " @#{tag}"
|
656
|
+
end
|
657
|
+
end
|
658
|
+
title = autotag(title) if @auto_tag
|
659
|
+
title += " @done(#{end_time.strftime('%Y-%m-%d %H:%M')})"
|
660
|
+
title.gsub!(/ +/, ' ')
|
661
|
+
title.strip!
|
662
|
+
new_entry = { 'title' => title, 'date' => start_time, 'section' => section }
|
663
|
+
new_entry['note'] = entry['notes'].split(/\n/).map(&:chomp) if entry.key?('notes')
|
664
|
+
new_items.push(new_entry)
|
665
|
+
end
|
666
|
+
total = new_items.count
|
667
|
+
new_items = dedup(new_items, opt[:no_overlap])
|
668
|
+
dups = total - new_items.count
|
669
|
+
@results.push(%(Skipped #{dups} items with overlapping times)) if dups > 0
|
670
|
+
@content[section]['items'].concat(new_items)
|
671
|
+
@results.push(%(Imported #{new_items.count} items to #{section}))
|
672
|
+
end
|
673
|
+
|
670
674
|
##
|
671
675
|
## @brief Return the content of the last note for a given section
|
672
676
|
##
|
@@ -701,7 +705,7 @@ class WWID
|
|
701
705
|
opt[:section] ||= 'all'
|
702
706
|
opt[:note] ||= []
|
703
707
|
opt[:tag] ||= []
|
704
|
-
opt[:tag_bool] ||=
|
708
|
+
opt[:tag_bool] ||= :and
|
705
709
|
|
706
710
|
last = last_entry(opt)
|
707
711
|
if last.nil?
|
@@ -727,7 +731,7 @@ class WWID
|
|
727
731
|
## @param opt (Hash) Additional Options
|
728
732
|
##
|
729
733
|
def last_entry(opt = {})
|
730
|
-
opt[:tag_bool] ||=
|
734
|
+
opt[:tag_bool] ||= :and
|
731
735
|
opt[:section] ||= @current_section
|
732
736
|
|
733
737
|
sec_arr = []
|
@@ -835,7 +839,12 @@ class WWID
|
|
835
839
|
done_date = item['date'] + (opt[:back] - item['date'])
|
836
840
|
end
|
837
841
|
elsif opt[:took]
|
838
|
-
|
842
|
+
if item['date'] + opt[:took] > Time.now
|
843
|
+
item['date'] = Time.now - opt[:took]
|
844
|
+
done_date = Time.now
|
845
|
+
else
|
846
|
+
done_date = item['date'] + opt[:took]
|
847
|
+
end
|
839
848
|
else
|
840
849
|
done_date = Time.now
|
841
850
|
end
|
@@ -1213,7 +1222,7 @@ class WWID
|
|
1213
1222
|
@content.each do |_k, v|
|
1214
1223
|
combined['items'] += v['items']
|
1215
1224
|
end
|
1216
|
-
section = if opt[:tag_filter] && opt[:tag_filter]['bool'] !=
|
1225
|
+
section = if opt[:tag_filter] && opt[:tag_filter]['bool'].normalize_bool != :not
|
1217
1226
|
opt[:tag_filter]['tags'].map do |tag|
|
1218
1227
|
"@#{tag}"
|
1219
1228
|
end.join(' + ')
|
@@ -1244,31 +1253,7 @@ class WWID
|
|
1244
1253
|
end
|
1245
1254
|
|
1246
1255
|
if opt[:tag_filter] && !opt[:tag_filter]['tags'].empty?
|
1247
|
-
items.
|
1248
|
-
case opt[:tag_filter]['bool']
|
1249
|
-
when /(AND|ALL)/
|
1250
|
-
del = false
|
1251
|
-
opt[:tag_filter]['tags'].each do |tag|
|
1252
|
-
unless item['title'] =~ /@#{tag}/
|
1253
|
-
del = true
|
1254
|
-
break
|
1255
|
-
end
|
1256
|
-
end
|
1257
|
-
del
|
1258
|
-
when /NONE/
|
1259
|
-
del = false
|
1260
|
-
opt[:tag_filter]['tags'].each do |tag|
|
1261
|
-
del = true if item['title'] =~ /@#{tag}/
|
1262
|
-
end
|
1263
|
-
del
|
1264
|
-
when /(OR|ANY)/
|
1265
|
-
del = true
|
1266
|
-
opt[:tag_filter]['tags'].each do |tag|
|
1267
|
-
del = false if item['title'] =~ /@#{tag}/
|
1268
|
-
end
|
1269
|
-
del
|
1270
|
-
end
|
1271
|
-
end
|
1256
|
+
items.select! { |item| item.has_tags?(opt[:tag_filter]['tags'], opt[:tag_filter]['bool']) }
|
1272
1257
|
end
|
1273
1258
|
|
1274
1259
|
if opt[:search]
|
@@ -1560,7 +1545,7 @@ class WWID
|
|
1560
1545
|
count = options[:keep] || 0
|
1561
1546
|
destination = options[:destination] || 'Archive'
|
1562
1547
|
tags = options[:tags] || []
|
1563
|
-
bool = options[:bool] ||
|
1548
|
+
bool = options[:bool] || :and
|
1564
1549
|
|
1565
1550
|
section = choose_section if section.nil? || section =~ /choose/i
|
1566
1551
|
archive_all = section =~ /^all$/i # && !(tags.nil? || tags.empty?)
|
@@ -1588,7 +1573,7 @@ class WWID
|
|
1588
1573
|
def do_archive(sect, destination, opt = {})
|
1589
1574
|
count = opt[:count] || 0
|
1590
1575
|
tags = opt[:tags] || []
|
1591
|
-
bool = opt[:bool] ||
|
1576
|
+
bool = opt[:bool] || :and
|
1592
1577
|
label = opt[:label] || true
|
1593
1578
|
|
1594
1579
|
if sect =~ /^all$/i
|
@@ -1601,7 +1586,7 @@ class WWID
|
|
1601
1586
|
counter = 0
|
1602
1587
|
|
1603
1588
|
all_sections.each do |section|
|
1604
|
-
items = @content[section]['items']
|
1589
|
+
items = @content[section]['items'].dup
|
1605
1590
|
|
1606
1591
|
moved_items = []
|
1607
1592
|
if !tags.empty? || opt[:search]
|
@@ -1625,13 +1610,7 @@ class WWID
|
|
1625
1610
|
@content[destination]['items'].concat(moved_items)
|
1626
1611
|
@results.push("Archived #{moved_items.length} items from #{section} to #{destination}")
|
1627
1612
|
else
|
1628
|
-
count = items.length if
|
1629
|
-
|
1630
|
-
@content[section]['items'] = if count.zero?
|
1631
|
-
[]
|
1632
|
-
else
|
1633
|
-
items[0..count - 1]
|
1634
|
-
end
|
1613
|
+
count = items.length if items.length < count
|
1635
1614
|
|
1636
1615
|
items.map! do |item|
|
1637
1616
|
if label && section != 'Currently'
|
@@ -1640,12 +1619,19 @@ class WWID
|
|
1640
1619
|
end
|
1641
1620
|
item
|
1642
1621
|
end
|
1622
|
+
|
1643
1623
|
if items.count > count
|
1644
1624
|
@content[destination]['items'].concat(items[count..-1])
|
1645
1625
|
else
|
1646
1626
|
@content[destination]['items'].concat(items)
|
1647
1627
|
end
|
1648
1628
|
|
1629
|
+
@content[section]['items'] = if count.zero?
|
1630
|
+
[]
|
1631
|
+
else
|
1632
|
+
items[0..count - 1]
|
1633
|
+
end
|
1634
|
+
|
1649
1635
|
@results.push("Archived #{items.length - count} items from #{section} to #{destination}")
|
1650
1636
|
end
|
1651
1637
|
end
|
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.66
|
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-07-
|
11
|
+
date: 2021-07-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|