doing 1.0.52 → 1.0.57
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +35 -13
- data/bin/doing +751 -577
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +221 -86
- metadata +2 -2
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
|
##
|