doing 2.0.5.pre → 2.0.9.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.
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,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
- 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
- search_match = opt[:search].nil? || opt[:search].empty? ? true : item.search(opt[:search])
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 Errors::InvalidArgument, "Can't skip menu when no query is provided" unless opt[:query]
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 Errors::InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
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 Errors::UserCancelled, 'Cancelled'
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
- logger.error('Error:', 'resume and restart can only be used on a single entry')
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
- 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 )
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.debug('Tags updated:', new_title)
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("Entry #{section == 'Archive' ? 'archived' : 'moved'}:",
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.debug('Entry deleted:', deleted.title)
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.debug('Entry updated:', section_items[s_idx].title.truncate(60))
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.debug('Completed/archived:', item.title)
1372
+ logger.info('Completed/archived:', item.title)
1347
1373
  else
1348
1374
  logger.count(:completed)
1349
- logger.debug('Completed:', item.title)
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:', "Added entries to existing file: #{file}")
1507
+ logger.warn('File update:', "added entries to existing file: #{file}")
1482
1508
  else
1483
1509
  @content = new_content
1484
- logger.warn('File update:', "Created new file: #{file}")
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 Errors::InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
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 Errors::InvalidArgument, 'Either source or destination does not exist'
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.info('Archived:', "#{moved_items.length} items from #{section} to #{destination}")
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:', "Whitelisted tags: #{whitelisted.join(', ')}") unless whitelisted.empty?
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:', "Synonym tags: #{tags}")
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('Autotag:', "no change to \"#{text}\"")
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
- d, h, m = format_time(v, human: true)
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
- logger.count(:added_tags, tag: tags_added, message: '%tags added to %count %items')
2187
- # logger.info('Added tags:', %(#{did_add} to "#{item.title}" in #{item.section}))
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
- logger.count(:removed_tags, tag: tags_removed, message: '%tags removed from %count %items')
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