doing 2.0.5.pre → 2.0.6.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 +20 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/bin/doing +133 -98
- data/doing.rdoc +88 -8
- 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/errors.rb +22 -15
- data/lib/doing/log_adapter.rb +27 -25
- data/lib/doing/plugin_manager.rb +1 -1
- data/lib/doing/string.rb +7 -6
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +83 -76
- data/lib/examples/commands/autotag.rb +63 -0
- data/scripts/generate_bash_completions.rb +3 -2
- data/scripts/generate_fish_completions.rb +4 -1
- data/scripts/generate_zsh_completions.rb +42 -38
- metadata +2 -2
- data/doing.fish +0 -278
data/lib/doing/plugin_manager.rb
CHANGED
@@ -113,7 +113,7 @@ module Doing
|
|
113
113
|
##
|
114
114
|
def list_plugins(options = {})
|
115
115
|
separator = options[:column] ? "\n" : "\t"
|
116
|
-
type = options[:type].nil? || options[:type] =~ /all/i ? 'all' :
|
116
|
+
type = options[:type].nil? || options[:type] =~ /all/i ? 'all' : valid_type(options[:type])
|
117
117
|
|
118
118
|
case type
|
119
119
|
when :import
|
data/lib/doing/string.rb
CHANGED
@@ -209,11 +209,12 @@ module Doing
|
|
209
209
|
title
|
210
210
|
end
|
211
211
|
|
212
|
-
def tag!(tag, value: nil, remove: false, rename_to: nil, regex: false)
|
213
|
-
replace tag(tag, value: value, remove: remove, rename_to: rename_to, regex: regex)
|
212
|
+
def tag!(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false)
|
213
|
+
replace tag(tag, value: value, remove: remove, rename_to: rename_to, regex: regex, single: single)
|
214
214
|
end
|
215
215
|
|
216
|
-
def tag(tag, value: nil, remove: false, rename_to: nil, regex: false)
|
216
|
+
def tag(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false)
|
217
|
+
log_level = single ? :info : :debug
|
217
218
|
title = dup
|
218
219
|
title.chomp!
|
219
220
|
tag = tag.sub(/^@?/, '')
|
@@ -241,10 +242,10 @@ module Doing
|
|
241
242
|
if rename_to
|
242
243
|
f = "@#{tag}".cyan
|
243
244
|
t = "@#{rename_to}".cyan
|
244
|
-
Doing.logger.
|
245
|
+
Doing.logger.write(log_level, 'Tag:', %(renamed #{f} to #{t} in "#{title}"))
|
245
246
|
else
|
246
247
|
f = "@#{tag}".cyan
|
247
|
-
Doing.logger.
|
248
|
+
Doing.logger.write(log_level, 'Tag:', %(removed #{f} from "#{title}"))
|
248
249
|
end
|
249
250
|
else
|
250
251
|
Doing.logger.debug('Skipped:', "not tagged #{"@#{tag}".cyan}")
|
@@ -260,7 +261,7 @@ module Doing
|
|
260
261
|
|
261
262
|
title.dedup_tags!
|
262
263
|
title.chomp!
|
263
|
-
|
264
|
+
Doing.logger.write(log_level, 'Tag:', %(added #{('@' + tag).cyan} to "#{title}"))
|
264
265
|
end
|
265
266
|
|
266
267
|
title.gsub(/ +/, ' ')
|
data/lib/doing/version.rb
CHANGED
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
|
@@ -1185,7 +1187,7 @@ module Doing
|
|
1185
1187
|
end
|
1186
1188
|
end
|
1187
1189
|
|
1188
|
-
log_change(tags_added: added, tags_removed: removed)
|
1190
|
+
log_change(tags_added: added, tags_removed: removed, item: item, single: items.count == 1)
|
1189
1191
|
|
1190
1192
|
item.note.add(opt[:note]) if opt[:note]
|
1191
1193
|
|
@@ -1217,7 +1219,7 @@ module Doing
|
|
1217
1219
|
@content[section][:items].concat([new_item])
|
1218
1220
|
|
1219
1221
|
logger.count(section == 'Archive' ? :archived : :moved)
|
1220
|
-
logger.debug("
|
1222
|
+
logger.debug("#{section == 'Archive' ? 'Archived' : 'Moved'}:",
|
1221
1223
|
"#{new_item.title.truncate(60)} from #{from} to #{section}")
|
1222
1224
|
new_item
|
1223
1225
|
end
|
@@ -1246,7 +1248,7 @@ module Doing
|
|
1246
1248
|
section_items = @content[section][:items]
|
1247
1249
|
deleted = section_items.delete(item)
|
1248
1250
|
logger.count(:deleted)
|
1249
|
-
logger.
|
1251
|
+
logger.info('Entry deleted:', deleted.title)
|
1250
1252
|
end
|
1251
1253
|
|
1252
1254
|
##
|
@@ -1261,16 +1263,13 @@ module Doing
|
|
1261
1263
|
section_items = @content[section][:items]
|
1262
1264
|
s_idx = section_items.index { |item| item.equal?(old_item) }
|
1263
1265
|
|
1264
|
-
unless s_idx
|
1265
|
-
Doing.logger.error('Fail to update:', 'Could not find item in index')
|
1266
|
-
raise Errors::ItemNotFound, 'Unable to find item in index, did it mutate?'
|
1267
|
-
end
|
1266
|
+
raise ItemNotFound, 'Unable to find item in index, did it mutate?' unless s_idx
|
1268
1267
|
|
1269
1268
|
return if section_items[s_idx].equal?(new_item)
|
1270
1269
|
|
1271
1270
|
section_items[s_idx] = new_item
|
1272
1271
|
logger.count(:updated)
|
1273
|
-
logger.
|
1272
|
+
logger.info('Entry updated:', section_items[s_idx].title.truncate(60))
|
1274
1273
|
new_item
|
1275
1274
|
end
|
1276
1275
|
|
@@ -1343,10 +1342,10 @@ module Doing
|
|
1343
1342
|
item.title = item.title.sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{item.section})")
|
1344
1343
|
move_item(item, 'Archive', label: false)
|
1345
1344
|
logger.count(:completed_archived)
|
1346
|
-
logger.
|
1345
|
+
logger.info('Completed/archived:', item.title)
|
1347
1346
|
else
|
1348
1347
|
logger.count(:completed)
|
1349
|
-
logger.
|
1348
|
+
logger.info('Completed:', item.title)
|
1350
1349
|
end
|
1351
1350
|
end
|
1352
1351
|
|
@@ -1478,10 +1477,10 @@ module Doing
|
|
1478
1477
|
if File.exist?(file)
|
1479
1478
|
init_doing_file(file)
|
1480
1479
|
@content.deep_merge(new_content)
|
1481
|
-
logger.warn('File update:', "
|
1480
|
+
logger.warn('File update:', "added entries to existing file: #{file}")
|
1482
1481
|
else
|
1483
1482
|
@content = new_content
|
1484
|
-
logger.warn('File update:', "
|
1483
|
+
logger.warn('File update:', "created new file: #{file}")
|
1485
1484
|
end
|
1486
1485
|
|
1487
1486
|
write(file, backup: false)
|
@@ -1572,10 +1571,7 @@ module Doing
|
|
1572
1571
|
opt[:multiple] = true
|
1573
1572
|
selected = choose_from_items(items, opt, include_section: opt[:section] =~ /^all$/i )
|
1574
1573
|
|
1575
|
-
if selected.empty?
|
1576
|
-
logger.debug('Skipped:', 'No selection')
|
1577
|
-
return
|
1578
|
-
end
|
1574
|
+
raise NoResults, 'no items selected' if selected.empty?
|
1579
1575
|
|
1580
1576
|
act_on(selected, opt)
|
1581
1577
|
return
|
@@ -1592,7 +1588,7 @@ module Doing
|
|
1592
1588
|
def output(items, title, is_single, opt = {})
|
1593
1589
|
out = nil
|
1594
1590
|
|
1595
|
-
raise
|
1591
|
+
raise InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
|
1596
1592
|
|
1597
1593
|
export_options = { page_title: title, is_single: is_single, options: opt }
|
1598
1594
|
|
@@ -1646,7 +1642,7 @@ module Doing
|
|
1646
1642
|
do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label], before: options[:before] })
|
1647
1643
|
write(doing_file)
|
1648
1644
|
else
|
1649
|
-
raise
|
1645
|
+
raise InvalidArgument, 'Either source or destination does not exist'
|
1650
1646
|
end
|
1651
1647
|
end
|
1652
1648
|
|
@@ -1705,7 +1701,12 @@ module Doing
|
|
1705
1701
|
@content[section][:items] = items
|
1706
1702
|
@content[destination][:items].concat(moved_items)
|
1707
1703
|
if moved_items.length.positive?
|
1708
|
-
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')
|
1709
1710
|
end
|
1710
1711
|
else
|
1711
1712
|
count = items.length if items.length < count
|
@@ -1733,10 +1734,11 @@ module Doing
|
|
1733
1734
|
else
|
1734
1735
|
items[0..count - 1]
|
1735
1736
|
end
|
1737
|
+
|
1736
1738
|
logger.count(destination == 'Archive' ? :archived : :moved,
|
1739
|
+
level: :info,
|
1737
1740
|
count: items.length - count,
|
1738
1741
|
message: "%count %items from #{section} to #{destination}")
|
1739
|
-
# logger.info('Archived:', "#{items.length - count} items from #{section} to #{destination}")
|
1740
1742
|
end
|
1741
1743
|
end
|
1742
1744
|
end
|
@@ -1935,11 +1937,11 @@ module Doing
|
|
1935
1937
|
end
|
1936
1938
|
end
|
1937
1939
|
|
1938
|
-
logger.debug('Autotag:', "
|
1940
|
+
logger.debug('Autotag:', "whitelisted tags: #{whitelisted.join(', ')}") unless whitelisted.empty?
|
1939
1941
|
new_tags = whitelisted
|
1940
1942
|
unless tail_tags.empty?
|
1941
1943
|
tags = tail_tags.uniq.map { |t| "@#{t}".cyan }.join(' ')
|
1942
|
-
logger.debug('Autotag:', "
|
1944
|
+
logger.debug('Autotag:', "synonym tags: #{tags}")
|
1943
1945
|
tags_a = tail_tags.map { |t| "@#{t}" }
|
1944
1946
|
text.add_tags!(tags_a.join(' '))
|
1945
1947
|
new_tags.concat(tags_a)
|
@@ -1948,7 +1950,7 @@ module Doing
|
|
1948
1950
|
unless text == original
|
1949
1951
|
logger.info('Autotag:', "added #{new_tags.join(', ')} to \"#{text}\"")
|
1950
1952
|
else
|
1951
|
-
logger.debug('
|
1953
|
+
logger.debug('Skipped:', "no change to \"#{text}\"")
|
1952
1954
|
end
|
1953
1955
|
|
1954
1956
|
text
|
@@ -2174,23 +2176,28 @@ EOS
|
|
2174
2176
|
logger.log_now(:error, 'STDERR output:', stderr)
|
2175
2177
|
end
|
2176
2178
|
|
2177
|
-
def log_change(tags_added: [], tags_removed: [], count: 1)
|
2179
|
+
def log_change(tags_added: [], tags_removed: [], count: 1, item: nil, single: false)
|
2178
2180
|
if tags_added.empty? && tags_removed.empty?
|
2179
2181
|
logger.count(:skipped, level: :debug, message: '%count %items with no change', count: count)
|
2180
2182
|
else
|
2181
|
-
|
2182
2183
|
if tags_added.empty?
|
2183
2184
|
logger.count(:skipped, level: :debug, message: 'no tags added to %count %items')
|
2184
|
-
# logger.debug('No tags added:', %("#{item.title}" in #{item.section}))
|
2185
2185
|
else
|
2186
|
-
|
2187
|
-
|
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
|
2188
2191
|
end
|
2189
2192
|
|
2190
2193
|
if tags_removed.empty?
|
2191
2194
|
logger.count(:skipped, level: :debug, message: 'no tags removed from %count %items')
|
2192
2195
|
else
|
2193
|
-
|
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
|
2194
2201
|
end
|
2195
2202
|
end
|
2196
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
|
@@ -82,7 +82,7 @@ class BashCompletions
|
|
82
82
|
fi
|
83
83
|
}
|
84
84
|
EOFUNC
|
85
|
-
|
85
|
+
clear
|
86
86
|
out.join("\n")
|
87
87
|
end
|
88
88
|
|
@@ -151,7 +151,7 @@ class BashCompletions
|
|
151
151
|
|
152
152
|
|
153
153
|
def get_help_sections(command = '')
|
154
|
-
res = `
|
154
|
+
res = `doing help #{command}`.strip
|
155
155
|
scanned = res.scan(/(?m-i)^([A-Z ]+)\n([\s\S]*?)(?=\n+[A-Z]+|\Z)/)
|
156
156
|
sections = {}
|
157
157
|
scanned.each do |sect|
|
@@ -203,6 +203,7 @@ class BashCompletions
|
|
203
203
|
out = []
|
204
204
|
out << main_function
|
205
205
|
out << 'complete -F _doing doing'
|
206
|
+
status('Complete', reset: false)
|
206
207
|
out.join("\n")
|
207
208
|
end
|
208
209
|
end
|
@@ -86,11 +86,12 @@ class FishCompletions
|
|
86
86
|
complete -f -c doing -n '__fish_doing_using_command template' -a '(__fish_doing_complete_templates)'
|
87
87
|
complete -f -c doing -s t -l type -x -n '__fish_doing_using_command import' -a '(__fish_doing_import_plugins)'
|
88
88
|
|
89
|
+
complete -xc doing -n '__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from (doing help -c)' -a "(doing help -c)"
|
89
90
|
EOFUNCTIONS
|
90
91
|
end
|
91
92
|
|
92
93
|
def get_help_sections(command = '')
|
93
|
-
res = `
|
94
|
+
res = `doing help #{command}`.strip
|
94
95
|
scanned = res.scan(/(?m-i)^([A-Z ]+)\n([\s\S]*?)(?=\n+[A-Z]+|\Z)/)
|
95
96
|
sections = {}
|
96
97
|
scanned.each do |sect|
|
@@ -179,6 +180,7 @@ class FishCompletions
|
|
179
180
|
out << "complete -f -c doing -s o -l output -x -n '__fish_doing_using_command #{need_export.join(' ')}' -a '(__fish_doing_export_plugins)'"
|
180
181
|
end
|
181
182
|
|
183
|
+
clear
|
182
184
|
out.join("\n")
|
183
185
|
end
|
184
186
|
|
@@ -194,6 +196,7 @@ class FishCompletions
|
|
194
196
|
out << generate_helpers
|
195
197
|
out << generate_subcommand_completions
|
196
198
|
out << generate_subcommand_option_completions
|
199
|
+
status('Complete', reset: false)
|
197
200
|
out.join("\n")
|
198
201
|
end
|
199
202
|
end
|