doing 2.0.2.pre → 2.0.7.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: 'overlapping %item') 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
 
@@ -759,10 +761,7 @@ module Doing
759
761
 
760
762
  selection = choose_from_items(items, opt, include_section: section =~ /^all$/i)
761
763
 
762
- if selection.empty?
763
- logger.debug('Skipped:', 'No selection')
764
- return
765
- end
764
+ raise NoResults, 'no items selected' if selection.empty?
766
765
 
767
766
  act_on(selection, opt)
768
767
  end
@@ -807,7 +806,7 @@ module Doing
807
806
  fzf_args.push('-1') unless opt[:show_if_single]
808
807
 
809
808
  unless opt[:menu]
810
- raise Errors::InvalidArgument, "Can't skip menu when no query is provided" unless opt[:query]
809
+ raise InvalidArgument, "Can't skip menu when no query is provided" unless opt[:query] && !opt[:query].empty?
811
810
 
812
811
  fzf_args.concat([%(--filter="#{opt[:query]}"), opt[:sort] ? '' : '--no-sort'])
813
812
  end
@@ -823,8 +822,10 @@ module Doing
823
822
  end
824
823
 
825
824
  def act_on(items, opt = {})
826
- actions = %i[editor delete tag flag finish cancel archive output save_to]
825
+ actions = %i[editor delete tag flag finish cancel archive output save_to again resume]
827
826
  has_action = false
827
+ single = items.count == 1
828
+
828
829
  actions.each do |a|
829
830
  if opt[a]
830
831
  has_action = true
@@ -864,7 +865,7 @@ module Doing
864
865
  opt[:reset] = true
865
866
  when /(add|remove) tag/
866
867
  type = action =~ /^add/ ? 'add' : 'remove'
867
- raise Errors::InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
868
+ raise InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
868
869
 
869
870
  print "#{Color.yellow}Tag to #{type}: #{Color.reset}"
870
871
  tag = $stdin.gets
@@ -879,7 +880,7 @@ module Doing
879
880
  next if tag =~ /^ *$/
880
881
 
881
882
  unless output_format
882
- raise Errors::UserCancelled, 'Cancelled'
883
+ raise UserCancelled, 'Cancelled'
883
884
  end
884
885
 
885
886
  opt[:output] = output_format.strip
@@ -912,7 +913,7 @@ module Doing
912
913
 
913
914
  if opt[:resume] || opt[:reset]
914
915
  if items.count > 1
915
- logger.error('Error:', 'resume and restart can only be used on a single entry')
916
+ raise InvalidArgument, 'resume and restart can only be used on a single entry'
916
917
  else
917
918
  item = items[0]
918
919
  if opt[:resume] && !opt[:reset]
@@ -942,7 +943,7 @@ module Doing
942
943
  if opt[:flag]
943
944
  tag = @config['marker_tag'] || 'flagged'
944
945
  items.map! do |item|
945
- tag_item(item, tag, date: false, remove: opt[:remove])
946
+ tag_item(item, tag, date: false, remove: opt[:remove], single: single)
946
947
  end
947
948
  end
948
949
 
@@ -951,7 +952,7 @@ module Doing
951
952
  items.map! do |item|
952
953
  if item.should_finish?
953
954
  should_date = !opt[:cancel] && item.should_time?
954
- tag_item(item, tag, date: should_date, remove: opt[:remove])
955
+ tag_item(item, tag, date: should_date, remove: opt[:remove], single: single)
955
956
  end
956
957
  end
957
958
  end
@@ -959,7 +960,7 @@ module Doing
959
960
  if opt[:tag]
960
961
  tag = opt[:tag]
961
962
  items.map! do |item|
962
- tag_item(item, tag, date: false, remove: opt[:remove])
963
+ tag_item(item, tag, date: false, remove: opt[:remove], single: single)
963
964
  end
964
965
  end
965
966
 
@@ -1057,7 +1058,7 @@ module Doing
1057
1058
  ## @param remove (Boolean) remove tags
1058
1059
  ## @param date (Boolean) Include timestamp?
1059
1060
  ##
1060
- def tag_item(item, tags, remove: false, date: false)
1061
+ def tag_item(item, tags, remove: false, date: false, single: false)
1061
1062
  added = []
1062
1063
  removed = []
1063
1064
 
@@ -1073,7 +1074,7 @@ module Doing
1073
1074
  end
1074
1075
  end
1075
1076
 
