doing 1.0.51 → 1.0.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -9
  3. data/bin/doing +754 -578
  4. data/lib/doing/version.rb +1 -1
  5. data/lib/doing/wwid.rb +221 -86
  6. metadata +6 -6
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '1.0.51'
2
+ VERSION = '1.0.56'
3
3
  end
data/lib/doing/wwid.rb CHANGED
@@ -3,6 +3,56 @@
3
3
  require 'deep_merge'
4
4
  require 'open3'
5
5
 
6
+ ##
7
+ ## @brief Hash helpers
8
+ ##
9
+ class Hash
10
+ def has_tags?(tags, bool = 'AND')
11
+ tags = tags.split(/ *, */) if tags.is_a? String
12
+ item = self
13
+ case bool
14
+ when 'AND'
15
+ result = true
16
+ tags.each do |tag|
17
+ unless item['title'] =~ /@#{tag}/
18
+ result = false
19
+ break
20
+ end
21
+ end
22
+ result
23
+ when 'NOT'
24
+ result = true
25
+ tags.each do |tag|
26
+ if item['title'] =~ /@#{tag}/
27
+ result = false
28
+ break
29
+ end
30
+ end
31
+ result
32
+ else
33
+ result = false
34
+ tags.each do |tag|
35
+ if item['title'] =~ /@#{tag}/
36
+ result = true
37
+ break
38
+ end
39
+ end
40
+ result
41
+ end
42
+ end
43
+
44
+ def matches_search?(search)
45
+ item = self
46
+ text = item['note'] ? item['title'] + item['note'].join(' ') : item['title']
47
+ pattern = if search.strip =~ %r{^/.*?/$}
48
+ search.sub(%r{/(.*?)/}, '\1')
49
+ else
50
+ search.split('').join('.{0,3}')
51
+ end
52
+ text =~ /#{pattern}/i ? true : false
53
+ end
54
+ end
55
+
6
56
  ##
7
57
  ## @brief String helpers
8
58
  ##
@@ -131,6 +181,7 @@ class WWID
131
181
  @config['autotag']['synonyms'] ||= {}
132
182
  @config['doing_file'] ||= '~/what_was_i_doing.md'
133
183
  @config['current_section'] ||= 'Currently'
184
+ @config['config_editor_app'] ||= nil
134
185
  @config['editor_app'] ||= nil
135
186
 
136
187
  @config['html_template'] ||= {}
@@ -333,7 +384,7 @@ class WWID
333
384
  tmpfile.unlink
334
385
  end
335
386
 
