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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -1
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/bin/doing +143 -108
- data/doing.rdoc +98 -18
- data/example_plugin.rb +1 -1
- data/generate_completions.sh +1 -0
- data/lib/completion/_doing.zsh +179 -127
- data/lib/completion/doing.bash +60 -27
- data/lib/completion/doing.fish +74 -23
- data/lib/doing/cli_status.rb +4 -0
- data/lib/doing/configuration.rb +2 -0
- data/lib/doing/errors.rb +22 -15
- data/lib/doing/log_adapter.rb +27 -25
- data/lib/doing/plugin_manager.rb +1 -1
- data/lib/doing/plugins/export/json_export.rb +2 -2
- data/lib/doing/plugins/export/template_export.rb +1 -1
- data/lib/doing/string.rb +9 -7
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +134 -88
- data/lib/examples/commands/autotag.rb +63 -0
- data/lib/examples/commands/wiki.rb +1 -0
- data/lib/examples/plugins/say_export.rb +1 -1
- data/scripts/generate_bash_completions.rb +3 -2
- data/scripts/generate_fish_completions.rb +4 -1
- data/scripts/generate_zsh_completions.rb +44 -39
- metadata +3 -3
- data/doing.fish +0 -278
data/lib/doing/wwid.rb
CHANGED
@@ -123,9 +123,9 @@ module Doing
|
|
123
123
|
## @param input (String) Text input for editor
|
124
124
|
##
|
125
125
|
def fork_editor(input = '')
|
126
|
-
# raise
|
126
|
+
# raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
127
127
|
|
128
|
-
raise
|
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
|
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
|
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
|
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
|
-
|
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
|
315
|
-
|
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
|
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
|
-
|
402
|
-
|
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
|
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.
|
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
|
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
|
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
|
-
|
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.
|
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.
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
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.
|
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("
|
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.
|
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.
|
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.
|
1345
|
+
logger.info('Completed/archived:', item.title)
|
1346
1346
|
else
|
1347
1347
|
logger.count(:completed)
|
1348
|
-
logger.
|
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:', "
|
1480
|
+
logger.warn('File update:', "added entries to existing file: #{file}")
|
1481
1481
|
else
|
1482
1482
|
@content = new_content
|
1483
|
-
logger.warn('File update:', "
|
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
|
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
|
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.
|
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:', "
|
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:', "
|
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('
|
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' %
|
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' %
|
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' %
|
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 =
|
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 =
|
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 =
|
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', *
|
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
|
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
|
-
|
2121
|
-
|
2122
|
-
|
2123
|
-
|
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
|
-
|
2148
|
-
|
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
|
-
|
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
|
@@ -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.
|
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?
|