doing 2.0.5.pre → 2.0.9.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -1
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/bin/doing +446 -111
- data/doing.rdoc +371 -13
- data/generate_completions.sh +1 -0
- data/lib/completion/_doing.zsh +179 -127
- data/lib/completion/doing.bash +60 -27
- data/lib/completion/doing.fish +74 -23
- data/lib/doing/cli_status.rb +4 -0
- data/lib/doing/errors.rb +22 -15
- data/lib/doing/item.rb +18 -12
- data/lib/doing/log_adapter.rb +27 -25
- data/lib/doing/plugin_manager.rb +1 -1
- data/lib/doing/plugins/import/calendar_import.rb +7 -1
- data/lib/doing/plugins/import/doing_import.rb +6 -6
- data/lib/doing/plugins/import/timing_import.rb +7 -1
- data/lib/doing/string.rb +29 -6
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +120 -86
- data/lib/examples/commands/autotag.rb +63 -0
- data/lib/examples/plugins/{templates → wiki_export/templates}/wiki.css +0 -0
- data/lib/examples/plugins/{templates → wiki_export/templates}/wiki.haml +0 -0
- data/lib/examples/plugins/{templates → wiki_export/templates}/wiki_index.haml +0 -0
- data/lib/examples/plugins/{wiki_export.rb → wiki_export/wiki_export.rb} +0 -0
- data/scripts/generate_bash_completions.rb +3 -2
- data/scripts/generate_fish_completions.rb +4 -1
- data/scripts/generate_zsh_completions.rb +44 -39
- metadata +6 -6
- data/doing.fish +0 -278
data/lib/doing/wwid.rb
CHANGED
@@ -123,9 +123,9 @@ module Doing
|
|
123
123
|
## @param input (String) Text input for editor
|
124
124
|
##
|
125
125
|
def fork_editor(input = '')
|
126
|
-
# raise
|
126
|
+
# raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
127
127
|
|
128
|
-
raise
|
128
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Util.default_editor.nil?
|
129
129
|
|
130
130
|
tmpfile = Tempfile.new(['doing', '.md'])
|
131
131
|
|
@@ -173,11 +173,11 @@ module Doing
|
|
173
173
|
## @return (Array) [(String)title, (Note)note]
|
174
174
|
##
|
175
175
|
def format_input(input)
|
176
|
-
raise
|
176
|
+
raise EmptyInput, 'No content in entry' if input.nil? || input.strip.empty?
|
177
177
|
|
178
178
|
input_lines = input.split(/[\n\r]+/).delete_if(&:ignore?)
|
179
179
|
title = input_lines[0]&.strip
|
180
|
-
raise
|
180
|
+
raise EmptyInput, 'No content in first line' if title.nil? || title.strip.empty?
|
181
181
|
|
182
182
|
note = Note.new
|
183
183
|
note.add(input_lines[1..-1]) if input_lines.length > 1
|
@@ -209,7 +209,7 @@ module Doing
|
|
209
209
|
##
|
210
210
|
def chronify(input, future: false, guess: :begin)
|
211
211
|
now = Time.now
|
212
|
-
raise
|
212
|
+
raise InvalidTimeExpression, "Invalid time expression #{input.inspect}" if input.to_s.strip == ''
|
213
213
|
|
214
214
|
secs_ago = if input.match(/^(\d+)$/)
|
215
215
|
# plain number, assume minutes
|
@@ -277,8 +277,7 @@ module Doing
|
|
277
277
|
##
|
278
278
|
def add_section(title)
|
279
279
|
if @content.key?(title.cap_first)
|
280
|
-
|
281
|
-
return
|
280
|
+
raise InvalidSection, %(section "#{title.cap_first}" already exists)
|
282
281
|
end
|
283
282
|
|
284
283
|
@content[title.cap_first] = { :original => "#{title}:", :items => [] }
|
@@ -311,11 +310,13 @@ module Doing
|
|
311
310
|
unless section || guessed
|
312
311
|
alt = guess_view(frag, guessed: true, suggest: true)
|
313
312
|
if alt
|
314
|
-
meant_view = yn("Did you mean
|
315
|
-
|
313
|
+
meant_view = yn("#{Color.boldwhite}Did you mean `#{Color.yellow}doing view #{alt}#{Color.boldwhite}`?", default_response: 'n')
|
314
|
+
|
315
|
+
raise WrongCommand.new("run again with #{"doing view #{alt}".boldwhite}", topic: 'Try again:') if meant_view
|
316
|
+
|
316
317
|
end
|
317
318
|
|
318
|
-
res = yn("Section #{frag} not found, create it", default_response: 'n')
|
319
|
+
res = yn("#{Color.boldwhite}Section #{frag.yellow}#{Color.boldwhite} not found, create it", default_response: 'n')
|
319
320
|
|
320
321
|
if res
|
321
322
|
add_section(frag.cap_first)
|
@@ -323,7 +324,7 @@ module Doing
|
|
323
324
|
return frag.cap_first
|
324
325
|
end
|
325
326
|
|
326
|
-
raise
|
327
|
+
raise InvalidSection.new("unknown section #{frag.yellow}", topic: 'Missing:')
|
327
328
|
end
|
328
329
|
section ? section.cap_first : guessed
|
329
330
|
end
|
@@ -398,11 +399,12 @@ module Doing
|
|
398
399
|
break
|
399
400
|
end
|
400
401
|
unless view || guessed
|
401
|
-
|
402
|
-
|
402
|
+
alt = guess_section(frag, guessed: true, suggest: true)
|
403
|
+
meant_view = yn("Did you mean `doing show #{alt}`?", default_response: 'n')
|
403
404
|
|
404
|
-
raise
|
405
|
+
raise WrongCommand.new("run again with #{"doing show #{alt}".yellow}", topic: 'Try again:') if meant_view
|
405
406
|
|
407
|
+
raise InvalidView.new(%(unkown view #{alt.yellow}), topic: 'Missing:')
|
406
408
|
end
|
407
409
|
view
|
408
410
|
end
|
@@ -448,8 +450,8 @@ module Doing
|
|
448
450
|
end
|
449
451
|
|
450
452
|
items.push(entry)
|
451
|
-
logger.count(:added)
|
452
|
-
logger.
|
453
|
+
# logger.count(:added, level: :debug)
|
454
|
+
logger.info('New entry:', %(added "#{entry.title}" to #{section}))
|
453
455
|
end
|
454
456
|
|
455
457
|
##
|
@@ -471,8 +473,8 @@ module Doing
|
|
471
473
|
duped = no_overlap ? item.overlapping_time?(comp) : item.same_time?(comp)
|
472
474
|
break if duped
|
473
475
|
end
|
474
|
-
logger.count(:skipped, level: :debug, message: 'overlapping
|
475
|
-
logger.log_now(:debug, 'Skipped:', "overlapping entry: #{item.title}") if duped
|
476
|
+
logger.count(:skipped, level: :debug, message: '%count overlapping %items') if duped
|
477
|
+
# logger.log_now(:debug, 'Skipped:', "overlapping entry: #{item.title}") if duped
|
476
478
|
duped
|
477
479
|
end
|
478
480
|
end
|
@@ -509,7 +511,7 @@ module Doing
|
|
509
511
|
|
510
512
|
last_item = last_entry({ section: section })
|
511
513
|
|
512
|
-
raise
|
514
|
+
raise NoEntryError, 'No entry found' unless last_item
|
513
515
|
|
514
516
|
logger.log_now(:info, 'Edit note:', last_item.title)
|
515
517
|
|
@@ -522,7 +524,7 @@ module Doing
|
|
522
524
|
if resume
|
523
525
|
item.tag('done', remove: true)
|
524
526
|
end
|
525
|
-
|
527
|
+
logger.info('Reset:', %(Reset #{resume ? 'and resumed ' : ''} "#{item.title}" in #{item.section}))
|
526
528
|
item
|
527
529
|
end
|
528
530
|
|
@@ -550,7 +552,7 @@ module Doing
|
|
550
552
|
title, note = format_input(new_item)
|
551
553
|
|
552
554
|
if title.nil? || title.empty?
|
553
|
-
logger.
|
555
|
+
logger.warn('Skipped:', 'No content provided')
|
554
556
|
return
|
555
557
|
end
|
556
558
|
end
|
@@ -573,7 +575,7 @@ module Doing
|
|
573
575
|
|
574
576
|
last = last_entry(opt)
|
575
577
|
if last.nil?
|
576
|
-
logger.
|
578
|
+
logger.warn('Skipped:', 'No previous entry found')
|
577
579
|
return
|
578
580
|
end
|
579
581
|
|
@@ -684,18 +686,29 @@ module Doing
|
|
684
686
|
items.sort_by! { |item| [item.date, item.title.downcase] }.reverse
|
685
687
|
filtered_items = items.select do |item|
|
686
688
|
keep = true
|
687
|
-
|
688
|
-
|
689
|
+
if opt[:unfinished]
|
690
|
+
finished = item.tags?('done', :and)
|
691
|
+
finished = opt[:not] ? !finished : finished
|
692
|
+
keep = false if finished
|
693
|
+
end
|
689
694
|
|
690
695
|
if keep && opt[:tag]
|
691
696
|
opt[:tag_bool] ||= :and
|
692
697
|
tag_match = opt[:tag].nil? || opt[:tag].empty? ? true : item.tags?(opt[:tag], opt[:tag_bool])
|
693
698
|
keep = false unless tag_match
|
699
|
+
keep = opt[:not] ? !keep : keep
|
694
700
|
end
|
695
701
|
|
696
702
|
if keep && opt[:search]
|
697
|
-
|
703
|
+
opt[:case] = opt[:case].normalize_case unless opt[:case].is_a?(Symbol)
|
704
|
+
search_match = if opt[:search].nil? || opt[:search].empty?
|
705
|
+
true
|
706
|
+
else
|
707
|
+
item.search(opt[:search], case_type: opt[:case])
|
708
|
+
end
|
709
|
+
|
698
710
|
keep = false unless search_match
|
711
|
+
keep = opt[:not] ? !keep : keep
|
699
712
|
end
|
700
713
|
|
701
714
|
if keep && opt[:date_filter]&.length == 2
|
@@ -708,30 +721,36 @@ module Doing
|
|
708
721
|
item.date.strftime('%F') == start_date.strftime('%F')
|
709
722
|
end
|
710
723
|
keep = false unless in_date_range
|
724
|
+
keep = opt[:not] ? !keep : keep
|
711
725
|
end
|
712
726
|
|
713
727
|
keep = false if keep && opt[:only_timed] && !item.interval
|
714
728
|
|
715
729
|
if keep && opt[:tag_filter] && !opt[:tag_filter]['tags'].empty?
|
716
730
|
keep = item.tags?(opt[:tag_filter]['tags'], opt[:tag_filter]['bool'])
|
731
|
+
keep = opt[:not] ? !keep : keep
|
717
732
|
end
|
718
733
|
|
719
734
|
if keep && opt[:before]
|
720
735
|
time_string = opt[:before]
|
721
736
|
cutoff = chronify(time_string, guess: :begin)
|
722
737
|
keep = cutoff && item.date <= cutoff
|
738
|
+
keep = opt[:not] ? !keep : keep
|
723
739
|
end
|
724
740
|
|
725
741
|
if keep && opt[:after]
|
726
742
|
time_string = opt[:after]
|
727
743
|
cutoff = chronify(time_string, guess: :end)
|
728
744
|
keep = cutoff && item.date >= cutoff
|
745
|
+
keep = opt[:not] ? !keep : keep
|
729
746
|
end
|
730
747
|
|
731
748
|
if keep && opt[:today]
|
732
749
|
keep = item.date >= Date.today.to_time && item.date < Date.today.next_day.to_time
|
750
|
+
keep = opt[:not] ? !keep : keep
|
733
751
|
elsif keep && opt[:yesterday]
|
734
752
|
keep = item.date >= Date.today.prev_day.to_time && item.date < Date.today.to_time
|
753
|
+
keep = opt[:not] ? !keep : keep
|
735
754
|
end
|
736
755
|
|
737
756
|
keep
|
@@ -753,16 +772,23 @@ module Doing
|
|
753
772
|
##
|
754
773
|
def interactive(opt = {})
|
755
774
|
section = opt[:section] ? guess_section(opt[:section]) : 'All'
|
775
|
+
|
776
|
+
search = nil
|
777
|
+
|
778
|
+
if opt[:search]
|
779
|
+
search = opt[:search]
|
780
|
+
search.sub!(/^'?/, "'") if opt[:exact]
|
781
|
+
opt[:search] = search
|
782
|
+
end
|
783
|
+
|
756
784
|
opt[:query] = opt[:search] if opt[:search] && !opt[:query]
|
785
|
+
opt[:query] = "!#{opt[:query]}" if opt[:not]
|
757
786
|
opt[:multiple] = true
|
758
|
-
items = filter_items([], opt: { section: section, search: opt[:search] })
|
787
|
+
items = filter_items([], opt: { section: section, search: opt[:search], case: opt[:case] })
|
759
788
|
|
760
789
|
selection = choose_from_items(items, opt, include_section: section =~ /^all$/i)
|
761
790
|
|
762
|
-
if selection.empty?
|
763
|
-
logger.debug('Skipped:', 'No selection')
|
764
|
-
return
|
765
|
-
end
|
791
|
+
raise NoResults, 'no items selected' if selection.empty?
|
766
792
|
|
767
793
|
act_on(selection, opt)
|
768
794
|
end
|
@@ -807,7 +833,7 @@ module Doing
|
|
807
833
|
fzf_args.push('-1') unless opt[:show_if_single]
|
808
834
|
|
809
835
|
unless opt[:menu]
|
810
|
-
raise
|
836
|
+
raise InvalidArgument, "Can't skip menu when no query is provided" unless opt[:query] && !opt[:query].empty?
|
811
837
|
|
812
838
|
fzf_args.concat([%(--filter="#{opt[:query]}"), opt[:sort] ? '' : '--no-sort'])
|
813
839
|
end
|
@@ -823,8 +849,10 @@ module Doing
|
|
823
849
|
end
|
824
850
|
|
825
851
|
def act_on(items, opt = {})
|
826
|
-
actions = %i[editor delete tag flag finish cancel archive output save_to]
|
852
|
+
actions = %i[editor delete tag flag finish cancel archive output save_to again resume]
|
827
853
|
has_action = false
|
854
|
+
single = items.count == 1
|
855
|
+
|
828
856
|
actions.each do |a|
|
829
857
|
if opt[a]
|
830
858
|
has_action = true
|
@@ -864,7 +892,7 @@ module Doing
|
|
864
892
|
opt[:reset] = true
|
865
893
|
when /(add|remove) tag/
|
866
894
|
type = action =~ /^add/ ? 'add' : 'remove'
|
867
|
-
raise
|
895
|
+
raise InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
|
868
896
|
|
869
897
|
print "#{Color.yellow}Tag to #{type}: #{Color.reset}"
|
870
898
|
tag = $stdin.gets
|
@@ -879,7 +907,7 @@ module Doing
|
|
879
907
|
next if tag =~ /^ *$/
|
880
908
|
|
881
909
|
unless output_format
|
882
|
-
raise
|
910
|
+
raise UserCancelled, 'Cancelled'
|
883
911
|
end
|
884
912
|
|
885
913
|
opt[:output] = output_format.strip
|
@@ -912,7 +940,7 @@ module Doing
|
|
912
940
|
|
913
941
|
if opt[:resume] || opt[:reset]
|
914
942
|
if items.count > 1
|
915
|
-
|
943
|
+
raise InvalidArgument, 'resume and restart can only be used on a single entry'
|
916
944
|
else
|
917
945
|
item = items[0]
|
918
946
|
if opt[:resume] && !opt[:reset]
|
@@ -933,7 +961,7 @@ module Doing
|
|
933
961
|
if opt[:delete]
|
934
962
|
res = opt[:force] ? true : yn("Delete #{items.size} items?", default_response: 'y')
|
935
963
|
if res
|
936
|
-
items.each { |item| delete_item(item) }
|
964
|
+
items.each { |item| delete_item(item, single: items.count == 1) }
|
937
965
|
write(@doing_file)
|
938
966
|
end
|
939
967
|
return
|
@@ -942,7 +970,7 @@ module Doing
|
|
942
970
|
if opt[:flag]
|
943
971
|
tag = @config['marker_tag'] || 'flagged'
|
944
972
|
items.map! do |item|
|
945
|
-
tag_item(item, tag, date: false, remove: opt[:remove])
|
973
|
+
tag_item(item, tag, date: false, remove: opt[:remove], single: single)
|
946
974
|
end
|
947
975
|
end
|
948
976
|
|
@@ -951,7 +979,7 @@ module Doing
|
|
951
979
|
items.map! do |item|
|
952
980
|
if item.should_finish?
|
953
981
|
should_date = !opt[:cancel] && item.should_time?
|
954
|
-
tag_item(item, tag, date: should_date, remove: opt[:remove])
|
982
|
+
tag_item(item, tag, date: should_date, remove: opt[:remove], single: single)
|
955
983
|
end
|
956
984
|
end
|
957
985
|
end
|
@@ -959,7 +987,7 @@ module Doing
|
|
959
987
|
if opt[:tag]
|
960
988
|
tag = opt[:tag]
|
961
989
|
items.map! do |item|
|
962
|
-
tag_item(item, tag, date: false, remove: opt[:remove])
|
990
|
+
tag_item(item, tag, date: false, remove: opt[:remove], single: single)
|
963
991
|
end
|
964
992
|
end
|
965
993
|
|
@@ -991,7 +1019,7 @@ module Doing
|
|
991
1019
|
title = input_lines[0]&.strip
|
992
1020
|
|
993
1021
|
if title.nil? || title =~ /^#{divider.strip}$/ || title.strip.empty?
|
994
|
-
delete_item(items[i])
|
1022
|
+
delete_item(items[i], single: new_items.count == 1)
|
995
1023
|
else
|
996
1024
|
note = input_lines.length > 1 ? input_lines[1..-1] : []
|
997
1025
|
|
@@ -1057,7 +1085,7 @@ module Doing
|
|
1057
1085
|
## @param remove (Boolean) remove tags
|
1058
1086
|
## @param date (Boolean) Include timestamp?
|
1059
1087
|
##
|
1060
|
-
def tag_item(item, tags, remove: false, date: false)
|
1088
|
+
def tag_item(item, tags, remove: false, date: false, single: false)
|
1061
1089
|
added = []
|
1062
1090
|
removed = []
|
1063
1091
|
|
@@ -1073,7 +1101,7 @@ module Doing
|
|
1073
1101
|
end
|
1074
1102
|
end
|
1075
1103
|
|
1076
|
-
log_change(tags_added: added, tags_removed: removed, count: 1)
|
1104
|
+
log_change(tags_added: added, tags_removed: removed, count: 1, item: item, single: single)
|
1077
1105
|
|
1078
1106
|
item
|
1079
1107
|
end
|
@@ -1097,21 +1125,22 @@ module Doing
|
|
1097
1125
|
|
1098
1126
|
items = filter_items([], opt: opt)
|
1099
1127
|
|
1100
|
-
logger.info('Skipped:', 'no items matched your search') if items.empty?
|
1101
|
-
|
1102
1128
|
if opt[:interactive]
|
1103
1129
|
items = choose_from_items(items, {
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
1130
|
+
menu: true,
|
1131
|
+
header: '',
|
1132
|
+
prompt: 'Select entries to tag > ',
|
1133
|
+
multiple: true,
|
1134
|
+
sort: true,
|
1135
|
+
show_if_single: true
|
1136
|
+
}, include_section: opt[:section] =~ /^all$/i)
|
1137
|
+
|
1138
|
+
raise NoResults, 'no items selected' if items.empty?
|
1111
1139
|
|
1112
|
-
return if items.nil?
|
1113
1140
|
end
|
1114
1141
|
|
1142
|
+
raise NoResults, 'no items matched your search' if items.empty?
|
1143
|
+
|
1115
1144
|
items.each do |item|
|
1116
1145
|
added = []
|
1117
1146
|
removed = []
|
@@ -1123,7 +1152,7 @@ module Doing
|
|
1123
1152
|
# logger.debug('Autotag:', 'No changes')
|
1124
1153
|
else
|
1125
1154
|
logger.count(:added_tags)
|
1126
|
-
logger.
|
1155
|
+
logger.write(items.count == 1 ? :info : :debug, 'Tagged:', new_title)
|
1127
1156
|
item.title = new_title
|
1128
1157
|
end
|
1129
1158
|
else
|
@@ -1185,7 +1214,7 @@ module Doing
|
|
1185
1214
|
end
|
1186
1215
|
end
|
1187
1216
|
|
1188
|
-
log_change(tags_added: added, tags_removed: removed)
|
1217
|
+
log_change(tags_added: added, tags_removed: removed, item: item, single: items.count == 1)
|
1189
1218
|
|
1190
1219
|
item.note.add(opt[:note]) if opt[:note]
|
1191
1220
|
|
@@ -1217,7 +1246,7 @@ module Doing
|
|
1217
1246
|
@content[section][:items].concat([new_item])
|
1218
1247
|
|
1219
1248
|
logger.count(section == 'Archive' ? :archived : :moved)
|
1220
|
-
logger.debug("
|
1249
|
+
logger.debug("#{section == 'Archive' ? 'Archived' : 'Moved'}:",
|
1221
1250
|
"#{new_item.title.truncate(60)} from #{from} to #{section}")
|
1222
1251
|
new_item
|
1223
1252
|
end
|
@@ -1240,13 +1269,13 @@ module Doing
|
|
1240
1269
|
##
|
1241
1270
|
## @param item The item
|
1242
1271
|
##
|
1243
|
-
def delete_item(item)
|
1272
|
+
def delete_item(item, single: false)
|
1244
1273
|
section = item.section
|
1245
1274
|
|
1246
1275
|
section_items = @content[section][:items]
|
1247
1276
|
deleted = section_items.delete(item)
|
1248
1277
|
logger.count(:deleted)
|
1249
|
-
logger.
|
1278
|
+
logger.info('Entry deleted:', deleted.title) if single
|
1250
1279
|
end
|
1251
1280
|
|
1252
1281
|
##
|
@@ -1261,16 +1290,13 @@ module Doing
|
|
1261
1290
|
section_items = @content[section][:items]
|
1262
1291
|
s_idx = section_items.index { |item| item.equal?(old_item) }
|
1263
1292
|
|
1264
|
-
unless s_idx
|
1265
|
-
Doing.logger.error('Fail to update:', 'Could not find item in index')
|
1266
|
-
raise Errors::ItemNotFound, 'Unable to find item in index, did it mutate?'
|
1267
|
-
end
|
1293
|
+
raise ItemNotFound, 'Unable to find item in index, did it mutate?' unless s_idx
|
1268
1294
|
|
1269
1295
|
return if section_items[s_idx].equal?(new_item)
|
1270
1296
|
|
1271
1297
|
section_items[s_idx] = new_item
|
1272
1298
|
logger.count(:updated)
|
1273
|
-
logger.
|
1299
|
+
logger.info('Entry updated:', section_items[s_idx].title.truncate(60))
|
1274
1300
|
new_item
|
1275
1301
|
end
|
1276
1302
|
|
@@ -1343,10 +1369,10 @@ module Doing
|
|
1343
1369
|
item.title = item.title.sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{item.section})")
|
1344
1370
|
move_item(item, 'Archive', label: false)
|
1345
1371
|
logger.count(:completed_archived)
|
1346
|
-
logger.
|
1372
|
+
logger.info('Completed/archived:', item.title)
|
1347
1373
|
else
|
1348
1374
|
logger.count(:completed)
|
1349
|
-
logger.
|
1375
|
+
logger.info('Completed:', item.title)
|
1350
1376
|
end
|
1351
1377
|
end
|
1352
1378
|
|
@@ -1478,10 +1504,10 @@ module Doing
|
|
1478
1504
|
if File.exist?(file)
|
1479
1505
|
init_doing_file(file)
|
1480
1506
|
@content.deep_merge(new_content)
|
1481
|
-
logger.warn('File update:', "
|
1507
|
+
logger.warn('File update:', "added entries to existing file: #{file}")
|
1482
1508
|
else
|
1483
1509
|
@content = new_content
|
1484
|
-
logger.warn('File update:', "
|
1510
|
+
logger.warn('File update:', "created new file: #{file}")
|
1485
1511
|
end
|
1486
1512
|
|
1487
1513
|
write(file, backup: false)
|
@@ -1565,17 +1591,13 @@ module Doing
|
|
1565
1591
|
|
1566
1592
|
items.reverse! if opt[:order] =~ /^d/i
|
1567
1593
|
|
1568
|
-
|
1569
1594
|
if opt[:interactive]
|
1570
1595
|
opt[:menu] = !opt[:force]
|
1571
1596
|
opt[:query] = '' # opt[:search]
|
1572
1597
|
opt[:multiple] = true
|
1573
1598
|
selected = choose_from_items(items, opt, include_section: opt[:section] =~ /^all$/i )
|
1574
1599
|
|
1575
|
-
if selected.empty?
|
1576
|
-
logger.debug('Skipped:', 'No selection')
|
1577
|
-
return
|
1578
|
-
end
|
1600
|
+
raise NoResults, 'no items selected' if selected.empty?
|
1579
1601
|
|
1580
1602
|
act_on(selected, opt)
|
1581
1603
|
return
|
@@ -1592,7 +1614,7 @@ module Doing
|
|
1592
1614
|
def output(items, title, is_single, opt = {})
|
1593
1615
|
out = nil
|
1594
1616
|
|
1595
|
-
raise
|
1617
|
+
raise InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
|
1596
1618
|
|
1597
1619
|
export_options = { page_title: title, is_single: is_single, options: opt }
|
1598
1620
|
|
@@ -1646,7 +1668,7 @@ module Doing
|
|
1646
1668
|
do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label], before: options[:before] })
|
1647
1669
|
write(doing_file)
|
1648
1670
|
else
|
1649
|
-
raise
|
1671
|
+
raise InvalidArgument, 'Either source or destination does not exist'
|
1650
1672
|
end
|
1651
1673
|
end
|
1652
1674
|
|
@@ -1705,7 +1727,12 @@ module Doing
|
|
1705
1727
|
@content[section][:items] = items
|
1706
1728
|
@content[destination][:items].concat(moved_items)
|
1707
1729
|
if moved_items.length.positive?
|
1708
|
-
logger.
|
1730
|
+
logger.count(destination == 'Archive' ? :archived : :moved,
|
1731
|
+
level: :info,
|
1732
|
+
count: moved_items.length,
|
1733
|
+
message: "%count %items from #{section} to #{destination}")
|
1734
|
+
else
|
1735
|
+
logger.info('Skipped:', 'No items were moved')
|
1709
1736
|
end
|
1710
1737
|
else
|
1711
1738
|
count = items.length if items.length < count
|
@@ -1733,10 +1760,11 @@ module Doing
|
|
1733
1760
|
else
|
1734
1761
|
items[0..count - 1]
|
1735
1762
|
end
|
1763
|
+
|
1736
1764
|
logger.count(destination == 'Archive' ? :archived : :moved,
|
1765
|
+
level: :info,
|
1737
1766
|
count: items.length - count,
|
1738
1767
|
message: "%count %items from #{section} to #{destination}")
|
1739
|
-
# logger.info('Archived:', "#{items.length - count} items from #{section} to #{destination}")
|
1740
1768
|
end
|
1741
1769
|
end
|
1742
1770
|
end
|
@@ -1873,7 +1901,8 @@ module Doing
|
|
1873
1901
|
end
|
1874
1902
|
|
1875
1903
|
opts[:search] = options[:search] if options[:search]
|
1876
|
-
|
1904
|
+
opts[:case] = options[:case]
|
1905
|
+
opts[:not] = options[:negate]
|
1877
1906
|
list_section(opts)
|
1878
1907
|
end
|
1879
1908
|
|
@@ -1935,11 +1964,11 @@ module Doing
|
|
1935
1964
|
end
|
1936
1965
|
end
|
1937
1966
|
|
1938
|
-
logger.debug('Autotag:', "
|
1967
|
+
logger.debug('Autotag:', "whitelisted tags: #{whitelisted.join(', ')}") unless whitelisted.empty?
|
1939
1968
|
new_tags = whitelisted
|
1940
1969
|
unless tail_tags.empty?
|
1941
1970
|
tags = tail_tags.uniq.map { |t| "@#{t}".cyan }.join(' ')
|
1942
|
-
logger.debug('Autotag:', "
|
1971
|
+
logger.debug('Autotag:', "synonym tags: #{tags}")
|
1943
1972
|
tags_a = tail_tags.map { |t| "@#{t}" }
|
1944
1973
|
text.add_tags!(tags_a.join(' '))
|
1945
1974
|
new_tags.concat(tags_a)
|
@@ -1948,7 +1977,7 @@ module Doing
|
|
1948
1977
|
unless text == original
|
1949
1978
|
logger.info('Autotag:', "added #{new_tags.join(', ')} to \"#{text}\"")
|
1950
1979
|
else
|
1951
|
-
logger.debug('
|
1980
|
+
logger.debug('Skipped:', "no change to \"#{text}\"")
|
1952
1981
|
end
|
1953
1982
|
|
1954
1983
|
text
|
@@ -2046,7 +2075,7 @@ EOS
|
|
2046
2075
|
(max - k.length).times do
|
2047
2076
|
spacer += ' '
|
2048
2077
|
end
|
2049
|
-
|
2078
|
+
_d, h, m = format_time(v, human: true)
|
2050
2079
|
output.push("┃ #{spacer}#{k}:#{format('%<h> 4dh %<m>02dm', h: h, m: m)} ┃")
|
2051
2080
|
end
|
2052
2081
|
|
@@ -2174,23 +2203,28 @@ EOS
|
|
2174
2203
|
logger.log_now(:error, 'STDERR output:', stderr)
|
2175
2204
|
end
|
2176
2205
|
|
2177
|
-
def log_change(tags_added: [], tags_removed: [], count: 1)
|
2206
|
+
def log_change(tags_added: [], tags_removed: [], count: 1, item: nil, single: false)
|
2178
2207
|
if tags_added.empty? && tags_removed.empty?
|
2179
2208
|
logger.count(:skipped, level: :debug, message: '%count %items with no change', count: count)
|
2180
2209
|
else
|
2181
|
-
|
2182
2210
|
if tags_added.empty?
|
2183
2211
|
logger.count(:skipped, level: :debug, message: 'no tags added to %count %items')
|
2184
|
-
# logger.debug('No tags added:', %("#{item.title}" in #{item.section}))
|
2185
2212
|
else
|
2186
|
-
|
2187
|
-
|
2213
|
+
if single && item
|
2214
|
+
logger.info('Tagged:', %(added #{tags_added.count == 1 ? 'tag' : 'tags'} #{tags_added.map {|t| "@#{t}"}.join(', ')} to #{item.title}))
|
2215
|
+
else
|
2216
|
+
logger.count(:added_tags, level: :info, tag: tags_added, message: '%tags added to %count %items')
|
2217
|
+
end
|
2188
2218
|
end
|
2189
2219
|
|
2190
2220
|
if tags_removed.empty?
|
2191
2221
|
logger.count(:skipped, level: :debug, message: 'no tags removed from %count %items')
|
2192
2222
|
else
|
2193
|
-
|
2223
|
+
if single && item
|
2224
|
+
logger.info('Untagged:', %(removed #{tags_removed.count == 1 ? 'tag' : 'tags'} #{tags_added.map {|t| "@#{t}"}.join(', ')} from #{item.title}))
|
2225
|
+
else
|
2226
|
+
logger.count(:removed_tags, level: :info, tag: tags_removed, message: '%tags removed from %count %items')
|
2227
|
+
end
|
2194
2228
|
end
|
2195
2229
|
end
|
2196
2230
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Example command that calls an existing command (tag) with
|
4
|
+
# preset options
|
5
|
+
desc 'Autotag last entry or filtered entries'
|
6
|
+
command :autotag do |c|
|
7
|
+
# Preserve some switches and flags. Values will be passed
|
8
|
+
# to tag command.
|
9
|
+
c.desc 'Section'
|
10
|
+
c.arg_name 'SECTION_NAME'
|
11
|
+
c.flag %i[s section], default_value: 'All'
|
12
|
+
|
13
|
+
c.desc 'How many recent entries to autotag (0 for all)'
|
14
|
+
c.arg_name 'COUNT'
|
15
|
+
c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
|
16
|
+
|
17
|
+
c.desc 'Don\'t ask permission to autotag all entries when count is 0'
|
18
|
+
c.switch %i[force], negatable: false, default_value: false
|
19
|
+
|
20
|
+
c.desc 'Autotag last entry (or entries) not marked @done'
|
21
|
+
c.switch %i[u unfinished], negatable: false, default_value: false
|
22
|
+
|
23
|
+
c.desc 'Autotag the last X entries containing TAG.
|
24
|
+
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
|
25
|
+
c.arg_name 'TAG'
|
26
|
+
c.flag [:tag]
|
27
|
+
|
28
|
+
c.desc 'Autotag entries matching search filter,
|
29
|
+
surround with slashes for regex (e.g. "/query.*/"),
|
30
|
+
start with single quote for exact match ("\'query")'
|
31
|
+
c.arg_name 'QUERY'
|
32
|
+
c.flag [:search]
|
33
|
+
|
34
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
35
|
+
c.arg_name 'BOOLEAN'
|
36
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
37
|
+
|
38
|
+
c.desc 'Select item(s) to tag from a menu of matching entries'
|
39
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
40
|
+
|
41
|
+
c.action do |global, options, _args|
|
42
|
+
# Force some switches and flags. We're using the tag
|
43
|
+
# command with settings that would invoke autotagging.
|
44
|
+
|
45
|
+
# Force enable autotag
|
46
|
+
options[:a] = true
|
47
|
+
options[:autotag] = true
|
48
|
+
|
49
|
+
# No need for date values
|
50
|
+
options[:d] = false
|
51
|
+
options[:date] = false
|
52
|
+
|
53
|
+
# Don't remove any tags
|
54
|
+
options[:rename] = nil
|
55
|
+
options[:regex] = false
|
56
|
+
options[:r] = false
|
57
|
+
options[:remove] = false
|
58
|
+
|
59
|
+
cmd = commands[:tag]
|
60
|
+
action = cmd.send(:get_action, nil)
|
61
|
+
action.call(global, options, [])
|
62
|
+
end
|
63
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|