1076
- log_change(tags_added: added, tags_removed: removed, count: 1)
1077
+ log_change(tags_added: added, tags_removed: removed, count: 1, item: item, single: single)
1077
1078
 
1078
1079
  item
1079
1080
  end
@@ -1097,21 +1098,22 @@ module Doing
1097
1098
 
1098
1099
  items = filter_items([], opt: opt)
1099
1100
 
1100
- logger.info('Skipped:', 'no items matched your search') if items.empty?
1101
-
1102
1101
  if opt[:interactive]
1103
1102
  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 )
1103
+ menu: true,
1104
+ header: '',
1105
+ prompt: 'Select entries to tag > ',
1106
+ multiple: true,
1107
+ sort: true,
1108
+ show_if_single: true
1109
+ }, include_section: opt[:section] =~ /^all$/i)
1110
+
1111
+ raise NoResults, 'no items selected' if items.empty?
1111
1112
 
1112
- return if items.nil?
1113
1113
  end
1114
1114
 
1115
+ raise NoResults, 'no items matched your search' if items.empty?
1116
+
1115
1117
  items.each do |item|
1116
1118
  added = []
1117
1119
  removed = []
@@ -1123,7 +1125,7 @@ module Doing
1123
1125
  # logger.debug('Autotag:', 'No changes')
1124
1126
  else
1125
1127
  logger.count(:added_tags)
1126
- logger.debug('Tags updated:', new_title)
1128
+ logger.write(items.count == 1 ? :info : :debug, 'Tagged:', new_title)
1127
1129
  item.title = new_title
1128
1130
  end
1129
1131
  else
@@ -1178,13 +1180,14 @@ module Doing
1178
1180
  else
1179
1181
  old_title = item.title.dup
1180
1182
  should_date = opt[:date] && item.should_time?
1183
+ item.title.tag!('done', remove: true) if tag =~ /done/ && !should_date
1181
1184
  item.title.tag!(tag, value: should_date ? done_date.strftime('%F %R') : nil)
1182
1185
  added << tag if old_title != item.title
1183
1186
  end
1184
1187
  end
1185
1188
  end
1186
1189
 
1187
- log_change(tags_added: added, tags_removed: removed)
1190
+ log_change(tags_added: added, tags_removed: removed, item: item, single: items.count == 1)
1188
1191
 
1189
1192
  item.note.add(opt[:note]) if opt[:note]
1190
1193
 
@@ -1216,7 +1219,7 @@ module Doing
1216
1219
  @content[section][:items].concat([new_item])
1217
1220
 
1218
1221
  logger.count(section == 'Archive' ? :archived : :moved)
1219
- logger.debug("Entry #{section == 'Archive' ? 'archived' : 'moved'}:",
1222
+ logger.debug("#{section == 'Archive' ? 'Archived' : 'Moved'}:",
1220
1223
  "#{new_item.title.truncate(60)} from #{from} to #{section}")
1221
1224
  new_item
1222
1225
  end
@@ -1245,7 +1248,7 @@ module Doing
1245
1248
  section_items = @content[section][:items]
1246
1249
  deleted = section_items.delete(item)
1247
1250
  logger.count(:deleted)
1248
- logger.debug('Entry deleted:', deleted.title)
1251
+ logger.info('Entry deleted:', deleted.title)
1249
1252
  end
1250
1253
 
1251
1254
  ##
@@ -1260,16 +1263,13 @@ module Doing
1260
1263
  section_items = @content[section][:items]
1261
1264
  s_idx = section_items.index { |item| item.equal?(old_item) }
1262
1265
 
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
1266
+ raise ItemNotFound, 'Unable to find item in index, did it mutate?' unless s_idx
1267
1267
 
1268
1268
  return if section_items[s_idx].equal?(new_item)
1269
1269
 
1270
1270
  section_items[s_idx] = new_item
1271
1271
  logger.count(:updated)
1272
- logger.debug('Entry updated:', section_items[s_idx].title.truncate(60))
1272
+ logger.info('Entry updated:', section_items[s_idx].title.truncate(60))
1273
1273
  new_item
1274
1274
  end
1275
1275
 
@@ -1342,10 +1342,10 @@ module Doing
1342
1342
  item.title = item.title.sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{item.section})")
1343
1343
  move_item(item, 'Archive', label: false)
1344
1344
  logger.count(:completed_archived)
