doing 2.0.2.pre → 2.0.7.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: '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?