doing 2.0.3.pre → 2.0.8.pre

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/bin/doing +316 -114
  6. data/doing.rdoc +244 -19
  7. data/example_plugin.rb +1 -1
  8. data/generate_completions.sh +1 -0
  9. data/lib/completion/_doing.zsh +179 -127
  10. data/lib/completion/doing.bash +60 -27
  11. data/lib/completion/doing.fish +74 -23
  12. data/lib/doing/cli_status.rb +4 -0
  13. data/lib/doing/configuration.rb +2 -0
  14. data/lib/doing/errors.rb +22 -15
  15. data/lib/doing/item.rb +12 -11
  16. data/lib/doing/log_adapter.rb +27 -25
  17. data/lib/doing/plugin_manager.rb +1 -1
  18. data/lib/doing/plugins/export/json_export.rb +2 -2
  19. data/lib/doing/plugins/export/template_export.rb +1 -1
  20. data/lib/doing/plugins/import/calendar_import.rb +7 -1
  21. data/lib/doing/plugins/import/doing_import.rb +6 -6
  22. data/lib/doing/plugins/import/timing_import.rb +7 -1
  23. data/lib/doing/string.rb +9 -7
  24. data/lib/doing/version.rb +1 -1
  25. data/lib/doing/wwid.rb +160 -92
  26. data/lib/examples/commands/autotag.rb +63 -0
  27. data/lib/examples/commands/wiki.rb +1 -0
  28. data/lib/examples/plugins/say_export.rb +1 -1
  29. data/lib/examples/plugins/{templates → wiki_export/templates}/wiki.css +0 -0
  30. data/lib/examples/plugins/{templates → wiki_export/templates}/wiki.haml +0 -0
  31. data/lib/examples/plugins/{templates → wiki_export/templates}/wiki_index.haml +0 -0
  32. data/lib/examples/plugins/{wiki_export.rb → wiki_export/wiki_export.rb} +0 -0
  33. data/scripts/generate_bash_completions.rb +3 -2
  34. data/scripts/generate_fish_completions.rb +4 -1
  35. data/scripts/generate_zsh_completions.rb +44 -39
  36. metadata +7 -7
  37. 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 Errors::NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
126
+ # raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
127
127
 
128
- raise Errors::MissingEditor, 'No EDITOR variable defined in environment' if Util.default_editor.nil?
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 Errors::EmptyInput, 'No content in entry' if input.nil? || input.strip.empty?
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 Errors::EmptyInput, 'No content in first line' if title.nil? || title.strip.empty?
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 Errors::InvalidTimeExpression, "Invalid time expression #{input.inspect}" if input.to_s.strip == ''
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
- logger.debug('Skipped': 'Section already exists')
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 `doing view #{alt}`?", default_response: 'n')
315
- raise Errors::InvalidSection, "Run again with `doing view #{alt}`" if meant_view
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 Errors::InvalidSection, "Unknown section: #{frag}"
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
- guess = guess_section(frag, guessed: true, suggest: true)
402
- exit_now! "Did you mean `doing show #{guess}`?" if guess
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 Errors::InvalidView, "Unknown view: #{frag}"
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.debug('Entry added:', %("#{entry.title}" to #{section}))
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 entry') if duped
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 Errors::NoEntryError, 'No entry found' unless last_item
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
- Doing.logger.info('Reset:', %(Reset #{resume ? 'and resumed ' : ''} "#{item.title}" in #{item.section}))
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.debug('Skipped:', 'No content provided')
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.debug('Skipped:', 'No previous entry found')
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
- finished = opt[:unfinished] && item.tags?('done', :and)
688
- keep = false if finished
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 Errors::InvalidArgument, "Can't skip menu when no query is provided" unless opt[:query]
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 Errors::InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
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 Errors::UserCancelled, 'Cancelled'
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
- logger.error('Error:', 'resume and restart can only be used on a single entry')
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
- menu: true,
1105
- header: '',
1106
- prompt: 'Select entries to tag > ',
1107
- multiple: true,
1108
- sort: true,
1109
- show_if_single: true
1110
- }, include_section: opt[:section] =~ /^all$/i )
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.debug('Tags updated:', new_title)
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("Entry #{section == 'Archive' ? 'archived' : 'moved'}:",
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.debug('Entry deleted:', deleted.title)
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.debug('Entry updated:', section_items[s_idx].title.truncate(60))
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.debug('Completed/archived:', item.title)
1368
+ logger.info('Completed/archived:', item.title)
1346
1369
  else
1347
1370
  logger.count(:completed)
1348
- logger.debug('Completed:', item.title)
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:', "Added entries to existing file: #{file}")
1503
+ logger.warn('File update:', "added entries to existing file: #{file}")
1481
1504
  else
1482
1505
  @content = new_content
1483
- logger.warn('File update:', "Created new file: #{file}")
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 Errors::InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
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 Errors::InvalidArgument, 'Either source or destination does not exist'
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.info('Archived:', "#{moved_items.length} items from #{section} to #{destination}")
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:', "Whitelisted tags: #{whitelisted.join(', ')}") unless whitelisted.empty?
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:', "Synonym tags: #{tags}")
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('Autotag:', "no change to \"#{text}\"")
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' % fmt_time(v)}</td></tr>\n"
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' % fmt_time(total)}</td>
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' % fmt_time(v)} |\n"
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 = fmt_time(v)
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 = fmt_time(v)
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 = fmt_time(total)
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', *fmt_time(seconds)) : false
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 fmt_time(seconds)
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
- days = (hours / 24).to_i
2121
- hours = (hours % 24).to_i
2122
- minutes = (minutes % 60).to_i
2123
- [days, hours, minutes]
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
- logger.count(:added_tags, tag: tags_added, message: '%tags added to %count %items')
2148
- # logger.info('Added tags:', %(#{did_add} to "#{item.title}" in #{item.section}))
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
- logger.count(:removed_tags, tag: tags_removed, message: '%tags removed from %count %items')
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