doing 1.0.67 → 1.0.72

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: 2adffea75d109f047bd36b1ce55c48b83bc61a5d05d2060ece89fc8041faaabd
4
- data.tar.gz: 546e1c04bf9f0b319905e21a0aadb4ce9afb2eee8c49b7e8bc551d589d4f75d0
3
+ metadata.gz: 8ab378bf9ba26ec5c6f852afa702d74bd8323bb9e4aca92e3e94b081c578b637
4
+ data.tar.gz: 1c868eb6ac0dbb438f69679abfcb2fc587465ce9848c4da607f537b8c98d60ca
5
5
  SHA512:
6
- metadata.gz: 34db37e7c5974f97bad9516970bccb8d1a3b28bd0eefec7fbbf57628137cd763fc5f5473c85cee05334088f64d8a7f8621d1bd6f41fc694ad4534569e3360313
7
- data.tar.gz: 510d4f38fbe89ba480aedf9f8845c99f33c7265c1f852be76151f75aed6e10fee49e9912e1b1161f6ebbd5b7b98889663e289136fe9e63828b71544c0b1fcd7c
6
+ metadata.gz: 257898f624de2463259c5b49e46bc1687edf56f428aa16d297d212554efabbff82f59f0ae98da9772fc1aa30673623d70a2903326dca6ffd510d6afec846ff43
7
+ data.tar.gz: f635bc6bbb42de40a4f5bdcd1abd1d4caeec1a94af7553f1bcff1d0fffcda6898c248941a38f7abfba807721a18d39fd1632d0ec228c56b940c502f036ac7f19
data/README.md CHANGED
@@ -25,11 +25,9 @@ While I'm working, I have hourly reminders to record what I'm working on, and I
25
25
 
26
26
  If there's something I want to look at later but doesn't need to be added to a task list or tracker, I can type `doing later check out the pinboard bookmarks from macdrifter`. When I get back to my computer --- or just need a refresher after a distraction --- I can type `doing last` to see what the last thing on my plate was. I can also type `doing recent` (or just `doing`) to get a list of the last few entries. `doing today` gives me everything since midnight for the current day, making it easy to see what I've accomplished over a sleepless night.
27
27
 
