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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e2384e06e56e40f7f6c2b26121d16e0caa2cf40639158fc0f079f8ae26738da
4
- data.tar.gz: 9e62f0ac38c0f19082094b88382e9779b743a4885403d796dddfb21a68ea1de2
3
+ metadata.gz: 6e02c8453f718dfa428e578ab1b125528aa07ffee1390bbe8222fb425abd63fa
4
+ data.tar.gz: 333707321b7e92312ee8a2874292b45df782c83eff2ddcdb5279b648b14730ff
5
5
  SHA512:
6
- metadata.gz: 8cdb34a404ae818f10ee9facaba83fd44c584d06950fd2908b6fbb25b04e6a117a74a20836b765a2dca1cde480928398a42b5999e1678499013cbb1e88e5ccac
7
- data.tar.gz: 5a771f4ba236518f54cd11bc99f1258b90026afe4face70db83e98f5f5ee97124eab732c7c65ccd61f1b98e18129c4034ea6c036c145e45abe298f058e25bc3e
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.61<!--END VER-->.
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, did - Add an entry
397
+ now, next - Add an entry
398
398
  later - Add an item to the Later section
399
- done - Add a completed item with @done(date). No argument finishes last entry.
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
- The `doing now` command can accept `-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`.
403
+ ##### now
404
404
 
405
- If you want to use `--back` with `doing done` but want the end time to be different than the start time, you can either use `--took` in addition, or just use `--took` on its own as it will backdate the start time such that the end time is now and the duration is equal to the value of the `--took` argument.
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
- `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 Archive with `-a`.
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
- `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. 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.
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 [: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?
@@ -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: /^(and|or|not)$/i, default_value: 'AND'
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] = 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
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: /^(and|or|not)$/i, default_value: 'AND'
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] = 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
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: /^(and|or|not)$/i, default_value: 'AND'
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,NONE)'
726
+ c.desc 'Tag boolean (AND,OR,NOT)'
697
727
  c.arg_name 'BOOLEAN'
698
- 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'
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
- if arg =~ /,/
760
- arg.split(/,/).each do |tag|
761
- tags.push(tag.strip.sub(/^@/, ''))
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[:b]
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] = 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
+
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'].upcase : 'OR'
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
@@ -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'
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '1.0.62'
2
+ VERSION = '1.0.66'
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 = []
@@ -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
- done_date = item['date'] + opt[:took]
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'] != 'NONE'
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.delete_if do |item|
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] || 'AND'
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] || 'AND'
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 count == 0 || items.length < count
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.62
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-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