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.
- checksums.yaml +4 -4
- data/README.md +35 -9
- data/bin/doing +754 -578
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +221 -86
- metadata +6 -6
data/lib/doing/version.rb
CHANGED
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]
|
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,
|
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 =
|
739
|
-
|
740
|
-
|
741
|
-
|
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
|
-
|
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]
|
795
|
-
title
|
796
|
-
|
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].
|
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
|
-
|
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
|
-
|
1117
|
-
|
1229
|
+
case opt[:tag_filter]['bool']
|
1230
|
+
when /(AND|ALL)/
|
1231
|
+
del = false
|
1118
1232
|
opt[:tag_filter]['tags'].each do |tag|
|
1119
|
-
|
1233
|
+
unless item['title'] =~ /@#{tag}/
|
1234
|
+
del = true
|
1235
|
+
break
|
1236
|
+
end
|
1120
1237
|
end
|
1121
|
-
|
1122
|
-
|
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
|
-
|
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
|
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] &&
|
1285
|
+
raise 'Unknown output format' if opt[:output] && (opt[:output] !~ /^(template|html|csv|json|timeline)$/i)
|
1177
1286
|
|
1178
|
-
|
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
|
-
|
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
|
-
|
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!(
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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 &&
|
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
|
-
|
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.
|
1521
|
-
if label &&
|
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'],
|
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
|
1659
|
-
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
|
-
|
1663
|
-
|
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
|
##
|