28
- _Side note:_ I actually use the library behind this utility as part of another script that mirrors entries in [Day One](http://dayoneapp.com) that have the tag `wwid`. I can use the hourly writing reminders and enter my stuff in the quick entry popup. Someday I'll get around to cleaning that up and putting it out there.
29
-
30
28
  ## Installation
31
29
 
32
- The current version of `doing` is <!--VER-->1.0.66<!--END VER-->.
30
+ The current version of `doing` is <!--VER-->1.0.70<!--END VER-->.
33
31
 
34
32
  $ [sudo] gem install doing
35
33
 
@@ -464,6 +462,7 @@ You can also include a `--no-date` switch to add `@done` without a finish date,
464
462
 
465
463
  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
464
 
465
+ Both `finish` and `cancel` accept `--unfinished` as an argument. This causes them to act on the last entry not already marked @done, no matter how far back it's dated or how many @done entries come after it. You can use `doing finish --unfinished X -s SECTION` to finish the last X unfinished entries in SECTION.
467
466
 
468
467
  ##### Tagging and Autotagging
469
468
 
@@ -618,6 +617,30 @@ Now you can run `doing import --type timing -s SECTION PATH`, where SECTION is t
618
617
  # Import to default section (Currently) and prefix entries with '[Imported]'
619
618
  doing import --prefix="[Imported]" "~/Desktop/All Activities.json"
620
619
 
620
+ #### Interactive Usage
621
+
622
+ If you have `fzf` installed (<https://github.com/junegunn/fzf>), you can use `doing select` to get a menu of all your items (or items in a given section) which can be searched with fuzzy matching. The menu allows multiple selections to be acted on directly.
623
+
624
+ To use the menu, type a search string or use the arrow keys to navigate up and down. Press tab on an entry you'd like to perform an action on. A marker will show up on the left indicating the entry is selected. Repeat the process and select as many entries as needed. When you hit Return, the selection will be passed back to doing.
625
+
626
+ Doing can perform several functions with this menu. Not all of doing's features are available, but the core functionality you'd need is there, plus you can open all selected entries in your editor, make changes to them, and when you save and close the entries are updated accordingly. This allows editing of everything from timestamps to tags to notes.
627
+
628
+ Run `doing help select` for a list of options:
629
+
630
+ -a, --archive - Archive selected items
631
+ -c, --cancel - Cancel selected items (add @done without timestamp)
632
+ -d, --delete - Delete selected items
633
+ -e, --editor - Edit selected item(s)
634
+ -f, --finish - Add @done with current time to selected item(s)
635
+ --flag - Add flag to selected item(s)
636
+ -m, --move=SECTION - Move selected items to section (default: none)
637
+ -s, --section=SECTION - Select from a specific section (default: none)
638
+ -t, --tag=TAG - Tag selected entries (default: none)
639
+
640
+ For example, `doing select -d -a` would present the menu, and then mark selected entries as @done (with timestamp) and move them to the Archive section.
641
+
642
+ Multiple actions can be performed at once by combining options. You can also combine the `--editor` switch with any other options. Other actions will be performed first, then the entries --- with any modifications performed --- will be presented in the editor for tweaking.
643
+
621
644
  ---
622
645
 
623
646
  ## Extras
data/bin/doing CHANGED
@@ -245,7 +245,7 @@ command :template do |c|
245
245
  c.action do |_global_options, options, args|
246
246
  raise 'No type specified, use `doing template [HAML|CSS]`' if args.empty?
247
247
 
248
- case options[:t]
248
+ case args[0]
249
249
  when /html|haml/i
250
250
  $stdout.puts wwid.haml_template
251
251
  when /css/i
@@ -256,6 +256,51 @@ command :template do |c|
256
256
  end
257
257
  end
258
258
 
259
+ desc 'Display an interactive menu to perform operations (requires fzf)'
260
+ long_desc 'Requires that `fzf` be installed and available in your path. <https://github.com/junegunn/fzf>
261
+
262
+ List all entries and select with typeahead fuzzy matching. Multiple selections are allowed, hit tab to add the highlighted entry to the selection. Return processes the selected entries.'
263
+ command :select do |c|
264
+ c.desc 'Select from a specific section'
265
+ c.arg_name 'SECTION'
266
+ c.flag %i[s section]
267
+
268
+ c.desc 'Tag selected entries'
269
+ c.arg_name 'TAG'
270
+ c.flag %i[t tag]
271
+
272
+ # c.desc 'Add @done to selected item(s), using start time of next item as the finish time'
273
+ # c.switch %i[a auto], negatable: false, default_value: false
274
+
275
+ c.desc 'Archive selected items'
276
+ c.switch %i[a archive], negatable: false, default_value: false
277
+
278
+ c.desc 'Move selected items to section'
279
+ c.arg_name 'SECTION'
280
+ c.flag %i[m move]
281
+
282
+ c.desc 'Cancel selected items (add @done without timestamp)'
283
+ c.switch %i[c cancel], negatable: false, default_value: false
284
+
285
+ c.desc 'Delete selected items'
286
+ c.switch %i[d delete], negatable: false, default_value: false
287
+
288
+ c.desc 'Edit selected item(s)'
289
+ c.switch %i[e editor], negatable: false, default_value: false
290
+
291
+ c.desc 'Add @done with current time to selected item(s)'
292
+ c.switch %i[f finish], negatable: false, default_value: false
293
+
294
+ c.desc 'Add flag to selected item(s)'
295
+ c.switch %i[flag], negatable: false, default_value: false
296
+
297
+ c.action do |_global_options, options, args|
298
+ section = options[:section] || 'All'
299
+ edit = options[:editor]
300
+ wwid.interactive(options)
301
+ end
302
+ end
303
+
259
304
  desc 'Add an item to the Later section'
260
305
  arg_name 'ENTRY'
261
306
  command :later do |c|
@@ -368,7 +413,7 @@ command %i[done did] do |c|
368
413
 
369
414
  date = options[:took] ? finish_date - took : finish_date
370
415
  elsif options[:took]
371
- finish_date = options[:back] ? date + took : nil
416
+ finish_date = date + took
372
417
  elsif options[:back]
373
418
  finish_date = date
374
419
  else
@@ -446,6 +491,9 @@ command :cancel do |c|
446
491
  c.arg_name 'BOOLEAN'
447
492
  c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
448
493
 
494
+ c.desc 'Cancel last entry (or entries) not already marked @done'
495
+ c.switch %i[u unfinished], negatable: false, default_value: false
496
+
449
497
  c.action do |_global_options, options, args|
450
498
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
451
499
 
@@ -478,7 +526,8 @@ command :cancel do |c|
478
526
  sequential: false,
479
527
  tag: tags,
480
528
  tag_bool: options[:bool],
481
- tags: ['done']
529
+ tags: ['done'],
530
+ unfinished: options[:unfinished]
482
531
  }
483
532
  wwid.tag_last(opts)
484
533
  end
@@ -512,6 +561,9 @@ command :finish do |c|
512
561
  c.arg_name 'BOOLEAN'
513
562
  c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
514
563
 
564
+ c.desc 'Finish last entry (or entries) not already marked @done'
565
+ c.switch %i[u unfinished], negatable: false, default_value: false
566
+
515
567
  c.desc %(Auto-generate finish dates from next entry's start time.
516
568
  Automatically generate completion dates 1 minute before next start date.
517
569
  --auto overrides the --date and --back parameters.)
@@ -574,7 +626,8 @@ command :finish do |c|
574
626
  sequential: options[:auto],
575
627
  tag: tags,
576
628
  tag_bool: options[:bool],
577
- tags: ['done']
629
+ tags: ['done'],
630
+ unfinished: options[:unfinished]
578
631
  }