336
- input
387
+ input.split(/\n/).delete_if {|line| line =~ /^#/ }.join("\n").strip
337
388
  end
338
389
 
339
390
  #
@@ -346,9 +397,20 @@ class WWID
346
397
  def format_input(input)
347
398
  raise 'No content in entry' if input.nil? || input.strip.empty?
348
399
 
349
- input_lines = input.split(/[\n\r]+/)
350
- title = input_lines[0].strip
400
+ input_lines = input.split(/[\n\r]+/).delete_if {|line| line =~ /^#/ || line =~ /^\s*$/ }
401
+ title = input_lines[0]&.strip
402
+ raise 'No content in first line' if title.nil? || title.strip.empty?
403
+
351
404
  note = input_lines.length > 1 ? input_lines[1..-1] : []
405
+ # If title line ends in a parenthetical, use that as the note
406
+ if note.empty? && title =~ /\(.*?\)$/
407
+ title.sub!(/\((.*?)\)$/) do
408
+ m = Regexp.last_match
409
+ note.push(m[1])
410
+ ''
411
+ end
412
+ end
413
+
352
414
  note.map!(&:strip)
353
415
  note.delete_if { |line| line =~ /^\s*$/ || line =~ /^#/ }
354
416
 
@@ -637,6 +699,8 @@ class WWID
637
699
  def restart_last(opt = {})
638
700
  opt[:section] ||= 'all'
639
701
  opt[:note] ||= []
702
+ opt[:tag] ||= []
703
+ opt[:tag_bool] ||= 'AND'
640
704
 
641
705
  last = last_entry(opt)
642
706
  if last.nil?
@@ -645,8 +709,9 @@ class WWID
645
709
  end
646
710
  # Remove @done tag
647
711
  title = last['title'].sub(/\s*@done(\(.*?\))?/, '').chomp
712
+ section = opt[:in].nil? ? last['section'] : guess_section(opt[:in])
648
713
  @auto_tag = false
649
- add_item(title, last['section'], { note: opt[:note], back: opt[:date], timed: true })
714
+ add_item(title, section, { note: opt[:note], back: opt[:date], timed: true })
650
715
  write(@doing_file)
651
716
  end
652
717
 
@@ -656,6 +721,7 @@ class WWID
656
721
  ## @param opt (Hash) Additional Options
657
722
  ##
658
723
  def last_entry(opt = {})
724
+ opt[:tag_bool] ||= 'AND'
659
725
  opt[:section] ||= @current_section
660
726
 
661
727
  sec_arr = []
@@ -682,6 +748,12 @@ class WWID
682
748
  all_items.concat(@content[section]['items'].dup) if @content.key?(section)
683
749
  end
684
750
 
751
+ if opt[:tag] && opt[:tag].length.positive?
752
+ all_items.select! { |item| item.has_tags?(opt[:tag], opt[:tag_bool]) }
753
+ elsif opt[:search]&.length
754
+ all_items.select! { |item| item.matches_search?(opt[:search]) }
755
+ end
756
+
685
757
  all_items.max_by { |item| item['date'] }
686
758
  end
687
759
 
@@ -735,41 +807,10 @@ class WWID
735
807
  items.map! do |item|
736
808
  break if index == count
737
809
 
738
- tag_match = if opt[:tag].length.positive?
739
- case opt[:tag_bool]
740
- when 'AND'
741
- result = true
742
- opt[:tag].each do |tag|
743
- unless item['title'] =~ /@#{tag}/
744
- result = false
745
- break
746
- end
747
- end
748
- result
749
- when 'NOT'
750
- result = true
751
- opt[:tag].each do |tag|
752
- if item['title'] =~ /@#{tag}/
753
- result = false
754
- break
755
- end
756
- end
757
- result
758
- else
759
- result = false
760
- opt[:tag].each do |tag|
761
- if item['title'] =~ /@#{tag}/
762
- result = true
763
- break
764
- end
765
- end
766
- result
767
- end
768
- else
769
- true
770
- end
771
-
772
- if tag_match
810
+ tag_match = opt[:tag].nil? || opt[:tag].empty? ? true : item.has_tags?(opt[:tag], opt[:tag_bool])
811
+ search_match = opt[:search].nil? || opt[:search].empty? ? true : item.matches_search?(opt[:search])
812
+
813
+ if tag_match && search_match
773
814
  if opt[:autotag]
774
815
  new_title = autotag(item['title']) if @auto_tag
775
816
  if new_title == item['title']
@@ -783,17 +824,23 @@ class WWID
783
824
  done_date = next_start - 1
784
825
  next_start = item['date']
785
826
  elsif opt[:back]
786
- done_date = item['date'] + (opt[:back] - item['date'])
827
+ if opt[:back].is_a? Integer
828
+ done_date = item['date'] + opt[:back]
829
+ else
830
+ done_date = item['date'] + (opt[:back] - item['date'])
831
+ end
787
832
  else
788
833
  done_date = Time.now
789
834
  end
790
835
 
791
836
  title = item['title']
792
837
  opt[:tags].each do |tag|
793
- tag.strip!
794
- if opt[:remove] && title =~ /@#{tag}\b/
795
- title.gsub!(/(^| )@#{tag}(\([^)]*\))?/, '')
796
- @results.push(%(Removed @#{tag}: "#{title}" in #{section}))
838
+ tag = tag.strip
839
+ if opt[:remove]
840
+ if title =~ /@#{tag}\b/
841
+ title.gsub!(/(^| )@#{tag}(\([^)]*\))?/, '')
842
+ @results.push(%(Removed @#{tag}: "#{title}" in #{section}))
843
+ end
797
844
  elsif title !~ /@#{tag}/
798
845
  title.chomp!
799
846
  title += if opt[:date]
@@ -839,6 +886,75 @@ class WWID
839
886
  write(@doing_file)
840
887
  end
841
888
 
889
+ ##
890
+ ## @brief Edit the last entry
891
+ ##
892
+ ## @param section (String) The section, default "All"
893
+ ##
894
+ def edit_last(section: 'All', options: {})
895
+ section = guess_section(section)
896
+
897
+ if section =~ /^all$/i
898
+ items = []
899
+ @content.each do |_k, v|
900
+ items.concat(v['items'])
901
+ end
902
+ # section = combined['items'].dup.sort_by { |item| item['date'] }.reverse[0]['section']
903
+ else
904
+ items = @content[section]['items']
905
+ end
906
+
907
+ items = items.sort_by { |item| item['date'] }.reverse
908
+
909
+ idx = nil
910
+
911
+ if options[:tag] && !options[:tag].empty?
912
+ items.each_with_index do |item, i|
913
+ if item.has_tags?(options[:tag], options[:tag_bool])
914
+ idx = i
915
+ break
916
+ end
917
+ end
918
+ elsif options[:search]
919
+ items.each_with_index do |item, i|
920
+ if item.matches_search?(options[:search])
921
+ idx = i
922
+ break
923
+ end
924
+ end
925
+ else
926
+ idx = 0
927
+ end
928
+
929
+ if idx.nil?
930
+ @results.push('No entries found')
931
+ return
932
+ end
933
+
934
+ section = items[idx]['section']
935
+
936
+ section_items = @content[section]['items']
937
+ s_idx = section_items.index(items[idx])
938
+
939
+ current_item = section_items[s_idx]['title']
940
+ old_note = section_items[s_idx]['note'] ? section_items[s_idx]['note'].map(&:strip).join("\n") : nil
941
+ current_item += "\n#{old_note}" unless old_note.nil?
942
+ new_item = fork_editor(current_item)
943
+ title, note = format_input(new_item)
944
+
945
+ if title.nil? || title.empty?
946
+ @results.push('No content provided')
947
+ elsif title == section_items[s_idx]['title'] && note == old_note
948
+ @results.push('No change in content')
949
+ else
950
+ section_items[s_idx]['title'] = title
951
+ section_items[s_idx]['note'] = note
952
+ @results.push("Entry edited: #{section_items[s_idx]['title']}")
953
+ @content[section]['items'] = section_items
954
+ write(@doing_file)
955
+ end
956
+ end
957
+
842
958
  ##
843
959
  ## @brief Add a note to the last entry in a section
844
960
  ##
@@ -934,7 +1050,7 @@ class WWID
934
1050
 
935
1051
  if opt[:new_item]
936
1052
  title, note = format_input(opt[:new_item])
937
- note.push(opt[:note].gsub(/ *$/, '')) if opt[:note]
1053
+ note.push(opt[:note].map(&:chomp)) if opt[:note]
938
1054
  title += " @#{tag}"
939
1055
  add_item(title.cap_first, opt[:section], { note: note.join(' ').rstrip, back: opt[:back] })
940
1056
  end
@@ -1092,10 +1208,7 @@ class WWID
1092
1208
  end
1093
1209
  end
1094
1210
 
1095
- if opt[:section].class != Hash
1096
- warn 'Invalid section object'
1097
- return
1098
- end
1211
+ raise 'Invalid section object' unless opt[:section].instance_of? Hash
1099
1212
 
1100
1213
  items = opt[:section]['items'].sort_by { |item| item['date'] }
1101
1214
 
@@ -1113,19 +1226,23 @@ class WWID
1113
1226
 
1114
1227
  if opt[:tag_filter] && !opt[:tag_filter]['tags'].empty?
1115
1228
  items.delete_if do |item|
1116
- if opt[:tag_filter]['bool'] =~ /(AND|ALL)/
1117
- score = 0
1229
+ case opt[:tag_filter]['bool']
1230
+ when /(AND|ALL)/
1231
+ del = false
1118
1232
  opt[:tag_filter]['tags'].each do |tag|
1119
- score += 1 if item['title'] =~ /@#{tag}/
1233
+ unless item['title'] =~ /@#{tag}/
1234
+ del = true
1235
+ break
1236
+ end
1120
1237
  end
1121
- score < opt[:tag_filter]['tags'].length
1122
- elsif opt[:tag_filter]['bool'] =~ /NONE/
1238
+ del
1239
+ when /NONE/
1123
1240
  del = false
1124
1241
  opt[:tag_filter]['tags'].each do |tag|
1125
1242
  del = true if item['title'] =~ /@#{tag}/
1126
1243
  end
1127
1244
  del
1128
- elsif opt[:tag_filter]['bool'] =~ /(OR|ANY)/
1245
+ when /(OR|ANY)/
1129
1246
  del = true
1130
1247
  opt[:tag_filter]['tags'].each do |tag|
1131
1248
  del = false if item['title'] =~ /@#{tag}/
@@ -1136,15 +1253,7 @@ class WWID
1136
1253
  end
1137
1254
 
1138
1255
  if opt[:search]
1139
- items.keep_if do |item|
1140
- text = item['note'] ? item['title'] + item['note'].join(' ') : item['title']
1141
- pattern = if opt[:search].strip =~ %r{^/.*?/$}
1142
- opt[:search].sub(%r{/(.*?)/}, '\1')
1143
- else
1144
- opt[:search].split('').join('.{0,3}')
1145
- end
1146
- text =~ /#{pattern}/i
1147
- end
1256
+ items.keep_if {|item| item.matches_search?(opt[:search]) }
1148
1257
  end
1149
1258
 
1150
1259
  if opt[:only_timed]
@@ -1173,9 +1282,10 @@ class WWID
1173
1282
 
1174
1283
  out = ''
1175
1284
 
1176
- raise 'Unknown output format' if opt[:output] && !(opt[:output] =~ /(template|html|csv|json|timeline)/)
1285
+ raise 'Unknown output format' if opt[:output] && (opt[:output] !~ /^(template|html|csv|json|timeline)$/i)
1177
1286
 
1178
- if opt[:output] == 'csv'
1287
+ case opt[:output]
1288
+ when /^csv$/i
1179
1289
  output = [CSV.generate_line(%w[date title note timer section])]
1180
1290
  items.each do |i|
1181
1291
  note = ''
@@ -1188,7 +1298,7 @@ class WWID
1188
1298
  output.push(CSV.generate_line([i['date'], i['title'], note, interval, i['section']]))
1189
1299
  end
1190
1300
  out = output.join('')
1191
- elsif opt[:output] == 'json' || opt[:output] == 'timeline'
1301
+ when /^(json|timeline)/i
1192
1302
  items_out = []
1193
1303
  max = items[-1]['date'].strftime('%F')
1194
1304
  min = items[0]['date'].strftime('%F')
@@ -1284,7 +1394,7 @@ class WWID
1284
1394
  EOTEMPLATE
1285
1395
  return template
1286
1396
  end
1287
- elsif opt[:output] == 'html'
1397
+ when /^html$/i
1288
1398
  page_title = section
1289
1399
  items_out = []
1290
1400
  items.each do |i|
@@ -1398,7 +1508,7 @@ class WWID
1398
1508
  else
1399
1509
  colors['default']
1400
1510
  end
1401
- output.gsub!(/\s(@[^ (]+)/, " #{colors[opt[:tags_color]]}\\1#{last_color}")
1511
+ output.gsub!(/(\s|m)(@[^ (]+)/, "\\1#{colors[opt[:tags_color]]}\\2#{last_color}")
1402
1512
  end
1403
1513
  output.sub!(/%note/, note)
1404
1514
  output.sub!(/%odnote/, note.gsub(/^\t*/, ''))
@@ -1413,7 +1523,7 @@ class WWID
1413
1523
  output.gsub!(/%n/, "\n")
1414
1524
  output.gsub!(/%t/, "\t")
1415
1525
 
1416
- out += output + "\n"
1526
+ out += "#{output}\n"
1417
1527
  end
1418
1528
  out += tag_times('text', opt[:sort_tags]) if opt[:totals]
1419
1529
  end
@@ -1474,7 +1584,8 @@ class WWID
1474
1584
 
1475
1585
  if tags && !tags.empty?
1476
1586
  items.delete_if do |item|
1477
- if bool =~ /(AND|ALL)/
1587
+ case bool
1588
+ when /(AND|ALL)/i
1478
1589
  score = 0
1479
1590
  tags.each do |tag|
1480
1591
  score += 1 if item['title'] =~ /@#{tag}/i
@@ -1482,14 +1593,14 @@ class WWID
1482
1593
  res = score < tags.length
1483
1594
  moved_items.push(item) if res
1484
1595
  res
1485
- elsif bool =~ /NONE/
1596
+ when /NONE/i
1486
1597
  del = false
1487
1598
  tags.each do |tag|
1488
1599
  del = true if item['title'] =~ /@#{tag}/i
1489
1600
  end
1490
1601
  moved_items.push(item) if del
1491
1602
  del
1492
- elsif bool =~ /(OR|ANY)/
1603
+ when /(OR|ANY)/i
1493
1604
  del = true
1494
1605
  tags.each do |tag|
1495
1606
  del = false if item['title'] =~ /@#{tag}/i
@@ -1499,7 +1610,7 @@ class WWID
1499
1610
  end
1500
1611
  end
1501
1612
  moved_items.each do |item|
1502
- if label && !(section == 'Currently')
1613
+ if label && section != 'Currently'
1503
1614
  item['title'] =
1504
1615
  item['title'].sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
1505
1616
  end
@@ -1508,23 +1619,27 @@ class WWID
1508
1619
  @content[destination]['items'] += items
1509
1620
  @results.push("Archived #{items.length} items from #{section} to #{destination}")
1510
1621
  else
1622
+ count = items.length if items.length < count
1511
1623
 
1512
- return if items.length < count
1513
-
1514
- @content[section]['items'] = if count == 0
1624
+ @content[section]['items'] = if count.zero?
1515
1625
  []
1516
1626
  else
1517
1627
  items[0..count - 1]
1518
1628
  end
1519
1629
 
1520
- items.each do |item|
1521
- if label && !(section == 'Currently')
1630
+ items.map! do |item|
1631
+ if label && section != 'Currently'
1522
1632
  item['title'] =
1523
1633
  item['title'].sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
1524
1634
  end
1635
+ item
1636
+ end
1637
+ if items.count > count
1638
+ @content[destination]['items'].concat(items[count..-1])
1639
+ else
1640
+ @content[destination]['items'].concat(items)
1525
1641
  end
1526
1642
 
1527
- @content[destination]['items'] += items[count..-1]
1528
1643
  @results.push("Archived #{items.length - count} items from #{section} to #{destination}")
1529
1644
  end
1530
1645
  end
@@ -1645,8 +1760,11 @@ class WWID
1645
1760
  cfg = @config['templates']['recent']
1646
1761
  section ||= @current_section
1647
1762
  section = guess_section(section)
1763
+
1648
1764
  list_section({ section: section, wrap_width: cfg['wrap_width'], count: count,
1649
- format: cfg['date_format'], template: cfg['template'], order: 'asc', times: times, totals: opt[:totals], sort_tags: opt[:sort_tags] })
1765
+ format: cfg['date_format'], template: cfg['template'],
1766
+ order: 'asc', times: times, totals: opt[:totals],
1767
+ sort_tags: opt[:sort_tags], tags_color: opt[:tags_color] })
1650
1768
  end
1651
1769
 
1652
1770
  ##
@@ -1655,12 +1773,29 @@ class WWID
1655
1773
  ## @param times (Bool) Show times
1656
1774
  ## @param section (String) Section to pull from, default Currently
1657
1775
  ##
1658
- def last(times = true, section = nil)
1659
- section ||= @current_section
1660
- section = guess_section(section)
1776
+ def last(times: true, section: nil, options: {})
1777
+ section = section.nil? || section =~ /all/i ? 'All' : guess_section(section)
1661
1778
  cfg = @config['templates']['last']
1662
- list_section({ section: section, wrap_width: cfg['wrap_width'], count: 1, format: cfg['date_format'],
1663
- template: cfg['template'], times: times })
1779
+
1780
+ opts = {
1781
+ section: section,
1782
+ wrap_width: cfg['wrap_width'],
1783
+ count: 1,
1784
+ format: cfg['date_format'],
1785
+ template: cfg['template'],
1786
+ times: times
1787
+ }
1788
+
1789
+ if options[:tag]
1790
+ opts[:tag_filter] = {
1791
+ 'tags' => options[:tag],
1792
+ 'bool' => options[:tag_bool]
1793
+ }
1794
+ end
1795
+
1796
+ opts[:search] = options[:search] if options[:search]
1797
+
1798
+ list_section(opts)
1664
1799
  end
1665
1800
 
1666
1801
  ##