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.
- 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?
|