579
632
  wwid.tag_last(opts)
580
633
  end
@@ -648,6 +701,9 @@ command :tag do |c|
648
701
  c.desc 'Remove given tag(s)'
649
702
  c.switch %i[r remove], negatable: false, default_value: false
650
703
 
704
+ c.desc 'Tag last entry (or entries) not marked @done'
705
+ c.switch %i[u unfinished], negatable: false, default_value: false
706
+
651
707
  c.desc 'Autotag entries based on autotag configuration in ~/.doingrc'
652
708
  c.switch %i[a autotag], negatable: false, default_value: false
653
709
 
@@ -691,7 +747,8 @@ command :tag do |c|
691
747
  date: options[:date],
692
748
  remove: options[:r],
693
749
  section: section,
694
- tags: tags
750
+ tags: tags,
751
+ unfinished: options[:unfinished]
695
752
  }
696
753
  wwid.tag_last(opts)
697
754
  end
@@ -706,9 +763,17 @@ command [:mark, :flag] do |c|
706
763
  c.desc 'Remove mark'
707
764
  c.switch %i[r remove], negatable: false, default_value: false
708
765
 
766
+ c.desc 'Mark last entry not marked @done'
767
+ c.switch %i[u unfinished], negatable: false, default_value: false
768
+
709
769
  c.action do |_global_options, options, _args|
710
770
  mark = wwid.config['marker_tag'] || 'flagged'
711
- wwid.tag_last({ tags: [mark], section: options[:s], remove: options[:r] })
771
+ wwid.tag_last({
772
+ remove: options[:r],
773
+ section: options[:s],
774
+ tags: [mark],
775
+ unfinished: options[:unfinished]
776
+ })
712
777
  end
