doing 2.0.5.pre → 2.0.9.pre

Sign up to get free protection for your applications and to get access to all the features.
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