1345
- logger.debug('Completed/archived:', item.title)
1345
+ logger.info('Completed/archived:', item.title)
1346
1346
  else
1347
1347
  logger.count(:completed)
1348
- logger.debug('Completed:', item.title)
1348
+ logger.info('Completed:', item.title)
1349
1349
  end
1350
1350
  end
1351
1351
 
@@ -1477,10 +1477,10 @@ module Doing
1477
1477
  if File.exist?(file)
1478
1478
  init_doing_file(file)
1479
1479
  @content.deep_merge(new_content)
1480
- logger.warn('File update:', "Added entries to existing file: #{file}")
1480
+ logger.warn('File update:', "added entries to existing file: #{file}")
1481
1481
  else
1482
1482
  @content = new_content
1483
- logger.warn('File update:', "Created new file: #{file}")
1483
+ logger.warn('File update:', "created new file: #{file}")
1484
1484
  end
1485
1485
 
1486
1486
  write(file, backup: false)
@@ -1571,10 +1571,7 @@ module Doing
1571
1571
  opt[:multiple] = true
1572
1572
  selected = choose_from_items(items, opt, include_section: opt[:section] =~ /^all$/i )
1573
1573
 
1574
- if selected.empty?
1575
- logger.debug('Skipped:', 'No selection')
1576
- return
1577
- end
1574
+ raise NoResults, 'no items selected' if selected.empty?
1578
1575
 
1579
1576
  act_on(selected, opt)
1580
1577
  return
@@ -1591,7 +1588,7 @@ module Doing
1591
1588
  def output(items, title, is_single, opt = {})
1592
1589
  out = nil
1593
1590
 
1594
- raise Errors::InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
1591
+ raise InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
1595
1592
 
1596
1593
  export_options = { page_title: title, is_single: is_single, options: opt }
1597
1594
 
@@ -1645,7 +1642,7 @@ module Doing
1645
1642
  do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label], before: options[:before] })
1646
1643
  write(doing_file)
1647
1644
  else
1648
- raise Errors::InvalidArgument, 'Either source or destination does not exist'
1645
+ raise InvalidArgument, 'Either source or destination does not exist'
1649
1646
  end
1650
1647
  end
1651
1648
 
@@ -1704,7 +1701,12 @@ module Doing
1704
1701
  @content[section][:items] = items
1705
1702
  @content[destination][:items].concat(moved_items)
1706
1703
  if moved_items.length.positive?
1707
- logger.info('Archived:', "#{moved_items.length} items from #{section} to #{destination}")
1704
+ logger.count(destination == 'Archive' ? :archived : :moved,
1705
+ level: :info,
1706
+ count: moved_items.length,
1707
+ message: "%count %items from #{section} to #{destination}")
1708
+ else
1709
+ logger.info('Skipped:', 'No items were moved')
1708
1710
  end
1709
1711
  else
1710
1712
  count = items.length if items.length < count
@@ -1732,10 +1734,11 @@ module Doing
1732
1734
  else
1733
1735
  items[0..count - 1]
1734
1736
  end
1737
+
1735
1738
  logger.count(destination == 'Archive' ? :archived : :moved,
1739
+ level: :info,
1736
1740
  count: items.length - count,
1737
1741
  message: "%count %items from #{section} to #{destination}")
1738
- # logger.info('Archived:', "#{items.length - count} items from #{section} to #{destination}")
1739
1742
  end
1740
1743
  end
1741
1744
  end
@@ -1934,11 +1937,11 @@ module Doing
1934
1937
  end
1935
1938
  end
1936
1939
 
1937
- logger.debug('Autotag:', "Whitelisted tags: #{whitelisted.join(', ')}") unless whitelisted.empty?
1940
+ logger.debug('Autotag:', "whitelisted tags: #{whitelisted.join(', ')}") unless whitelisted.empty?
1938
1941
  new_tags = whitelisted
1939
1942
  unless tail_tags.empty?
1940
1943
  tags = tail_tags.uniq.map { |t| "@#{t}".cyan }.join(' ')
1941
- logger.debug('Autotag:', "Synonym tags: #{tags}")
1944
+ logger.debug('Autotag:', "synonym tags: #{tags}")
1942
1945
  tags_a = tail_tags.map { |t| "@#{t}" }
1943
1946
  text.add_tags!(tags_a.join(' '))
1944
1947
  new_tags.concat(tags_a)
@@ -1947,7 +1950,7 @@ module Doing
1947
1950
  unless text == original
