doing 1.0.52 → 1.0.57

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -13
  3. data/bin/doing +751 -577
  4. data/lib/doing/version.rb +1 -1
  5. data/lib/doing/wwid.rb +221 -86
  6. metadata +2 -2
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '1.0.52'
2
+ VERSION = '1.0.57'
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
  ##