doing 2.0.3.pre → 2.0.8.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 +40 -1
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/bin/doing +316 -114
- data/doing.rdoc +244 -19
- data/example_plugin.rb +1 -1
- 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/configuration.rb +2 -0
- data/lib/doing/errors.rb +22 -15
- data/lib/doing/item.rb +12 -11
- data/lib/doing/log_adapter.rb +27 -25
- data/lib/doing/plugin_manager.rb +1 -1
- data/lib/doing/plugins/export/json_export.rb +2 -2
- data/lib/doing/plugins/export/template_export.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 +9 -7
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +160 -92
- data/lib/examples/commands/autotag.rb +63 -0
- data/lib/examples/commands/wiki.rb +1 -0
- data/lib/examples/plugins/say_export.rb +1 -1
- 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 +7 -7
- 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,20 +686,26 @@ 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
|
search_match = opt[:search].nil? || opt[:search].empty? ? true : item.search(opt[:search])
|
698
704
|
keep = false unless search_match
|
705
|
+
keep = opt[:not] ? !keep : keep
|
699
706
|
end
|
700
707
|
|
708
|
+
|
701
709
|
if keep && opt[:date_filter]&.length == 2
|
702
710
|
start_date = opt[:date_filter][0]
|
703
711
|
end_date = opt[:date_filter][1]
|
@@ -708,30 +716,36 @@ module Doing
|
|
708
716
|
item.date.strftime('%F') == start_date.strftime('%F')
|
709
717
|
end
|
710
718
|
keep = false unless in_date_range
|
719
|
+
keep = opt[:not] ? !keep : keep
|
711
720
|
end
|
712
721
|
|
713
722
|
keep = false if keep && opt[:only_timed] && !item.interval
|
714
723
|
|
715
724
|
if keep && opt[:tag_filter] && !opt[:tag_filter]['tags'].empty?
|
716
725
|
keep = item.tags?(opt[:tag_filter]['tags'], opt[:tag_filter]['bool'])
|
726
|
+
keep = opt[:not] ? !keep : keep
|
717
727
|
end
|
718
728
|
|
719
729
|
if keep && opt[:before]
|
720
730
|
time_string = opt[:before]
|
721
731
|
cutoff = chronify(time_string, guess: :begin)
|
722
732
|
keep = cutoff && item.date <= cutoff
|
733
|
+
keep = opt[:not] ? !keep : keep
|
723
734
|
end
|
724
735
|
|
725
736
|
if keep && opt[:after]
|
726
737
|
time_string = opt[:after]
|
727
738
|
cutoff = chronify(time_string, guess: :end)
|
728
739
|
keep = cutoff && item.date >= cutoff
|
740
|
+
keep = opt[:not] ? !keep : keep
|
729
741
|
end
|
730
742
|
|
731
743
|
if keep && opt[:today]
|
732
744
|
keep = item.date >= Date.today.to_time && item.date < Date.today.next_day.to_time
|
745
|
+
keep = opt[:not] ? !keep : keep
|
733
746
|
elsif keep && opt[:yesterday]
|
734
747
|
keep = item.date >= Date.today.prev_day.to_time && item.date < Date.today.to_time
|
748
|
+
keep = opt[:not] ? !keep : keep
|
735
749
|
end
|
736
750
|
|
737
751
|
keep
|
@@ -753,16 +767,24 @@ module Doing
|
|
753
767
|
##
|
754
768
|
def interactive(opt = {})
|
755
769
|
section = opt[:section] ? guess_section(opt[:section]) : 'All'
|
770
|
+
|
771
|
+
search = nil
|
772
|
+
|
773
|
+
if opt[:search]
|
774
|
+
search = opt[:search]
|
775
|
+
search.sub!(/^'?/, "'") if opt[:exact]
|
776
|
+
search.downcase! if opt[:case] == false
|
777
|
+
opt[:search] = search
|
778
|
+
end
|
779
|
+
|
756
780
|
opt[:query] = opt[:search] if opt[:search] && !opt[:query]
|
781
|
+
opt[:query] = "!#{opt[:query]}" if opt[:not]
|
757
782
|
opt[:multiple] = true
|
758
783
|
items = filter_items([], opt: { section: section, search: opt[:search] })
|
759
784
|
|
760
785
|
selection = choose_from_items(items, opt, include_section: section =~ /^all$/i)
|
761
786
|
|
762
|
-
if selection.empty?
|
763
|
-
logger.debug('Skipped:', 'No selection')
|
764
|
-
return
|
765
|
-
end
|
787
|
+
raise NoResults, 'no items selected' if selection.empty?
|
766
788
|
|
767
789
|
act_on(selection, opt)
|
768
790
|
end
|
@@ -807,7 +829,7 @@ module Doing
|
|
807
829
|
fzf_args.push('-1') unless opt[:show_if_single]
|
808
830
|
|
809
831
|
unless opt[:menu]
|
810
|
-
raise
|
832
|
+
raise InvalidArgument, "Can't skip menu when no query is provided" unless opt[:query] && !opt[:query].empty?
|
811
833
|
|
812
834
|
fzf_args.concat([%(--filter="#{opt[:query]}"), opt[:sort] ? '' : '--no-sort'])
|
813
835
|
end
|
@@ -823,8 +845,10 @@ module Doing
|
|
823
845
|
end
|
824
846
|
|
825
847
|
def act_on(items, opt = {})
|
826
|
-
actions = %i[editor delete tag flag finish cancel archive output save_to]
|
848
|
+
actions = %i[editor delete tag flag finish cancel archive output save_to again resume]
|
827
849
|
has_action = false
|
850
|
+
single = items.count == 1
|
851
|
+
|
828
852
|
actions.each do |a|
|
829
853
|
if opt[a]
|
830
854
|
has_action = true
|
@@ -864,7 +888,7 @@ module Doing
|
|
864
888
|
opt[:reset] = true
|
865
889
|
when /(add|remove) tag/
|
866
890
|
type = action =~ /^add/ ? 'add' : 'remove'
|
867
|
-
raise
|
891
|
+
raise InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
|
868
892
|
|
869
893
|
print "#{Color.yellow}Tag to #{type}: #{Color.reset}"
|
870
894
|
tag = $stdin.gets
|
@@ -879,7 +903,7 @@ module Doing
|
|
879
903
|
next if tag =~ /^ *$/
|
880
904
|
|
881
905
|
unless output_format
|
882
|
-
raise
|
906
|
+
raise UserCancelled, 'Cancelled'
|
883
907
|
end
|
884
908
|
|
885
909
|
opt[:output] = output_format.strip
|
@@ -912,7 +936,7 @@ module Doing
|
|
912
936
|
|
913
937
|
if opt[:resume] || opt[:reset]
|
914
938
|
if items.count > 1
|
915
|
-
|
939
|
+
raise InvalidArgument, 'resume and restart can only be used on a single entry'
|
916
940
|
else
|
917
941
|
item = items[0]
|
918
942
|
if opt[:resume] && !opt[:reset]
|
@@ -942,7 +966,7 @@ module Doing
|
|
942
966
|
if opt[:flag]
|
943
967
|
tag = @config['marker_tag'] || 'flagged'
|
944
968
|
items.map! do |item|
|
945
|
-
tag_item(item, tag, date: false, remove: opt[:remove])
|
969
|
+
tag_item(item, tag, date: false, remove: opt[:remove], single: single)
|
946
970
|
end
|
947
971
|
end
|
948
972
|
|
@@ -951,7 +975,7 @@ module Doing
|
|
951
975
|
items.map! do |item|
|
952
976
|
if item.should_finish?
|
953
977
|
should_date = !opt[:cancel] && item.should_time?
|
954
|
-
tag_item(item, tag, date: should_date, remove: opt[:remove])
|
978
|
+
tag_item(item, tag, date: should_date, remove: opt[:remove], single: single)
|
955
979
|
end
|
956
980
|
end
|
957
981
|
end
|
@@ -959,7 +983,7 @@ module Doing
|
|
959
983
|
if opt[:tag]
|
960
984
|
tag = opt[:tag]
|
961
985
|
items.map! do |item|
|
962
|
-
tag_item(item, tag, date: false, remove: opt[:remove])
|
986
|
+
tag_item(item, tag, date: false, remove: opt[:remove], single: single)
|
963
987
|
end
|
964
988
|
end
|
965
989
|
|
@@ -1057,7 +1081,7 @@ module Doing
|
|
1057
1081
|
## @param remove (Boolean) remove tags
|
1058
1082
|
## @param date (Boolean) Include timestamp?
|
1059
1083
|
##
|
1060
|
-
def tag_item(item, tags, remove: false, date: false)
|
1084
|
+
def tag_item(item, tags, remove: false, date: false, single: false)
|
1061
1085
|
added = []
|
1062
1086
|
removed = []
|
1063
1087
|
|
@@ -1073,7 +1097,7 @@ module Doing
|
|
1073
1097
|
end
|
1074
1098
|
end
|
1075
1099
|
|
1076
|
-
log_change(tags_added: added, tags_removed: removed, count: 1)
|
1100
|
+
log_change(tags_added: added, tags_removed: removed, count: 1, item: item, single: single)
|
1077
1101
|
|
1078
1102
|
item
|
1079
1103
|
end
|
@@ -1097,21 +1121,22 @@ module Doing
|
|
1097
1121
|
|
1098
1122
|
items = filter_items([], opt: opt)
|
1099
1123
|
|
1100
|
-
logger.info('Skipped:', 'no items matched your search') if items.empty?
|
1101
|
-
|
1102
1124
|
if opt[:interactive]
|
1103
1125
|
items = choose_from_items(items, {
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
1126
|
+
menu: true,
|
1127
|
+
header: '',
|
1128
|
+
prompt: 'Select entries to tag > ',
|
1129
|
+
multiple: true,
|
1130
|
+
sort: true,
|
1131
|
+
show_if_single: true
|
1132
|
+
}, include_section: opt[:section] =~ /^all$/i)
|
1133
|
+
|
1134
|
+
raise NoResults, 'no items selected' if items.empty?
|
1111
1135
|
|
1112
|
-
return if items.nil?
|
1113
1136
|
end
|
1114
1137
|
|
1138
|
+
raise NoResults, 'no items matched your search' if items.empty?
|
1139
|
+
|
1115
1140
|
items.each do |item|
|
1116
1141
|
added = []
|
1117
1142
|
removed = []
|
@@ -1123,7 +1148,7 @@ module Doing
|
|
1123
1148
|
# logger.debug('Autotag:', 'No changes')
|
1124
1149
|
else
|
1125
1150
|
logger.count(:added_tags)
|
1126
|
-
logger.
|
1151
|
+
logger.write(items.count == 1 ? :info : :debug, 'Tagged:', new_title)
|
1127
1152
|
item.title = new_title
|
1128
1153
|
end
|
1129
1154
|
else
|
@@ -1178,13 +1203,14 @@ module Doing
|
|
1178
1203
|
else
|
1179
1204
|
old_title = item.title.dup
|
1180
1205
|
should_date = opt[:date] && item.should_time?
|
1206
|
+
item.title.tag!('done', remove: true) if tag =~ /done/ && !should_date
|
1181
1207
|
item.title.tag!(tag, value: should_date ? done_date.strftime('%F %R') : nil)
|
1182
1208
|
added << tag if old_title != item.title
|
1183
1209
|
end
|
1184
1210
|
end
|
1185
1211
|
end
|
1186
1212
|
|
1187
|
-
log_change(tags_added: added, tags_removed: removed)
|
1213
|
+
log_change(tags_added: added, tags_removed: removed, item: item, single: items.count == 1)
|
1188
1214
|
|
1189
1215
|
item.note.add(opt[:note]) if opt[:note]
|
1190
1216
|
|
@@ -1216,7 +1242,7 @@ module Doing
|
|
1216
1242
|
@content[section][:items].concat([new_item])
|
1217
1243
|
|
1218
1244
|
logger.count(section == 'Archive' ? :archived : :moved)
|
1219
|
-
logger.debug("
|
1245
|
+
logger.debug("#{section == 'Archive' ? 'Archived' : 'Moved'}:",
|
1220
1246
|
"#{new_item.title.truncate(60)} from #{from} to #{section}")
|
1221
1247
|
new_item
|
1222
1248
|
end
|
@@ -1245,7 +1271,7 @@ module Doing
|
|
1245
1271
|
section_items = @content[section][:items]
|
1246
1272
|
deleted = section_items.delete(item)
|
1247
1273
|
logger.count(:deleted)
|
1248
|
-
logger.
|
1274
|
+
logger.info('Entry deleted:', deleted.title)
|
1249
1275
|
end
|
1250
1276
|
|
1251
1277
|
##
|
@@ -1260,16 +1286,13 @@ module Doing
|
|
1260
1286
|
section_items = @content[section][:items]
|
1261
1287
|
s_idx = section_items.index { |item| item.equal?(old_item) }
|
1262
1288
|
|
1263
|
-
unless s_idx
|
1264
|
-
Doing.logger.error('Fail to update:', 'Could not find item in index')
|
1265
|
-
raise Errors::ItemNotFound, 'Unable to find item in index, did it mutate?'
|
1266
|
-
end
|
1289
|
+
raise ItemNotFound, 'Unable to find item in index, did it mutate?' unless s_idx
|
1267
1290
|
|
1268
1291
|
return if section_items[s_idx].equal?(new_item)
|
1269
1292
|
|
1270
1293
|
section_items[s_idx] = new_item
|
1271
1294
|
logger.count(:updated)
|
1272
|
-
logger.
|
1295
|
+
logger.info('Entry updated:', section_items[s_idx].title.truncate(60))
|
1273
1296
|
new_item
|
1274
1297
|
end
|
1275
1298
|
|
@@ -1342,10 +1365,10 @@ module Doing
|
|
1342
1365
|
item.title = item.title.sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{item.section})")
|
1343
1366
|
move_item(item, 'Archive', label: false)
|
1344
1367
|
logger.count(:completed_archived)
|
1345
|
-
logger.
|
1368
|
+
logger.info('Completed/archived:', item.title)
|
1346
1369
|
else
|
1347
1370
|
logger.count(:completed)
|
1348
|
-
logger.
|
1371
|
+
logger.info('Completed:', item.title)
|
1349
1372
|
end
|
1350
1373
|
end
|
1351
1374
|
|
@@ -1477,10 +1500,10 @@ module Doing
|
|
1477
1500
|
if File.exist?(file)
|
1478
1501
|
init_doing_file(file)
|
1479
1502
|
@content.deep_merge(new_content)
|
1480
|
-
logger.warn('File update:', "
|
1503
|
+
logger.warn('File update:', "added entries to existing file: #{file}")
|
1481
1504
|
else
|
1482
1505
|
@content = new_content
|
1483
|
-
logger.warn('File update:', "
|
1506
|
+
logger.warn('File update:', "created new file: #{file}")
|
1484
1507
|
end
|
1485
1508
|
|
1486
1509
|
write(file, backup: false)
|
@@ -1564,17 +1587,13 @@ module Doing
|
|
1564
1587
|
|
1565
1588
|
items.reverse! if opt[:order] =~ /^d/i
|
1566
1589
|
|
1567
|
-
|
1568
1590
|
if opt[:interactive]
|
1569
1591
|
opt[:menu] = !opt[:force]
|
1570
1592
|
opt[:query] = '' # opt[:search]
|
1571
1593
|
opt[:multiple] = true
|
1572
1594
|
selected = choose_from_items(items, opt, include_section: opt[:section] =~ /^all$/i )
|
1573
1595
|
|
1574
|
-
if selected.empty?
|
1575
|
-
logger.debug('Skipped:', 'No selection')
|
1576
|
-
return
|
1577
|
-
end
|
1596
|
+
raise NoResults, 'no items selected' if selected.empty?
|
1578
1597
|
|
1579
1598
|
act_on(selected, opt)
|
1580
1599
|
return
|
@@ -1591,7 +1610,7 @@ module Doing
|
|
1591
1610
|
def output(items, title, is_single, opt = {})
|
1592
1611
|
out = nil
|
1593
1612
|
|
1594
|
-
raise
|
1613
|
+
raise InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
|
1595
1614
|
|
1596
1615
|
export_options = { page_title: title, is_single: is_single, options: opt }
|
1597
1616
|
|
@@ -1645,7 +1664,7 @@ module Doing
|
|
1645
1664
|
do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label], before: options[:before] })
|
1646
1665
|
write(doing_file)
|
1647
1666
|
else
|
1648
|
-
raise
|
1667
|
+
raise InvalidArgument, 'Either source or destination does not exist'
|
1649
1668
|
end
|
1650
1669
|
end
|
1651
1670
|
|
@@ -1704,7 +1723,12 @@ module Doing
|
|
1704
1723
|
@content[section][:items] = items
|
1705
1724
|
@content[destination][:items].concat(moved_items)
|
1706
1725
|
if moved_items.length.positive?
|
1707
|
-
logger.
|
1726
|
+
logger.count(destination == 'Archive' ? :archived : :moved,
|
1727
|
+
level: :info,
|
1728
|
+
count: moved_items.length,
|
1729
|
+
message: "%count %items from #{section} to #{destination}")
|
1730
|
+
else
|
1731
|
+
logger.info('Skipped:', 'No items were moved')
|
1708
1732
|
end
|
1709
1733
|
else
|
1710
1734
|
count = items.length if items.length < count
|
@@ -1732,10 +1756,11 @@ module Doing
|
|
1732
1756
|
else
|
1733
1757
|
items[0..count - 1]
|
1734
1758
|
end
|
1759
|
+
|
1735
1760
|
logger.count(destination == 'Archive' ? :archived : :moved,
|
1761
|
+
level: :info,
|
1736
1762
|
count: items.length - count,
|
1737
1763
|
message: "%count %items from #{section} to #{destination}")
|
1738
|
-
# logger.info('Archived:', "#{items.length - count} items from #{section} to #{destination}")
|
1739
1764
|
end
|
1740
1765
|
end
|
1741
1766
|
end
|
@@ -1872,7 +1897,7 @@ module Doing
|
|
1872
1897
|
end
|
1873
1898
|
|
1874
1899
|
opts[:search] = options[:search] if options[:search]
|
1875
|
-
|
1900
|
+
opts[:not] = options[:negate]
|
1876
1901
|
list_section(opts)
|
1877
1902
|
end
|
1878
1903
|
|
@@ -1934,11 +1959,11 @@ module Doing
|
|
1934
1959
|
end
|
1935
1960
|
end
|
1936
1961
|
|
1937
|
-
logger.debug('Autotag:', "
|
1962
|
+
logger.debug('Autotag:', "whitelisted tags: #{whitelisted.join(', ')}") unless whitelisted.empty?
|
1938
1963
|
new_tags = whitelisted
|
1939
1964
|
unless tail_tags.empty?
|
1940
1965
|
tags = tail_tags.uniq.map { |t| "@#{t}".cyan }.join(' ')
|
1941
|
-
logger.debug('Autotag:', "
|
1966
|
+
logger.debug('Autotag:', "synonym tags: #{tags}")
|
1942
1967
|
tags_a = tail_tags.map { |t| "@#{t}" }
|
1943
1968
|
text.add_tags!(tags_a.join(' '))
|
1944
1969
|
new_tags.concat(tags_a)
|
@@ -1947,7 +1972,7 @@ module Doing
|
|
1947
1972
|
unless text == original
|
1948
1973
|
logger.info('Autotag:', "added #{new_tags.join(', ')} to \"#{text}\"")
|
1949
1974
|
else
|
1950
|
-
logger.debug('
|
1975
|
+
logger.debug('Skipped:', "no change to \"#{text}\"")
|
1951
1976
|
end
|
1952
1977
|
|
1953
1978
|
text
|
@@ -1997,7 +2022,7 @@ module Doing
|
|
1997
2022
|
EOS
|
1998
2023
|
sorted_tags_data.reverse.each do |k, v|
|
1999
2024
|
if v > 0
|
2000
|
-
output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{'%02d:%02d:%02d' %
|
2025
|
+
output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{'%02d:%02d:%02d' % format_time(v)}</td></tr>\n"
|
2001
2026
|
end
|
2002
2027
|
end
|
2003
2028
|
tail = <<EOS
|
@@ -2008,7 +2033,7 @@ EOS
|
|
2008
2033
|
<tfoot>
|
2009
2034
|
<tr>
|
2010
2035
|
<td style="text-align:left;"><strong>Total</strong></td>
|
2011
|
-
<td style="text-align:left;">#{'%02d:%02d:%02d' %
|
2036
|
+
<td style="text-align:left;">#{'%02d:%02d:%02d' % format_time(total)}</td>
|
2012
2037
|
</tr>
|
2013
2038
|
</tfoot>
|
2014
2039
|
</table>
|
@@ -2022,7 +2047,7 @@ EOS
|
|
2022
2047
|
EOS
|
2023
2048
|
sorted_tags_data.reverse.each do |k, v|
|
2024
2049
|
if v > 0
|
2025
|
-
output += "| #{' ' * (pad - k.length)}#{k} | #{'%02d:%02d:%02d' %
|
2050
|
+
output += "| #{' ' * (pad - k.length)}#{k} | #{'%02d:%02d:%02d' % format_time(v)} |\n"
|
2026
2051
|
end
|
2027
2052
|
end
|
2028
2053
|
tail = "[Tag Totals]"
|
@@ -2030,7 +2055,7 @@ EOS
|
|
2030
2055
|
when :json
|
2031
2056
|
output = []
|
2032
2057
|
sorted_tags_data.reverse.each do |k, v|
|
2033
|
-
d, h, m =
|
2058
|
+
d, h, m = format_time(v)
|
2034
2059
|
output << {
|
2035
2060
|
'tag' => k,
|
2036
2061
|
'seconds' => v,
|
@@ -2038,6 +2063,39 @@ EOS
|
|
2038
2063
|
}
|
2039
2064
|
end
|
2040
2065
|
output
|
2066
|
+
when :human
|
2067
|
+
output = []
|
2068
|
+
sorted_tags_data.reverse.each do |k, v|
|
2069
|
+
spacer = ''
|
2070
|
+
(max - k.length).times do
|
2071
|
+
spacer += ' '
|
2072
|
+
end
|
2073
|
+
d, h, m = format_time(v, human: true)
|
2074
|
+
output.push("┃ #{spacer}#{k}:#{format('%<h> 4dh %<m>02dm', h: h, m: m)} ┃")
|
2075
|
+
end
|
2076
|
+
|
2077
|
+
header = '┏━━ Tag Totals '
|
2078
|
+
(max - 2).times { header += '━' }
|
2079
|
+
header += '┓'
|
2080
|
+
footer = '┗'
|
2081
|
+
(max + 12).times { footer += '━' }
|
2082
|
+
footer += '┛'
|
2083
|
+
divider = '┣'
|
2084
|
+
(max + 12).times { divider += '━' }
|
2085
|
+
divider += '┫'
|
2086
|
+
output = output.empty? ? '' : "\n#{header}\n#{output.join("\n")}"
|
2087
|
+
d, h, m = format_time(total, human: true)
|
2088
|
+
output += "\n#{divider}"
|
2089
|
+
spacer = ''
|
2090
|
+
(max - 6).times do
|
2091
|
+
spacer += ' '
|
2092
|
+
end
|
2093
|
+
total = "┃ #{spacer}total: "
|
2094
|
+
total += format('%<h> 4dh %<m>02dm', h: h, m: m)
|
2095
|
+
total += ' ┃'
|
2096
|
+
output += "\n#{total}"
|
2097
|
+
output += "\n#{footer}"
|
2098
|
+
output
|
2041
2099
|
else
|
2042
2100
|
output = []
|
2043
2101
|
sorted_tags_data.reverse.each do |k, v|
|
@@ -2045,12 +2103,12 @@ EOS
|
|
2045
2103
|
(max - k.length).times do
|
2046
2104
|
spacer += ' '
|
2047
2105
|
end
|
2048
|
-
d, h, m =
|
2106
|
+
d, h, m = format_time(v)
|
2049
2107
|
output.push("#{k}:#{spacer}#{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}")
|
2050
2108
|
end
|
2051
2109
|
|
2052
2110
|
output = output.empty? ? '' : "\n--- Tag Totals ---\n#{output.join("\n")}"
|
2053
|
-
d, h, m =
|
2111
|
+
d, h, m = format_time(total)
|
2054
2112
|
output += "\n\nTotal tracked: #{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}\n"
|
2055
2113
|
output
|
2056
2114
|
end
|
@@ -2076,7 +2134,7 @@ EOS
|
|
2076
2134
|
record_tag_times(item, seconds) if record
|
2077
2135
|
return seconds.positive? ? seconds : false unless formatted
|
2078
2136
|
|
2079
|
-
return seconds.positive? ? format('%02d:%02d:%02d', *
|
2137
|
+
return seconds.positive? ? format('%02d:%02d:%02d', *format_time(seconds)) : false
|
2080
2138
|
end
|
2081
2139
|
|
2082
2140
|
false
|
@@ -2106,7 +2164,7 @@ EOS
|
|
2106
2164
|
##
|
2107
2165
|
## @param seconds The seconds
|
2108
2166
|
##
|
2109
|
-
def
|
2167
|
+
def format_time(seconds, human: false)
|
2110
2168
|
return [0, 0, 0] if seconds.nil?
|
2111
2169
|
|
2112
2170
|
if seconds.class == String && seconds =~ /(\d+):(\d+):(\d+)/
|
@@ -2117,10 +2175,15 @@ EOS
|
|
2117
2175
|
end
|
2118
2176
|
minutes = (seconds / 60).to_i
|
2119
2177
|
hours = (minutes / 60).to_i
|
2120
|
-
|
2121
|
-
|
2122
|
-
|
2123
|
-
|
2178
|
+
if human
|
2179
|
+
minutes = (minutes % 60).to_i
|
2180
|
+
[0, hours, minutes]
|
2181
|
+
else
|
2182
|
+
days = (hours / 24).to_i
|
2183
|
+
hours = (hours % 24).to_i
|
2184
|
+
minutes = (minutes % 60).to_i
|
2185
|
+
[days, hours, minutes]
|
2186
|
+
end
|
2124
2187
|
end
|
2125
2188
|
|
2126
2189
|
private
|
@@ -2135,23 +2198,28 @@ EOS
|
|
2135
2198
|
logger.log_now(:error, 'STDERR output:', stderr)
|
2136
2199
|
end
|
2137
2200
|
|
2138
|
-
def log_change(tags_added: [], tags_removed: [], count: 1)
|
2201
|
+
def log_change(tags_added: [], tags_removed: [], count: 1, item: nil, single: false)
|
2139
2202
|
if tags_added.empty? && tags_removed.empty?
|
2140
2203
|
logger.count(:skipped, level: :debug, message: '%count %items with no change', count: count)
|
2141
2204
|
else
|
2142
|
-
|
2143
2205
|
if tags_added.empty?
|
2144
2206
|
logger.count(:skipped, level: :debug, message: 'no tags added to %count %items')
|
2145
|
-
# logger.debug('No tags added:', %("#{item.title}" in #{item.section}))
|
2146
2207
|
else
|
2147
|
-
|
2148
|
-
|
2208
|
+
if single && item
|
2209
|
+
logger.info('Tagged:', %(added #{tags_added.count == 1 ? 'tag' : 'tags'} #{tags_added.map {|t| "@#{t}"}.join(', ')} to #{item.title}))
|
2210
|
+
else
|
2211
|
+
logger.count(:added_tags, level: :info, tag: tags_added, message: '%tags added to %count %items')
|
2212
|
+
end
|
2149
2213
|
end
|
2150
2214
|
|
2151
2215
|
if tags_removed.empty?
|
2152
2216
|
logger.count(:skipped, level: :debug, message: 'no tags removed from %count %items')
|
2153
2217
|
else
|
2154
|
-
|
2218
|
+
if single && item
|
2219
|
+
logger.info('Untagged:', %(removed #{tags_removed.count == 1 ? 'tag' : 'tags'} #{tags_added.map {|t| "@#{t}"}.join(', ')} from #{item.title}))
|
2220
|
+
else
|
2221
|
+
logger.count(:removed_tags, level: :info, tag: tags_removed, message: '%tags removed from %count %items')
|
2222
|
+
end
|
2155
2223
|
end
|
2156
2224
|
end
|
2157
2225
|
end
|