1948
1951
  logger.info('Autotag:', "added #{new_tags.join(', ')} to \"#{text}\"")
1949
1952
  else
1950
- logger.debug('Autotag:', "no change to \"#{text}\"")
1953
+ logger.debug('Skipped:', "no change to \"#{text}\"")
1951
1954
  end
1952
1955
 
1953
1956
  text
@@ -1997,7 +2000,7 @@ module Doing
1997
2000
  EOS
1998
2001
  sorted_tags_data.reverse.each do |k, v|
1999
2002
  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"
2003
+ output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{'%02d:%02d:%02d' % format_time(v)}</td></tr>\n"
2001
2004
  end
2002
2005
  end
2003
2006
  tail = <<EOS
@@ -2008,7 +2011,7 @@ EOS
2008
2011
  <tfoot>
2009
2012
  <tr>
2010
2013
  <td style="text-align:left;"><strong>Total</strong></td>
2011
- <td style="text-align:left;">#{'%02d:%02d:%02d' % fmt_time(total)}</td>
2014
+ <td style="text-align:left;">#{'%02d:%02d:%02d' % format_time(total)}</td>
2012
2015
  </tr>
2013
2016
  </tfoot>
2014
2017
  </table>
@@ -2022,7 +2025,7 @@ EOS
2022
2025
  EOS
2023
2026
  sorted_tags_data.reverse.each do |k, v|
2024
2027
  if v > 0
2025
- output += "| #{' ' * (pad - k.length)}#{k} | #{'%02d:%02d:%02d' % fmt_time(v)} |\n"
2028
+ output += "| #{' ' * (pad - k.length)}#{k} | #{'%02d:%02d:%02d' % format_time(v)} |\n"
2026
2029
  end
2027
2030
  end
2028
2031
  tail = "[Tag Totals]"
@@ -2030,7 +2033,7 @@ EOS
2030
2033
  when :json
2031
2034
  output = []
2032
2035
  sorted_tags_data.reverse.each do |k, v|
2033
- d, h, m = fmt_time(v)
2036
+ d, h, m = format_time(v)
2034
2037
  output << {
2035
2038
  'tag' => k,
2036
2039
  'seconds' => v,
@@ -2038,6 +2041,39 @@ EOS
2038
2041
  }
2039
2042
  end
2040
2043
  output
2044
+ when :human
2045
+ output = []
2046
+ sorted_tags_data.reverse.each do |k, v|
2047
+ spacer = ''
2048
+ (max - k.length).times do
2049
+ spacer += ' '
2050
+ end
2051
+ d, h, m = format_time(v, human: true)
2052
+ output.push("┃ #{spacer}#{k}:#{format('%<h> 4dh %<m>02dm', h: h, m: m)} ┃")
2053
+ end
2054
+
2055
+ header = '┏━━ Tag Totals '
2056
+ (max - 2).times { header += '━' }
2057
+ header += '┓'
2058
+ footer = '┗'
2059
+ (max + 12).times { footer += '━' }
2060
+ footer += '┛'
2061
+ divider = '┣'
2062
+ (max + 12).times { divider += '━' }
2063
+ divider += '┫'
2064
+ output = output.empty? ? '' : "\n#{header}\n#{output.join("\n")}"
2065
+ d, h, m = format_time(total, human: true)
2066
+ output += "\n#{divider}"
2067
+ spacer = ''
2068
+ (max - 6).times do
2069
+ spacer += ' '
2070
+ end
2071
+ total = "┃ #{spacer}total: "
2072
+ total += format('%<h> 4dh %<m>02dm', h: h, m: m)
2073
+ total += ' ┃'
2074
+ output += "\n#{total}"
2075
+ output += "\n#{footer}"
2076
+ output
2041
2077
  else
2042
2078
  output = []
2043
2079
  sorted_tags_data.reverse.each do |k, v|
@@ -2045,12 +2081,12 @@ EOS
2045
2081
  (max - k.length).times do
2046
2082
  spacer += ' '
2047
2083
  end
2048
- d, h, m = fmt_time(v)
2084
+ d, h, m = format_time(v)
2049
2085
  output.push("#{k}:#{spacer}#{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}")
2050
2086
  end
2051
2087
 
2052
2088
  output = output.empty? ? '' : "\n--- Tag Totals ---\n#{output.join("\n")}"