713
778
  end
714
779
 
@@ -737,7 +802,7 @@ command :show do |c|
737
802
 
738
803
  c.desc 'Sort order (asc/desc)'
739
804
  c.arg_name 'ORDER'
740
- c.flag %i[s sort], must_match: /^(?:a|d)/i, default_value: 'ASC'
805
+ c.flag %i[s sort], must_match: /^[ad].*/i, default_value: 'ASC'
741
806
 
742
807
  c.desc %(
743
808
  Date range to show, or a single day to filter date on.
data/lib/doing/helpers.rb CHANGED
@@ -6,6 +6,7 @@ class ::Hash
6
6
  tags = tags.split(/ *, */) if tags.is_a? String
7
7
  bool = bool.normalize_bool if bool.is_a? String
8
8
  item = self
9
+ tags.map! {|t| t.strip.sub(/^@/, '')}
9
10
  case bool
10
11
  when :and
11
12
  result = true
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '1.0.67'
2
+ VERSION = '1.0.72'
3
3
  end
data/lib/doing/wwid.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'deep_merge'
4
4
  require 'open3'
5
5
  require 'pp'
6
+ require 'shellwords'
6
7
 
7
8
  ##
8
9
  ## @brief Main "What Was I Doing" methods
@@ -318,8 +319,8 @@ class WWID
318
319
 
319
320
  note = input_lines.length > 1 ? input_lines[1..-1] : []
320
321
  # If title line ends in a parenthetical, use that as the note
321
- if note.empty? && title =~ /\(.*?\)$/
322
- title.sub!(/\((.*?)\)$/) do
322
+ if note.empty? && title =~ /\s+\(.*?\)$/
323
+ title.sub!(/\s+\((.*?)\)$/) do
323
324
  m = Regexp.last_match
324
325
  note.push(m[1])
325
326
  ''
@@ -765,6 +766,129 @@ class WWID
765
766
  all_items.max_by { |item| item['date'] }
766
767
  end
767
768
 
769
+
770
+ ##
771
+ ## @brief Display an interactive menu of entries
772
+ ##
773
+ ## @param opt (Hash) Additional options
774
+ ##
775
+ def interactive(opt = {})
776
+ raise "Select command requires that fzf be installed" unless exec_available('fzf')
777
+
778
+ section = opt[:section] ? guess_section(opt[:section]) : 'All'
779
+
780
+
781
+ if section =~ /^all$/i
782
+ combined = { 'items' => [] }
783
+ @content.each do |_k, v|
784
+ combined['items'] += v['items']
785
+ end
786
+ items = combined['items'].dup.sort_by { |item| item['date'] }.reverse
787
+ else
788
+ items = @content[section]['items']
789
+ end
790
+
791
+
792
+ options = items.map.with_index do |item, i|
793
+ out = [
794
+ i,
795
+ ') ',
796
+ item['date'],
797
+ ' | ',
798
+ item['title'],
799
+ ]
800
+ if opt[:section] =~ /^all/i
801
+ out.concat([
802
+ ' (',
803
+ item['section'],
804
+ ') '
805
+ ])
806
+ end
807
+ out.join('')
808
+ end
809
+
810
+ res = `echo #{Shellwords.escape(options.join("\n"))}|fzf -m`
811
+ selected = []
812
+ res.split(/\n/).each do |item|
813
+ idx = item.match(/^(\d+)\)/)[1].to_i
814
+ selected.push(items[idx])
815
+ end
816
+
817
+ if selected.empty?
818
+ @results.push("No selection")
819
+ return
820
+ end
821
+
822
+ if opt[:delete]
823
+ res = yn("Delete #{selected.size} items?", default_response: 'y')
824
+ if res
825
+ selected.each {|item| delete_item(item) }
826
+ write(@doing_file)
827
+ end
828
+ return
829
+ end
830
+
831
+ if opt[:flag]
832
+ tag = @config['marker_tag'] || 'flagged'
833
+ selected.map! {|item| tag_item(item, tag, date: false) }
834
+ end
835
+
836
+ if opt[:finish] || opt[:cancel]
837
+ tag = 'done'
838
+ selected.map! {|item| tag_item(item, tag, date: !opt[:cancel])}
839
+ end
840
+
841
+ if opt[:tag]
842
+ tag = opt[:tag]
843
+ selected.map! {|item| tag_item(item, tag, date: false)}
844
+ end
845
+
846
+ if opt[:archive] || opt[:move]
847
+ section = opt[:archive] ? 'Archive' : guess_section(opt[:move])
848
+ selected.map! {|item| move_item(item, section) }
849
+ end
850
+
851
+ write(@doing_file)
852
+
853
+ if opt[:editor]
854
+
855
+ editable_items = []
856
+
857
+ selected.each do |item|
858
+ editable = "#{item['date']} | #{item['title']}"
859
+ old_note = item['note'] ? item['note'].map(&:strip).join("\n") : nil
860
+ editable += "\n#{old_note}" unless old_note.nil?
861
+ editable_items << editable
862
+ end
863
+
864
+ new_items = fork_editor(editable_items.join("\n---\n") + "\n\n# You may delete entries, but leave all --- lines in place").split(/\n---\n/)
865
+
866
+ new_items.each_with_index do |new_item, i|
867
+ input_lines = new_item.split(/[\n\r]+/).delete_if {|line| line =~ /^#/ || line =~ /^\s*$/ }
868
+ title = input_lines[0]&.strip
869
+ if title.nil? || title =~ /^---$/ || title.strip.empty?
870
+ delete_item(selected[i])
871
+ else
872
+ note = input_lines.length > 1 ? input_lines[1..-1] : []
873
+
874
+ note.map!(&:strip)
875
+ note.delete_if { |line| line =~ /^\s*$/ || line =~ /^#/ }
876
+
877
+ date = title.match(/^([\d\-: ]+) \| /)[1]
878
+ title.sub!(/^([\d\-: ]+) \| /, '')
879
+
880
+ item = selected[i].dup
881
+ item['title'] = title
882
+ item['note'] = note
883
+ item['date'] = Time.parse(date)
884
+ update_item(selected[i], item)
885
+ end
886
+ end
887
+
888
+ write(@doing_file)
889
+ end
890
+ end
891
+
768
892
  ##
769
893
  ## @brief Tag the last entry or X entries
770
894
  ##
@@ -781,6 +905,7 @@ class WWID
781
905
  opt[:autotag] ||= false
782
906
  opt[:back] ||= false
783
907
  opt[:took] ||= nil
908
+ opt[:unfinished] ||= false
784
909
 
785
910
  sec_arr = []
786
911
 
@@ -816,10 +941,11 @@ class WWID
816
941
  items.map! do |item|
817
942
  break if idx == count
818
943
 
944
+ finished = opt[:unfinished] && item.has_tags?('done', :and)
819
945
  tag_match = opt[:tag].nil? || opt[:tag].empty? ? true : item.has_tags?(opt[:tag], opt[:tag_bool])
820
946
  search_match = opt[:search].nil? || opt[:search].empty? ? true : item.matches_search?(opt[:search])
821
947
 
822
- if tag_match && search_match
948
+ if tag_match && search_match && !finished
823
949
  if opt[:autotag]
824
950
  new_title = autotag(item['title']) if @auto_tag
825
951
  if new_title == item['title']
@@ -832,12 +958,6 @@ class WWID
832
958
  if opt[:sequential]
833
959
  done_date = next_start - 1
834
960
  next_start = item['date']
835
- elsif opt[:back]
836
- if opt[:back].is_a? Integer
837
- done_date = item['date'] + opt[:back]
838
- else
839
- done_date = item['date'] + (opt[:back] - item['date'])
840
- end
841
961
  elsif opt[:took]
842
962
  if item['date'] + opt[:took] > Time.now
843
963
  item['date'] = Time.now - opt[:took]
@@ -845,6 +965,12 @@ class WWID
845
965
  else
846
966
  done_date = item['date'] + opt[:took]
847
967
  end
968
+ elsif opt[:back]
969
+ if opt[:back].is_a? Integer
970
+ done_date = item['date'] + opt[:back]
971
+ else
972
+ done_date = item['date'] + (opt[:back] - item['date'])
973
+ end
848
974
  else
849
975
  done_date = Time.now
850
976
  end
@@ -902,6 +1028,71 @@ class WWID
902
1028
  write(@doing_file)
903
1029
  end
904
1030
 
1031
+ def move_item(item, section)
1032
+ old_section = item['section']
1033
+ new_item = item.dup
1034
+ new_item['section'] = section
1035
+
1036
+ section_items = @content[old_section]['items']
1037
+ section_items.delete(item)
1038
+ @content[old_section]['items'] = section_items
1039
+
1040
+ archive_items = @content[section]['items']
1041
+ archive_items.push(new_item)
1042
+ # archive_items = archive_items.sort_by { |item| item['date'] }
1043
+ @content[section]['items'] = archive_items
1044
+
1045
+ @results.push("Entry moved to #{section}: #{new_item['title']}")
1046
+ return new_item
1047
+ end
1048
+
1049
+ ##
1050
+ ## @brief Delete an item from the index
1051
+ ##
1052
+ ## @param old_item
1053
+ ##
1054
+ def delete_item(old_item)
1055
+ section = old_item['section']
1056
+
1057
+ section_items = @content[section]['items']
1058
+ deleted = section_items.delete(old_item)
1059
+ @results.push("Entry deleted: #{deleted['title']}")
1060
+ @content[section]['items'] = section_items
1061
+ end
1062
+
1063
+ ##
1064
+ ## @brief Tag an item from the index
1065
+ ##
1066
+ ## @param old_item (Item) The item to tag
1067
+ ## @param tag (string) The tag to apply
1068
+ ## @param date (Boolean) Include timestamp?
1069
+ ##
1070
+ def tag_item(old_item, tag, date: false)
1071
+ title = old_item['title'].dup
1072
+ done_date = Time.now
1073
+ if title !~ /@#{tag}/
1074
+ title.chomp!
1075
+ if date
1076
+ title += " @#{tag}(#{done_date.strftime('%F %R')})"
1077
+ else
1078
+ title += " @#{tag}"
1079
+ end
1080
+ new_item = old_item.dup
1081
+ new_item['title'] = title
1082
+ update_item(old_item, new_item)
1083
+ return new_item
1084
+ else
1085
+ @results.push(%(Item already @#{tag}: "#{title}" in #{old_item['section']}))
1086
+ return old_item
1087
+ end
1088
+ end
1089
+
1090
+ ##
1091
+ ## @brief Update an item in the index with a modified item
1092
+ ##
1093
+ ## @param old_item The old item
1094
+ ## @param new_item The new item
1095
+ ##
905
1096
  def update_item(old_item, new_item)
906
1097
  section = old_item['section']
907
1098
 
@@ -2003,4 +2194,12 @@ EOS
2003
2194
  minutes = (minutes % 60).to_i
2004
2195
  [days, hours, minutes]
2005
2196
  end
2197
+
2198
+ def exec_available(cli)
2199
+ if File.exists?(File.expand_path(cli))
2200
+ File.executable?(File.expand_path(cli))
2201
+ else
2202
+ system "which #{cli}", :out => File::NULL, :err => File::NULL
2203
+ end
2204
+ end
2006
2205
  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.67
4
+ version: 1.0.72
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-26 00:00:00.000000000 Z
11
+ date: 2021-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake