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.
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