2053
- d, h, m = fmt_time(total)
2089
+ d, h, m = format_time(total)
2054
2090
  output += "\n\nTotal tracked: #{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}\n"
2055
2091
  output
2056
2092
  end
@@ -2076,7 +2112,7 @@ EOS
2076
2112
  record_tag_times(item, seconds) if record
2077
2113
  return seconds.positive? ? seconds : false unless formatted
2078
2114
 
2079
- return seconds.positive? ? format('%02d:%02d:%02d', *fmt_time(seconds)) : false
2115
+ return seconds.positive? ? format('%02d:%02d:%02d', *format_time(seconds)) : false
2080
2116
  end
2081
2117
 
2082
2118
  false
@@ -2106,7 +2142,7 @@ EOS
2106
2142
  ##
2107
2143
  ## @param seconds The seconds
2108
2144
  ##
2109
- def fmt_time(seconds)
2145
+ def format_time(seconds, human: false)
2110
2146
  return [0, 0, 0] if seconds.nil?
2111
2147
 
2112
2148
  if seconds.class == String && seconds =~ /(\d+):(\d+):(\d+)/
@@ -2117,10 +2153,15 @@ EOS
2117
2153
  end
2118
2154
  minutes = (seconds / 60).to_i
2119
2155
  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]
2156
+ if human
2157
+ minutes = (minutes % 60).to_i
2158
+ [0, hours, minutes]
2159
+ else
2160
+ days = (hours / 24).to_i
2161
+ hours = (hours % 24).to_i
2162
+ minutes = (minutes % 60).to_i
2163
+ [days, hours, minutes]
2164
+ end
2124
2165
  end
2125
2166
 
2126
2167
  private
@@ -2135,23 +2176,28 @@ EOS
2135
2176
  logger.log_now(:error, 'STDERR output:', stderr)
2136
2177
  end
2137
2178
 
2138
- def log_change(tags_added: [], tags_removed: [], count: 1)
2179
+ def log_change(tags_added: [], tags_removed: [], count: 1, item: nil, single: false)
2139
2180
  if tags_added.empty? && tags_removed.empty?
2140
2181
  logger.count(:skipped, level: :debug, message: '%count %items with no change', count: count)
2141
2182
  else
2142
-
2143
2183
  if tags_added.empty?
2144
2184
  logger.count(:skipped, level: :debug, message: 'no tags added to %count %items')
2145
- # logger.debug('No tags added:', %("#{item.title}" in #{item.section}))
2146
2185
  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}))
2186
+ if single && item
2187
+ logger.info('Tagged:', %(added #{tags_added.count == 1 ? 'tag' : 'tags'} #{tags_added.map {|t| "@#{t}"}.join(', ')} to #{item.title}))
2188
+ else
2189
+ logger.count(:added_tags, level: :info, tag: tags_added, message: '%tags added to %count %items')
2190
+ end
2149
2191
  end
2150
2192
 
2151
2193
  if tags_removed.empty?
2152
2194
  logger.count(:skipped, level: :debug, message: 'no tags removed from %count %items')
2153
2195
  else
2154
- logger.count(:removed_tags, tag: tags_removed, message: '%tags removed from %count %items')
2196
+ if single && item
2197
+ logger.info('Untagged:', %(removed #{tags_removed.count == 1 ? 'tag' : 'tags'} #{tags_added.map {|t| "@#{t}"}.join(', ')} from #{item.title}))
2198
+ else
2199
+ logger.count(:removed_tags, level: :info, tag: tags_removed, message: '%tags removed from %count %items')
2200
+ end
2155
2201
  end
2156
2202
  end
2157
2203
  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
@@ -75,6 +75,7 @@ command :wiki do |c|
75
75
  File.open(File.join('doing_wiki', 'index.html'), 'w') do |f|
76
76
  f.puts index_out
77
77
  end
78
+ Doing.logger.warn("Wiki written to doing_wiki directory")
78
79
  end
79
80
  end
80
81
  end
@@ -149,7 +149,7 @@ module Doing
149
149
  finished_at = i.end_date
150
150
  took += finished_at.strftime('%A %B %e at %I:%M%p')
151
151
 
152
- d, h, m = wwid.fmt_time(interval)
152
+ d, h, m = wwid.format_time(interval)
153
153
  took += ' and it took'
154
154
  took += " #{d.to_i} days" if d.to_i.positive?
155
155
  took += " #{h.to_i} hours" if h.to_i.positive?