doing 1.0.63 → 1.0.67
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +24 -1
- data/bin/doing +127 -36
- data/lib/doing.rb +1 -0
- data/lib/doing/helpers.rb +120 -0
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +106 -125
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2adffea75d109f047bd36b1ce55c48b83bc61a5d05d2060ece89fc8041faaabd
|
4
|
+
data.tar.gz: 546e1c04bf9f0b319905e21a0aadb4ce9afb2eee8c49b7e8bc551d589d4f75d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34db37e7c5974f97bad9516970bccb8d1a3b28bd0eefec7fbbf57628137cd763fc5f5473c85cee05334088f64d8a7f8621d1bd6f41fc694ad4534569e3360313
|
7
|
+
data.tar.gz: 510d4f38fbe89ba480aedf9f8845c99f33c7265c1f852be76151f75aed6e10fee49e9912e1b1161f6ebbd5b7b98889663e289136fe9e63828b71544c0b1fcd7c
|
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.66<!--END VER-->.
|
33
33
|
|
34
34
|
$ [sudo] gem install doing
|
35
35
|
|
@@ -595,6 +595,29 @@ Example: Archive all Currently items for _@client_ that are marked _@done_
|
|
595
595
|
|
596
596
|
doing archive @client @done
|
597
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
|
+
|
598
621
|
---
|
599
622
|
|
600
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?
|
@@ -443,7 +444,7 @@ command :cancel do |c|
|
|
443
444
|
|
444
445
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
445
446
|
c.arg_name 'BOOLEAN'
|
446
|
-
c.flag [:bool], must_match:
|
447
|
+
c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
|
447
448
|
|
448
449
|
c.action do |_global_options, options, args|
|
449
450
|
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
@@ -452,7 +453,16 @@ command :cancel do |c|
|
|
452
453
|
tags = []
|
453
454
|
else
|
454
455
|
tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
|
455
|
-
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
|
456
466
|
end
|
457
467
|
|
458
468
|
raise 'Only one argument allowed' if args.length > 1
|
@@ -500,7 +510,7 @@ command :finish do |c|
|
|
500
510
|
|
501
511
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
502
512
|
c.arg_name 'BOOLEAN'
|
503
|
-
c.flag [:bool], must_match:
|
513
|
+
c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
|
504
514
|
|
505
515
|
c.desc %(Auto-generate finish dates from next entry's start time.
|
506
516
|
Automatically generate completion dates 1 minute before next start date.
|
@@ -537,7 +547,16 @@ command :finish do |c|
|
|
537
547
|
tags = []
|
538
548
|
else
|
539
549
|
tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
|
540
|
-
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
|
541
560
|
end
|
542
561
|
|
543
562
|
raise 'Only one argument allowed' if args.length > 1
|
@@ -582,7 +601,7 @@ command [:again, :resume] do |c|
|
|
582
601
|
|
583
602
|
c.desc 'Boolean used to combine multiple tags'
|
584
603
|
c.arg_name 'BOOLEAN'
|
585
|
-
c.flag [:bool], must_match:
|
604
|
+
c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
|
586
605
|
|
587
606
|
c.desc 'Note'
|
588
607
|
c.arg_name 'TEXT'
|
@@ -590,6 +609,16 @@ command [:again, :resume] do |c|
|
|
590
609
|
|
591
610
|
c.action do |_global_options, options, _args|
|
592
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
|
593
622
|
opts = {
|
594
623
|
in: options[:in],
|
595
624
|
note: options[:n],
|
@@ -694,9 +723,9 @@ command :show do |c|
|
|
694
723
|
c.arg_name 'TAG'
|
695
724
|
c.flag [:tag]
|
696
725
|
|
697
|
-
c.desc 'Tag boolean (AND,OR,
|
726
|
+
c.desc 'Tag boolean (AND,OR,NOT)'
|
698
727
|
c.arg_name 'BOOLEAN'
|
699
|
-
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'
|
700
729
|
|
701
730
|
c.desc 'Max count to show'
|
702
731
|
c.arg_name 'MAX'
|
@@ -708,7 +737,7 @@ command :show do |c|
|
|
708
737
|
|
709
738
|
c.desc 'Sort order (asc/desc)'
|
710
739
|
c.arg_name 'ORDER'
|
711
|
-
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'
|
712
741
|
|
713
742
|
c.desc %(
|
714
743
|
Date range to show, or a single day to filter date on.
|
@@ -728,14 +757,14 @@ command :show do |c|
|
|
728
757
|
default = 'time'
|
729
758
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
730
759
|
c.arg_name 'KEY'
|
731
|
-
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
|
732
761
|
|
733
762
|
c.desc 'Only show items with recorded time intervals'
|
734
763
|
c.switch [:only_timed], default_value: false, negatable: false
|
735
764
|
|
736
765
|
c.desc 'Output to export format (csv|html|json|template|timeline)'
|
737
766
|
c.arg_name 'FORMAT'
|
738
|
-
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
|
739
768
|
c.action do |_global_options, options, args|
|
740
769
|
tag_filter = false
|
741
770
|
tags = []
|
@@ -757,13 +786,9 @@ command :show do |c|
|
|
757
786
|
end
|
758
787
|
if args.length.positive?
|
759
788
|
args.each do |arg|
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
end
|
764
|
-
else
|
765
|
-
tags.push(arg.strip.sub(/^@/, ''))
|
766
|
-
end
|
789
|
+
arg.split(/,/).each do |tag|
|
790
|
+
tags.push(tag.strip.sub(/^@/, ''))
|
791
|
+
end
|
767
792
|
end
|
768
793
|
end
|
769
794
|
else
|
@@ -771,11 +796,21 @@ command :show do |c|
|
|
771
796
|
end
|
772
797
|
|
773
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
|
774
809
|
|
775
810
|
unless tags.empty?
|
776
811
|
tag_filter = {
|
777
812
|
'tags' => tags,
|
778
|
-
'bool' => options[:
|
813
|
+
'bool' => options[:bool]
|
779
814
|
}
|
780
815
|
end
|
781
816
|
|
@@ -832,7 +867,7 @@ command [:grep, :search] do |c|
|
|
832
867
|
|
833
868
|
c.desc 'Output to export format (csv|html|json|template|timeline)'
|
834
869
|
c.arg_name 'FORMAT'
|
835
|
-
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
|
836
871
|
|
837
872
|
c.desc 'Show time intervals on @done tasks'
|
838
873
|
c.switch %i[t times], default_value: true
|
@@ -844,7 +879,7 @@ command [:grep, :search] do |c|
|
|
844
879
|
default = 'time'
|
845
880
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
846
881
|
c.arg_name 'KEY'
|
847
|
-
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
|
848
883
|
|
849
884
|
c.desc 'Only show items with recorded time intervals'
|
850
885
|
c.switch [:only_timed], default_value: false, negatable: false
|
@@ -891,7 +926,7 @@ command :recent do |c|
|
|
891
926
|
default = 'time'
|
892
927
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
893
928
|
c.arg_name 'KEY'
|
894
|
-
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
|
895
930
|
|
896
931
|
c.action do |global_options, options, args|
|
897
932
|
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
@@ -931,11 +966,11 @@ command :today do |c|
|
|
931
966
|
default = 'time'
|
932
967
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
933
968
|
c.arg_name 'KEY'
|
934
|
-
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
|
935
970
|
|
936
971
|
c.desc 'Output to export format (csv|html|json|template|timeline)'
|
937
972
|
c.arg_name 'FORMAT'
|
938
|
-
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
|
939
974
|
|
940
975
|
c.action do |_global_options, options, _args|
|
941
976
|
options[:t] = true if options[:totals]
|
@@ -966,11 +1001,11 @@ command :on do |c|
|
|
966
1001
|
default = 'time'
|
967
1002
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
968
1003
|
c.arg_name 'KEY'
|
969
|
-
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
|
970
1005
|
|
971
1006
|
c.desc 'Output to export format (csv|html|json|template|timeline)'
|
972
1007
|
c.arg_name 'FORMAT'
|
973
|
-
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
|
974
1009
|
|
975
1010
|
c.action do |_global_options, options, args|
|
976
1011
|
exit_now! 'Missing date argument' if args.empty?
|
@@ -1008,7 +1043,7 @@ command :yesterday do |c|
|
|
1008
1043
|
|
1009
1044
|
c.desc 'Output to export format (csv|html|json|template|timeline)'
|
1010
1045
|
c.arg_name 'FORMAT'
|
1011
|
-
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
|
1012
1047
|
|
1013
1048
|
c.desc 'Show time intervals on @done tasks'
|
1014
1049
|
c.switch %i[t times], default_value: true
|
@@ -1020,7 +1055,7 @@ command :yesterday do |c|
|
|
1020
1055
|
default = 'time'
|
1021
1056
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
1022
1057
|
c.arg_name 'KEY'
|
1023
|
-
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
|
1024
1059
|
|
1025
1060
|
c.action do |_global_options, options, _args|
|
1026
1061
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
@@ -1044,7 +1079,7 @@ command :last do |c|
|
|
1044
1079
|
|
1045
1080
|
c.desc 'Tag boolean'
|
1046
1081
|
c.arg_name 'BOOLEAN'
|
1047
|
-
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'
|
1048
1083
|
|
1049
1084
|
c.desc 'Search filter, surround with slashes for regex (/query/)'
|
1050
1085
|
c.arg_name 'QUERY'
|
@@ -1057,7 +1092,15 @@ command :last do |c|
|
|
1057
1092
|
tags = []
|
1058
1093
|
else
|
1059
1094
|
tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
|
1060
|
-
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
|
+
|
1061
1104
|
end
|
1062
1105
|
|
1063
1106
|
if options[:e]
|
@@ -1130,7 +1173,7 @@ command :view do |c|
|
|
1130
1173
|
|
1131
1174
|
c.desc 'Output to export format (csv|html|json|template|timeline)'
|
1132
1175
|
c.arg_name 'FORMAT'
|
1133
|
-
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
|
1134
1177
|
|
1135
1178
|
c.desc 'Show time intervals on @done tasks'
|
1136
1179
|
c.switch %i[t times], default_value: true
|
@@ -1145,7 +1188,7 @@ command :view do |c|
|
|
1145
1188
|
default = 'time'
|
1146
1189
|
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
1147
1190
|
c.arg_name 'KEY'
|
1148
|
-
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
|
1149
1192
|
|
1150
1193
|
c.desc 'Only show items with recorded time intervals'
|
1151
1194
|
c.switch [:only_timed], default_value: false, negatable: true
|
@@ -1178,7 +1221,7 @@ command :view do |c|
|
|
1178
1221
|
else
|
1179
1222
|
view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
|
1180
1223
|
end
|
1181
|
-
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
|
1182
1225
|
end
|
1183
1226
|
|
1184
1227
|
# If the -o/--output flag was specified, override any default in the view template
|
@@ -1257,7 +1300,7 @@ command :archive do |c|
|
|
1257
1300
|
|
1258
1301
|
c.desc 'Tag boolean (AND|OR|NOT)'
|
1259
1302
|
c.arg_name 'BOOLEAN'
|
1260
|
-
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'
|
1261
1304
|
|
1262
1305
|
c.desc 'Search filter'
|
1263
1306
|
c.arg_name 'QUERY'
|
@@ -1281,6 +1324,16 @@ command :archive do |c|
|
|
1281
1324
|
|
1282
1325
|
tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag]
|
1283
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
|
1284
1337
|
opts = {
|
1285
1338
|
bool: options[:bool],
|
1286
1339
|
destination: options[:to],
|
@@ -1391,6 +1444,44 @@ command :undo do |c|
|
|
1391
1444
|
end
|
1392
1445
|
end
|
1393
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
|
+
|
1394
1485
|
pre do |global, _command, _options, _args|
|
1395
1486
|
if global[:config_file] && global[:config_file] != wwid.config_file
|
1396
1487
|
wwid.config_file = global[:config_file]
|
data/lib/doing.rb
CHANGED
@@ -0,0 +1,120 @@
|
|
1
|
+
##
|
2
|
+
## @brief Hash helpers
|
3
|
+
##
|
4
|
+
class ::Hash
|
5
|
+
def has_tags?(tags, bool = :and)
|
6
|
+
tags = tags.split(/ *, */) if tags.is_a? String
|
7
|
+
bool = bool.normalize_bool if bool.is_a? String
|
8
|
+
item = self
|
9
|
+
case bool
|
10
|
+
when :and
|
11
|
+
result = true
|
12
|
+
tags.each do |tag|
|
13
|
+
unless item['title'] =~ /@#{tag}/
|
14
|
+
result = false
|
15
|
+
break
|
16
|
+
end
|
17
|
+
end
|
18
|
+
result
|
19
|
+
when :not
|
20
|
+
result = true
|
21
|
+
tags.each do |tag|
|
22
|
+
if item['title'] =~ /@#{tag}/
|
23
|
+
result = false
|
24
|
+
break
|
25
|
+
end
|
26
|
+
end
|
27
|
+
result
|
28
|
+
else
|
29
|
+
result = false
|
30
|
+
tags.each do |tag|
|
31
|
+
if item['title'] =~ /@#{tag}/
|
32
|
+
result = true
|
33
|
+
break
|
34
|
+
end
|
35
|
+
end
|
36
|
+
result
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def matches_search?(search)
|
41
|
+
item = self
|
42
|
+
text = item['note'] ? item['title'] + item['note'].join(' ') : item['title']
|
43
|
+
pattern = if search.strip =~ %r{^/.*?/$}
|
44
|
+
search.sub(%r{/(.*?)/}, '\1')
|
45
|
+
else
|
46
|
+
search.split('').join('.{0,3}')
|
47
|
+
end
|
48
|
+
text =~ /#{pattern}/i ? true : false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
## @brief String helpers
|
54
|
+
##
|
55
|
+
class ::String
|
56
|
+
def cap_first
|
57
|
+
sub(/^\w/) do |m|
|
58
|
+
m.upcase
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
##
|
64
|
+
## @brief Convert a boolean string to a symbol
|
65
|
+
##
|
66
|
+
## @return Symbol :and, :or, or :not
|
67
|
+
##
|
68
|
+
def normalize_bool!
|
69
|
+
replace normalize_bool
|
70
|
+
end
|
71
|
+
|
72
|
+
def normalize_bool(default = :and)
|
73
|
+
case self
|
74
|
+
when /(and|all)/i
|
75
|
+
:and
|
76
|
+
when /(any|or)/i
|
77
|
+
:or
|
78
|
+
when /(not|none)/i
|
79
|
+
:not
|
80
|
+
else
|
81
|
+
default.is_a?(Symbol) ? default : default.normalize_bool
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
##
|
87
|
+
## @brief Turn raw urls into HTML links
|
88
|
+
##
|
89
|
+
## @param opt (Hash) Additional Options
|
90
|
+
##
|
91
|
+
def link_urls(opt = {})
|
92
|
+
opt[:format] ||= :html
|
93
|
+
if opt[:format] == :html
|
94
|
+
gsub(%r{(?mi)((http|https)://)?([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&:/~+#]*[\w\-@^=%&/~+#])?}) do |_match|
|
95
|
+
m = Regexp.last_match
|
96
|
+
proto = m[1].nil? ? 'http://' : ''
|
97
|
+
%(<a href="#{proto}#{m[0]}" title="Link to #{m[0]}">[#{m[3]}]</a>)
|
98
|
+
end.gsub(/<(\w+:.*?)>/) do |match|
|
99
|
+
m = Regexp.last_match
|
100
|
+
if m[1] =~ /<a href/
|
101
|
+
match
|
102
|
+
else
|
103
|
+
%(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
else
|
107
|
+
self
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class ::Symbol
|
113
|
+
def normalize_bool!
|
114
|
+
replace normalize_bool
|
115
|
+
end
|
116
|
+
|
117
|
+
def normalize_bool
|
118
|
+
to_s.normalize_bool
|
119
|
+
end
|
120
|
+
end
|
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 = []
|
@@ -1218,7 +1222,7 @@ class WWID
|
|
1218
1222
|
@content.each do |_k, v|
|
1219
1223
|
combined['items'] += v['items']
|
1220
1224
|
end
|
1221
|
-
section = if opt[:tag_filter] && opt[:tag_filter]['bool'] !=
|
1225
|
+
section = if opt[:tag_filter] && opt[:tag_filter]['bool'].normalize_bool != :not
|
1222
1226
|
opt[:tag_filter]['tags'].map do |tag|
|
1223
1227
|
"@#{tag}"
|
1224
1228
|
end.join(' + ')
|
@@ -1249,31 +1253,7 @@ class WWID
|
|
1249
1253
|
end
|
1250
1254
|
|
1251
1255
|
if opt[:tag_filter] && !opt[:tag_filter]['tags'].empty?
|
1252
|
-
items.
|
1253
|
-
case opt[:tag_filter]['bool']
|
1254
|
-
when /(AND|ALL)/
|
1255
|
-
del = false
|
1256
|
-
opt[:tag_filter]['tags'].each do |tag|
|
1257
|
-
unless item['title'] =~ /@#{tag}/
|
1258
|
-
del = true
|
1259
|
-
break
|
1260
|
-
end
|
1261
|
-
end
|
1262
|
-
del
|
1263
|
-
when /NONE/
|
1264
|
-
del = false
|
1265
|
-
opt[:tag_filter]['tags'].each do |tag|
|
1266
|
-
del = true if item['title'] =~ /@#{tag}/
|
1267
|
-
end
|
1268
|
-
del
|
1269
|
-
when /(OR|ANY)/
|
1270
|
-
del = true
|
1271
|
-
opt[:tag_filter]['tags'].each do |tag|
|
1272
|
-
del = false if item['title'] =~ /@#{tag}/
|
1273
|
-
end
|
1274
|
-
del
|
1275
|
-
end
|
1276
|
-
end
|
1256
|
+
items.select! { |item| item.has_tags?(opt[:tag_filter]['tags'], opt[:tag_filter]['bool']) }
|
1277
1257
|
end
|
1278
1258
|
|
1279
1259
|
if opt[:search]
|
@@ -1565,7 +1545,7 @@ class WWID
|
|
1565
1545
|
count = options[:keep] || 0
|
1566
1546
|
destination = options[:destination] || 'Archive'
|
1567
1547
|
tags = options[:tags] || []
|
1568
|
-
bool = options[:bool] ||
|
1548
|
+
bool = options[:bool] || :and
|
1569
1549
|
|
1570
1550
|
section = choose_section if section.nil? || section =~ /choose/i
|
1571
1551
|
archive_all = section =~ /^all$/i # && !(tags.nil? || tags.empty?)
|
@@ -1593,7 +1573,7 @@ class WWID
|
|
1593
1573
|
def do_archive(sect, destination, opt = {})
|
1594
1574
|
count = opt[:count] || 0
|
1595
1575
|
tags = opt[:tags] || []
|
1596
|
-
bool = opt[:bool] ||
|
1576
|
+
bool = opt[:bool] || :and
|
1597
1577
|
label = opt[:label] || true
|
1598
1578
|
|
1599
1579
|
if sect =~ /^all$/i
|
@@ -1606,7 +1586,7 @@ class WWID
|
|
1606
1586
|
counter = 0
|
1607
1587
|
|
1608
1588
|
all_sections.each do |section|
|
1609
|
-
items = @content[section]['items']
|
1589
|
+
items = @content[section]['items'].dup
|
1610
1590
|
|
1611
1591
|
moved_items = []
|
1612
1592
|
if !tags.empty? || opt[:search]
|
@@ -1630,13 +1610,7 @@ class WWID
|
|
1630
1610
|
@content[destination]['items'].concat(moved_items)
|
1631
1611
|
@results.push("Archived #{moved_items.length} items from #{section} to #{destination}")
|
1632
1612
|
else
|
1633
|
-
count = items.length if
|
1634
|
-
|
1635
|
-
@content[section]['items'] = if count.zero?
|
1636
|
-
[]
|
1637
|
-
else
|
1638
|
-
items[0..count - 1]
|
1639
|
-
end
|
1613
|
+
count = items.length if items.length < count
|
1640
1614
|
|
1641
1615
|
items.map! do |item|
|
1642
1616
|
if label && section != 'Currently'
|
@@ -1645,12 +1619,19 @@ class WWID
|
|
1645
1619
|
end
|
1646
1620
|
item
|
1647
1621
|
end
|
1622
|
+
|
1648
1623
|
if items.count > count
|
1649
1624
|
@content[destination]['items'].concat(items[count..-1])
|
1650
1625
|
else
|
1651
1626
|
@content[destination]['items'].concat(items)
|
1652
1627
|
end
|
1653
1628
|
|
1629
|
+
@content[section]['items'] = if count.zero?
|
1630
|
+
[]
|
1631
|
+
else
|
1632
|
+
items[0..count - 1]
|
1633
|
+
end
|
1634
|
+
|
1654
1635
|
@results.push("Archived #{items.length - count} items from #{section} to #{destination}")
|
1655
1636
|
end
|
1656
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.67
|
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
|
@@ -165,6 +165,7 @@ files:
|
|
165
165
|
- README.md
|
166
166
|
- bin/doing
|
167
167
|
- lib/doing.rb
|
168
|
+
- lib/doing/helpers.rb
|
168
169
|
- lib/doing/version.rb
|
169
170
|
- lib/doing/wwid.rb
|
170
171
|
- lib/templates/doing.css
|