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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c103e507e82866174c88e6c3613f613964eaacba5f499631598b9ecbbb0cbe55
4
- data.tar.gz: edb32425ba3e81c1e3ff5bfeed3d0cb0b2dd8c2a353bc3d6adee6b98bf3536e0
3
+ metadata.gz: 2adffea75d109f047bd36b1ce55c48b83bc61a5d05d2060ece89fc8041faaabd
4
+ data.tar.gz: 546e1c04bf9f0b319905e21a0aadb4ce9afb2eee8c49b7e8bc551d589d4f75d0
5
5
  SHA512:
6
- metadata.gz: 20b03da63cba70b6c12a2f7775429f974fba8bc12b5f7c411d7a8e4a765f6c3113d3a627cf96684e4fbdb26c1b0430e0beec196cd0d2f655dd4459ba10b0902f
7
- data.tar.gz: 934a87ead078f5fa7d2b7260921bfdeb15a57ecddd7a793003ad7dabb5dbf5bc093c1048258ffaba41e0ad00f5d49ab26cbdb825cca5e7f21c30446d92e3b10e
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.62<!--END VER-->.
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 [:now, :next] do |c|
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
- title, note = wwid.format_input($stdin.read)
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: /^(and|or|not)$/i, default_value: 'AND'
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] = options[:bool] =~ /^(and|or|not)$/i ? options[:bool].upcase : 'AND'
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: /^(and|or|not)$/i, default_value: 'AND'
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] = options[:bool] =~ /^(and|or|not)$/i ? options[:bool].upcase : 'AND'
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: /^(and|or|not)$/i, default_value: 'AND'
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,NONE)'
726
+ c.desc 'Tag boolean (AND,OR,NOT)'
698
727
  c.arg_name 'BOOLEAN'
699
- c.flag %i[b bool], must_match: /^(and|or|not)$/i, default_value: 'OR'
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
- if arg =~ /,/
761
- arg.split(/,/).each do |tag|
762
- tags.push(tag.strip.sub(/^@/, ''))
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[:b]
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] = options[:bool] =~ /^(and|or|not)$/i ? options[:bool].upcase : 'AND'
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'].upcase : 'OR'
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
@@ -8,4 +8,5 @@ require 'tempfile'
8
8
  require 'chronic'
9
9
  require 'haml'
10
10
  require 'json'
11
+ require 'doing/helpers.rb'
11
12
  require 'doing/wwid.rb'
@@ -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\-.,@?^=%&amp;:/~+#]*[\w\-@^=%&amp;/~+#])?}) 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
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '1.0.63'
2
+ VERSION = '1.0.67'
3
3
  end
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\-.,@?^=%&amp;:/~+#]*[\w\-@^=%&amp;/~+#])?}) 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.gsub(/ *$/, ''))
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] ||= 'AND'
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] ||= 'AND'
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'] != 'NONE'
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.delete_if do |item|
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] || 'AND'
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] || 'AND'
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 count == 0 || items.length < count
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.63
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-17 00:00:00.000000000 Z
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