doing 2.0.22 → 2.1.0pre
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/.yardoc/checksums +18 -15
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +36 -1
- data/Gemfile.lock +8 -1
- data/README.md +7 -1
- data/Rakefile +23 -4
- data/bin/doing +323 -173
- data/doc/Array.html +354 -1
- data/doc/Doing/Color.html +104 -92
- data/doc/Doing/Completion.html +216 -0
- data/doc/Doing/Configuration.html +340 -5
- data/doc/Doing/Content.html +229 -0
- data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/doc/Doing/Errors/EmptyInput.html +1 -1
- data/doc/Doing/Errors/NoResults.html +1 -1
- data/doc/Doing/Errors/PluginException.html +1 -1
- data/doc/Doing/Errors/UserCancelled.html +1 -1
- data/doc/Doing/Errors/WrongCommand.html +1 -1
- data/doc/Doing/Errors.html +1 -1
- data/doc/Doing/Hooks.html +1 -1
- data/doc/Doing/Item.html +337 -49
- data/doc/Doing/Items.html +444 -35
- data/doc/Doing/LogAdapter.html +139 -51
- data/doc/Doing/Note.html +253 -22
- data/doc/Doing/Pager.html +74 -36
- data/doc/Doing/Plugins.html +1 -1
- data/doc/Doing/Prompt.html +674 -0
- data/doc/Doing/Section.html +354 -0
- data/doc/Doing/Util.html +57 -1
- data/doc/Doing/WWID.html +477 -670
- data/doc/Doing/WWIDFile.html +398 -0
- data/doc/Doing.html +5 -5
- data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/doc/GLI/Commands.html +1 -1
- data/doc/GLI.html +1 -1
- data/doc/Hash.html +97 -1
- data/doc/Status.html +37 -3
- data/doc/String.html +599 -23
- data/doc/Symbol.html +3 -3
- data/doc/Time.html +1 -1
- data/doc/_index.html +22 -1
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +8 -2
- data/doc/index.html +8 -2
- data/doc/method_list.html +453 -173
- data/doc/top-level-namespace.html +1 -1
- data/doing.gemspec +3 -0
- data/doing.rdoc +79 -27
- data/example_plugin.rb +5 -5
- data/lib/completion/_doing.zsh +42 -42
- data/lib/completion/doing.bash +10 -10
- data/lib/completion/doing.fish +1 -280
- data/lib/doing/array.rb +36 -0
- data/lib/doing/colors.rb +70 -66
- data/lib/doing/completion/bash_completion.rb +1 -2
- data/lib/doing/completion/fish_completion.rb +1 -1
- data/lib/doing/completion/zsh_completion.rb +1 -1
- data/lib/doing/completion.rb +6 -0
- data/lib/doing/configuration.rb +134 -23
- data/lib/doing/hash.rb +37 -0
- data/lib/doing/item.rb +77 -12
- data/lib/doing/items.rb +125 -0
- data/lib/doing/log_adapter.rb +58 -4
- data/lib/doing/note.rb +53 -1
- data/lib/doing/pager.rb +49 -38
- data/lib/doing/plugins/export/markdown_export.rb +4 -4
- data/lib/doing/plugins/export/template_export.rb +2 -2
- data/lib/doing/plugins/import/calendar_import.rb +4 -4
- data/lib/doing/plugins/import/doing_import.rb +5 -7
- data/lib/doing/plugins/import/timing_import.rb +3 -3
- data/lib/doing/prompt.rb +206 -0
- data/lib/doing/section.rb +30 -0
- data/lib/doing/string.rb +123 -35
- data/lib/doing/util.rb +14 -6
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +307 -614
- data/lib/doing.rb +6 -2
- data/lib/examples/plugins/capture_thing_import.rb +162 -0
- data/rdoc_to_mmd.rb +14 -8
- data/scripts/generate_bash_completions.rb +1 -1
- data/scripts/generate_fish_completions.rb +1 -1
- data/scripts/generate_zsh_completions.rb +1 -1
- metadata +73 -5
- data/lib/doing/wwidfile.rb +0 -117
data/lib/doing/wwid.rb
CHANGED
@@ -16,6 +16,7 @@ module Doing
|
|
16
16
|
|
17
17
|
attr_accessor :config, :config_file, :auto_tag, :default_option
|
18
18
|
|
19
|
+
include Color
|
19
20
|
# include Util
|
20
21
|
|
21
22
|
##
|
@@ -24,11 +25,8 @@ module Doing
|
|
24
25
|
def initialize
|
25
26
|
@timers = {}
|
26
27
|
@recorded_items = []
|
27
|
-
@content =
|
28
|
-
@doingrc_needs_update = false
|
29
|
-
@default_config_file = '.doingrc'
|
28
|
+
@content = Items.new
|
30
29
|
@auto_tag = true
|
31
|
-
@user_home = Util.user_home
|
32
30
|
end
|
33
31
|
|
34
32
|
##
|
@@ -56,52 +54,56 @@ module Doing
|
|
56
54
|
create(@doing_file) unless File.exist?(@doing_file)
|
57
55
|
input = IO.read(@doing_file)
|
58
56
|
input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
|
57
|
+
logger.debug('Read:', "read file #{@doing_file}")
|
59
58
|
elsif File.exist?(File.expand_path(path)) && File.file?(File.expand_path(path)) && File.stat(File.expand_path(path)).size.positive?
|
60
59
|
@doing_file = File.expand_path(path)
|
61
60
|
input = IO.read(File.expand_path(path))
|
62
61
|
input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
|
62
|
+
logger.debug('Read:', "read file #{File.expand_path(path)}")
|
63
63
|
elsif path.length < 256
|
64
64
|
@doing_file = File.expand_path(path)
|
65
65
|
create(path)
|
66
66
|
input = IO.read(File.expand_path(path))
|
67
67
|
input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
|
68
|
+
logger.debug('Read:', "read file #{File.expand_path(path)}")
|
68
69
|
end
|
69
70
|
|
70
71
|
@other_content_top = []
|
71
72
|
@other_content_bottom = []
|
72
73
|
|
73
|
-
section =
|
74
|
+
section = nil
|
74
75
|
lines = input.split(/[\n\r]/)
|
75
|
-
current = 0
|
76
76
|
|
77
77
|
lines.each do |line|
|
78
78
|
next if line =~ /^\s*$/
|
79
79
|
|
80
80
|
if line =~ /^(\S[\S ]+):\s*(@\S+\s*)*$/
|
81
81
|
section = Regexp.last_match(1)
|
82
|
-
@content
|
83
|
-
@content[section][:original] = line
|
84
|
-
@content[section][:items] = []
|
85
|
-
current = 0
|
82
|
+
@content.add_section(Section.new(section, original: line), log: false)
|
86
83
|
elsif line =~ /^\s*- (\d{4}-\d\d-\d\d \d\d:\d\d) \| (.*)/
|
84
|
+
if section.nil?
|
85
|
+
section = 'Uncategorized'
|
86
|
+
@content.add_section(Section.new(section, original: 'Uncategorized:'), log: false)
|
87
|
+
end
|
88
|
+
|
87
89
|
date = Regexp.last_match(1).strip
|
88
90
|
title = Regexp.last_match(2).strip
|
89
91
|
item = Item.new(date, title, section)
|
90
|
-
@content
|
91
|
-
|
92
|
-
|
93
|
-
# if content[section][:items].length - 1 == current
|
92
|
+
@content.push(item)
|
93
|
+
elsif @content.count.zero?
|
94
|
+
# if content[section].items.length - 1 == current
|
94
95
|
@other_content_top.push(line)
|
95
96
|
elsif line =~ /^\S/
|
96
97
|
@other_content_bottom.push(line)
|
97
98
|
else
|
98
|
-
prev_item = @content
|
99
|
+
prev_item = @content.last
|
99
100
|
prev_item.note = Note.new unless prev_item.note
|
100
101
|
|
101
102
|
prev_item.note.add(line)
|
102
103
|
# end
|
103
104
|
end
|
104
105
|
end
|
106
|
+
|
105
107
|
Hooks.trigger :post_read, self
|
106
108
|
end
|
107
109
|
|
@@ -122,7 +124,7 @@ module Doing
|
|
122
124
|
##
|
123
125
|
## @param input [String] Text input for editor
|
124
126
|
##
|
125
|
-
def fork_editor(input = '')
|
127
|
+
def fork_editor(input = '', message: :default)
|
126
128
|
# raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
127
129
|
|
128
130
|
raise MissingEditor, 'No EDITOR variable defined in environment' if Util.default_editor.nil?
|
@@ -131,7 +133,9 @@ module Doing
|
|
131
133
|
|
132
134
|
File.open(tmpfile.path, 'w+') do |f|
|
133
135
|
f.puts input
|
134
|
-
|
136
|
+
unless message.nil?
|
137
|
+
f.puts message == :default ? "# The first line is the entry title, any lines after that are added as a note" : message
|
138
|
+
end
|
135
139
|
end
|
136
140
|
|
137
141
|
pid = Process.fork { system("#{Util.editor_with_args} #{tmpfile.path}") }
|
@@ -177,13 +181,37 @@ module Doing
|
|
177
181
|
title = input_lines[0]&.strip
|
178
182
|
raise EmptyInput, 'No content in first line' if title.nil? || title.strip.empty?
|
179
183
|
|
184
|
+
date = nil
|
185
|
+
iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/
|
186
|
+
done_rx = /(?<=^| )@(?<tag>done|finished|completed?)\((?<date>.*?)\)/i
|
187
|
+
date_rx = /^(?:\s*- )?(?<date>.*?) \| (?=\S)/
|
188
|
+
|
189
|
+
title.gsub!(done_rx) do
|
190
|
+
m = Regexp.last_match
|
191
|
+
t = m['tag']
|
192
|
+
d = m['date']
|
193
|
+
parsed_date = d =~ date_rx ? Time.parse(d) : chronify(d, guess: :begin)
|
194
|
+
parsed_date.nil? ? m[0] : "@#{t}(#{parsed_date.strftime('%F %R')})"
|
195
|
+
end
|
196
|
+
|
197
|
+
if title =~ date_rx
|
198
|
+
m = title.match(date_rx)
|
199
|
+
d = m['date']
|
200
|
+
date = if d =~ iso_rx
|
201
|
+
Time.parse(d)
|
202
|
+
else
|
203
|
+
chronify(d, guess: :begin)
|
204
|
+
end
|
205
|
+
title.sub!(date_rx, '').strip!
|
206
|
+
end
|
207
|
+
|
180
208
|
note = Note.new
|
181
209
|
note.add(input_lines[1..-1]) if input_lines.length > 1
|
182
210
|
# If title line ends in a parenthetical, use that as the note
|
183
211
|
if note.empty? && title =~ /\s+\(.*?\)$/
|
184
|
-
title.sub!(/\s+\((
|
212
|
+
title.sub!(/\s+\((?<note>.*?)\)$/) do
|
185
213
|
m = Regexp.last_match
|
186
|
-
note.add(m[
|
214
|
+
note.add(m['note'])
|
187
215
|
''
|
188
216
|
end
|
189
217
|
end
|
@@ -191,7 +219,7 @@ module Doing
|
|
191
219
|
note.strip_lines!
|
192
220
|
note.compress
|
193
221
|
|
194
|
-
[title, note]
|
222
|
+
[date, title, note]
|
195
223
|
end
|
196
224
|
|
197
225
|
##
|
@@ -265,21 +293,7 @@ module Doing
|
|
265
293
|
## @return [Array] section titles
|
266
294
|
##
|
267
295
|
def sections
|
268
|
-
@content.
|
269
|
-
end
|
270
|
-
|
271
|
-
##
|
272
|
-
## Adds a section.
|
273
|
-
##
|
274
|
-
## @param title [String] The new section title
|
275
|
-
##
|
276
|
-
def add_section(title)
|
277
|
-
if @content.key?(title.cap_first)
|
278
|
-
raise InvalidSection, %(section "#{title.cap_first}" already exists)
|
279
|
-
end
|
280
|
-
|
281
|
-
@content[title.cap_first] = { :original => "#{title}:", :items => [] }
|
282
|
-
logger.info('New section:', %("#{title.cap_first}"))
|
296
|
+
@content.section_titles
|
283
297
|
end
|
284
298
|
|
285
299
|
##
|
@@ -292,8 +306,9 @@ module Doing
|
|
292
306
|
return 'All' if frag =~ /^all$/i
|
293
307
|
frag ||= @config['current_section']
|
294
308
|
|
295
|
-
|
296
|
-
|
309
|
+
return frag.cap_first if @content.section?(frag)
|
310
|
+
|
311
|
+
section = nil
|
297
312
|
re = frag.split('').join('.*?')
|
298
313
|
sections.each do |sect|
|
299
314
|
next unless sect =~ /#{re}/i
|
@@ -308,79 +323,25 @@ module Doing
|
|
308
323
|
unless section || guessed
|
309
324
|
alt = guess_view(frag, guessed: true, suggest: true)
|
310
325
|
if alt
|
311
|
-
meant_view = yn("#{
|
326
|
+
meant_view = Prompt.yn("#{boldwhite("Did you mean")} `#{yellow("doing view #{alt}")}#{boldwhite}`?", default_response: 'n')
|
312
327
|
|
313
328
|
raise WrongCommand.new("run again with #{"doing view #{alt}".boldwhite}", topic: 'Try again:') if meant_view
|
314
329
|
|
315
330
|
end
|
316
331
|
|
317
|
-
res = yn("#{
|
332
|
+
res = Prompt.yn("#{boldwhite}Section #{frag.yellow}#{boldwhite} not found, create it", default_response: 'n')
|
318
333
|
|
319
334
|
if res
|
320
|
-
add_section(frag.cap_first)
|
335
|
+
@content.add_section(frag.cap_first, log: true)
|
321
336
|
write(@doing_file)
|
322
337
|
return frag.cap_first
|
323
338
|
end
|
324
339
|
|
325
|
-
raise InvalidSection.new("unknown section #{frag.
|
340
|
+
raise InvalidSection.new("unknown section #{frag.bold.white}", topic: 'Missing:')
|
326
341
|
end
|
327
342
|
section ? section.cap_first : guessed
|
328
343
|
end
|
329
344
|
|
330
|
-
##
|
331
|
-
## Ask a yes or no question in the terminal
|
332
|
-
##
|
333
|
-
## @param question [String] The question
|
334
|
-
## to ask
|
335
|
-
## @param default_response (Bool) default
|
336
|
-
## response if no input
|
337
|
-
##
|
338
|
-
## @return (Bool) yes or no
|
339
|
-
##
|
340
|
-
def yn(question, default_response: false)
|
341
|
-
if default_response.is_a?(String)
|
342
|
-
default = default_response =~ /y/i ? true : false
|
343
|
-
else
|
344
|
-
default = default_response
|
345
|
-
end
|
346
|
-
|
347
|
-
# if global --default is set, answer default
|
348
|
-
return default if @default_option
|
349
|
-
|
350
|
-
# if this isn't an interactive shell, answer default
|
351
|
-
return default unless $stdout.isatty
|
352
|
-
|
353
|
-
# clear the buffer
|
354
|
-
if ARGV&.length
|
355
|
-
ARGV.length.times do
|
356
|
-
ARGV.shift
|
357
|
-
end
|
358
|
-
end
|
359
|
-
system 'stty cbreak'
|
360
|
-
|
361
|
-
cw = Color.white
|
362
|
-
cbw = Color.boldwhite
|
363
|
-
cbg = Color.boldgreen
|
364
|
-
cd = Color.default
|
365
|
-
|
366
|
-
options = unless default.nil?
|
367
|
-
"#{cw}[#{default ? "#{cbg}Y#{cw}/#{cbw}n" : "#{cbw}y#{cw}/#{cbg}N"}#{cw}]#{cd}"
|
368
|
-
else
|
369
|
-
"#{cw}[#{cbw}y#{cw}/#{cbw}n#{cw}]#{cd}"
|
370
|
-
end
|
371
|
-
$stdout.syswrite "#{cbw}#{question.sub(/\?$/, '')} #{options}#{cbw}?#{cd} "
|
372
|
-
res = $stdin.sysread 1
|
373
|
-
puts
|
374
|
-
system 'stty cooked'
|
375
|
-
|
376
|
-
res.chomp!
|
377
|
-
res.downcase!
|
378
|
-
|
379
|
-
return default if res.empty?
|
380
|
-
|
381
|
-
res =~ /y/i ? true : false
|
382
|
-
end
|
383
|
-
|
384
345
|
##
|
385
346
|
## Attempt to match a string with an existing view
|
386
347
|
##
|
@@ -400,11 +361,14 @@ module Doing
|
|
400
361
|
end
|
401
362
|
unless view || guessed
|
402
363
|
alt = guess_section(frag, guessed: true, suggest: true)
|
403
|
-
|
364
|
+
|
365
|
+
raise InvalidView.new(%(unknown view #{frag.bold.white}), topic: 'Missing:') unless alt
|
366
|
+
|
367
|
+
meant_view = Prompt.yn("Did you mean `doing show #{alt}`?", default_response: 'n')
|
404
368
|
|
405
369
|
raise WrongCommand.new("run again with #{"doing show #{alt}".yellow}", topic: 'Try again:') if meant_view
|
406
370
|
|
407
|
-
raise InvalidView.new(%(
|
371
|
+
raise InvalidView.new(%(unknown view #{alt.bold.white}), topic: 'Missing:')
|
408
372
|
end
|
409
373
|
view
|
410
374
|
end
|
@@ -414,17 +378,22 @@ module Doing
|
|
414
378
|
##
|
415
379
|
## @param title [String] The entry title
|
416
380
|
## @param section [String] The section to add to
|
417
|
-
## @param opt [Hash] Additional Options
|
381
|
+
## @param opt [Hash] Additional Options
|
382
|
+
##
|
383
|
+
## @option opt :date [Date] item start date
|
384
|
+
## @option opt :note [Array] item note (will be converted if value is String)
|
385
|
+
## @option opt :back [Date] backdate
|
386
|
+
## @option opt :timed [Boolean] new item is timed entry, marks previous entry as @done
|
418
387
|
##
|
419
388
|
def add_item(title, section = nil, opt = {})
|
420
389
|
section ||= @config['current_section']
|
421
|
-
add_section(section
|
390
|
+
@content.add_section(section, log: false)
|
391
|
+
opt[:back] ||= opt[:date] ? opt[:date] : Time.now
|
422
392
|
opt[:date] ||= Time.now
|
423
|
-
|
424
|
-
opt[:back] ||= Time.now
|
393
|
+
note = Note.new
|
425
394
|
opt[:timed] ||= false
|
426
395
|
|
427
|
-
|
396
|
+
note.add(opt[:note]) if opt[:note]
|
428
397
|
|
429
398
|
title = [title.strip.cap_first]
|
430
399
|
title = title.join(' ')
|
@@ -434,10 +403,11 @@ module Doing
|
|
434
403
|
title.add_tags!(@config['default_tags']) unless @config['default_tags'].empty?
|
435
404
|
end
|
436
405
|
|
437
|
-
title.
|
406
|
+
title.compress!
|
438
407
|
entry = Item.new(opt[:back], title.strip, section)
|
439
|
-
entry.note =
|
440
|
-
|
408
|
+
entry.note = note
|
409
|
+
|
410
|
+
items = @content.dup
|
441
411
|
if opt[:timed]
|
442
412
|
items.reverse!
|
443
413
|
items.each_with_index do |i, x|
|
@@ -446,10 +416,9 @@ module Doing
|
|
446
416
|
items[x].title = "#{i.title} @done(#{opt[:back].strftime('%F %R')})"
|
447
417
|
break
|
448
418
|
end
|
449
|
-
items.reverse!
|
450
419
|
end
|
451
420
|
|
452
|
-
|
421
|
+
@content.push(entry)
|
453
422
|
# logger.count(:added, level: :debug)
|
454
423
|
logger.info('New entry:', %(added "#{entry.title}" to #{section}))
|
455
424
|
end
|
@@ -460,16 +429,10 @@ module Doing
|
|
460
429
|
## @param items [Array] The items to deduplicate
|
461
430
|
## @param no_overlap [Boolean] Remove items with overlapping time spans
|
462
431
|
##
|
463
|
-
def dedup(items, no_overlap
|
464
|
-
|
465
|
-
combined = []
|
466
|
-
@content.each do |_k, v|
|
467
|
-
combined += v[:items]
|
468
|
-
end
|
469
|
-
|
432
|
+
def dedup(items, no_overlap: false)
|
470
433
|
items.delete_if do |item|
|
471
434
|
duped = false
|
472
|
-
|
435
|
+
@content.each do |comp|
|
473
436
|
duped = no_overlap ? item.overlapping_time?(comp) : item.same_time?(comp)
|
474
437
|
break if duped
|
475
438
|
end
|
@@ -519,17 +482,32 @@ module Doing
|
|
519
482
|
"#{last_item.title}\n# EDIT BELOW THIS LINE ------------\n#{note}"
|
520
483
|
end
|
521
484
|
|
485
|
+
# Reset start date to current time, optionally remove
|
486
|
+
# done tag (resume)
|
487
|
+
#
|
488
|
+
# @param item [Item] the item to reset/resume
|
489
|
+
# @param resume [Boolean] removing @done tag if true
|
490
|
+
#
|
522
491
|
def reset_item(item, resume: false)
|
523
492
|
item.date = Time.now
|
524
|
-
if resume
|
525
|
-
item.tag('done', remove: true)
|
526
|
-
end
|
493
|
+
item.tag('done', remove: true) if resume
|
527
494
|
logger.info('Reset:', %(Reset #{resume ? 'and resumed ' : ''} "#{item.title}" in #{item.section}))
|
528
495
|
item
|
529
496
|
end
|
530
497
|
|
498
|
+
# Duplicate an item and add it as a new item
|
499
|
+
#
|
500
|
+
# @param item [Item] the item to duplicate
|
501
|
+
# @param opt [Hash] additional options
|
502
|
+
#
|
503
|
+
# @option opt :editor [Boolean] open new item in editor
|
504
|
+
# @option opt :date [String] set start date
|
505
|
+
# @option opt :in [String] add new item to section :in
|
506
|
+
# @option opt :note [Note] add note to new item
|
507
|
+
#
|
508
|
+
# @return nothing
|
509
|
+
#
|
531
510
|
def repeat_item(item, opt = {})
|
532
|
-
original = item.dup
|
533
511
|
if item.should_finish?
|
534
512
|
if item.should_time?
|
535
513
|
item.title.tag!('done', value: Time.now.strftime('%F %R'))
|
@@ -546,10 +524,13 @@ module Doing
|
|
546
524
|
note = opt[:note] || Note.new
|
547
525
|
|
548
526
|
if opt[:editor]
|
549
|
-
|
550
|
-
to_edit
|
527
|
+
start = opt[:date] ? opt[:date] : Time.now
|
528
|
+
to_edit = "#{start.strftime('%F %R')} | #{title}"
|
529
|
+
to_edit += "\n#{note.strip_lines.join("\n")}" unless note.empty?
|
551
530
|
new_item = fork_editor(to_edit)
|
552
|
-
title, note = format_input(new_item)
|
531
|
+
date, title, note = format_input(new_item)
|
532
|
+
|
533
|
+
opt[:date] = date unless date.nil?
|
553
534
|
|
554
535
|
if title.nil? || title.empty?
|
555
536
|
logger.warn('Skipped:', 'No content provided')
|
@@ -557,9 +538,8 @@ module Doing
|
|
557
538
|
end
|
558
539
|
end
|
559
540
|
|
560
|
-
update_item(original, item)
|
541
|
+
# @content.update_item(original, item)
|
561
542
|
add_item(title, section, { note: note, back: opt[:date], timed: true })
|
562
|
-
write(@doing_file)
|
563
543
|
end
|
564
544
|
|
565
545
|
##
|
@@ -569,6 +549,7 @@ module Doing
|
|
569
549
|
##
|
570
550
|
def repeat_last(opt = {})
|
571
551
|
opt[:section] ||= 'all'
|
552
|
+
opt[:section] = guess_section(opt[:section])
|
572
553
|
opt[:note] ||= []
|
573
554
|
opt[:tag] ||= []
|
574
555
|
opt[:tag_bool] ||= :and
|
@@ -580,6 +561,7 @@ module Doing
|
|
580
561
|
end
|
581
562
|
|
582
563
|
repeat_item(last, opt)
|
564
|
+
write(@doing_file)
|
583
565
|
end
|
584
566
|
|
585
567
|
##
|
@@ -591,19 +573,19 @@ module Doing
|
|
591
573
|
opt[:tag_bool] ||= :and
|
592
574
|
opt[:section] ||= @config['current_section']
|
593
575
|
|
594
|
-
items = filter_items(
|
576
|
+
items = filter_items(Items.new, opt: opt)
|
595
577
|
|
596
578
|
logger.debug('Filtered:', "Parameters matched #{items.count} entries")
|
597
579
|
|
598
580
|
if opt[:interactive]
|
599
|
-
last_entry = choose_from_items(items,
|
581
|
+
last_entry = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i,
|
600
582
|
menu: true,
|
601
583
|
header: '',
|
602
584
|
prompt: 'Select an entry > ',
|
603
585
|
multiple: false,
|
604
586
|
sort: false,
|
605
587
|
show_if_single: true
|
606
|
-
|
588
|
+
)
|
607
589
|
else
|
608
590
|
last_entry = items.max_by { |item| item.date }
|
609
591
|
end
|
@@ -611,42 +593,6 @@ module Doing
|
|
611
593
|
last_entry
|
612
594
|
end
|
613
595
|
|
614
|
-
def fzf
|
615
|
-
fzf_dir = File.join(File.dirname(__FILE__), '../helpers/fzf')
|
616
|
-
FileUtils.mkdir_p(fzf_dir) unless File.directory?(fzf_dir)
|
617
|
-
fzf = File.join(fzf_dir, 'bin/fzf')
|
618
|
-
return fzf if File.exist?(fzf)
|
619
|
-
|
620
|
-
Doing.logger.log_now(:warn, 'Compiling and installing FZF. This will only happen once')
|
621
|
-
Doing.logger.log_now(:warn, 'fzf is copyright Junegunn Choi <https://github.com/junegunn/fzf/blob/master/LICENSE>')
|
622
|
-
|
623
|
-
res = `#{fzf_dir}/install --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish`
|
624
|
-
|
625
|
-
raise DoingRuntimeError unless File.exist?(fzf)
|
626
|
-
|
627
|
-
fzf
|
628
|
-
end
|
629
|
-
|
630
|
-
##
|
631
|
-
## Generate a menu of options and allow user selection
|
632
|
-
##
|
633
|
-
## @return [String] The selected option
|
634
|
-
##
|
635
|
-
def choose_from(options, prompt: 'Make a selection: ', multiple: false, sorted: true, fzf_args: [])
|
636
|
-
return nil unless $stdout.isatty
|
637
|
-
|
638
|
-
# fzf_args << '-1' # User is expecting a menu, and even if only one it seves as confirmation
|
639
|
-
fzf_args << %(--prompt "#{prompt}")
|
640
|
-
fzf_args << '--multi' if multiple
|
641
|
-
header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
|
642
|
-
fzf_args << %(--header "#{header}")
|
643
|
-
options.sort! if sorted
|
644
|
-
res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
|
645
|
-
return false if res.strip.size.zero?
|
646
|
-
|
647
|
-
res
|
648
|
-
end
|
649
|
-
|
650
596
|
def all_tags(items, opt: {})
|
651
597
|
all_tags = []
|
652
598
|
items.each { |item| all_tags.concat(item.tags).uniq! }
|
@@ -685,8 +631,8 @@ module Doing
|
|
685
631
|
end
|
686
632
|
# fzf_args << '-e' if opt[:exact]
|
687
633
|
# puts fzf_args.join(' ')
|
688
|
-
res = `echo #{Shellwords.escape(scannable)}|#{fzf} #{fzf_args.join(' ')}`
|
689
|
-
selected =
|
634
|
+
res = `echo #{Shellwords.escape(scannable)}|#{Prompt.fzf} #{fzf_args.join(' ')}`
|
635
|
+
selected = Items.new
|
690
636
|
res.split(/\n/).each do |item|
|
691
637
|
idx = item.match(/\|(\d+)$/)[1].to_i
|
692
638
|
selected.push(items[idx])
|
@@ -714,15 +660,11 @@ module Doing
|
|
714
660
|
## @option opt [Number] :count (Number to return)
|
715
661
|
## @option opt [String] :age ('old' or 'new')
|
716
662
|
##
|
717
|
-
def filter_items(items =
|
663
|
+
def filter_items(items = Items.new, opt: {})
|
718
664
|
if items.nil? || items.empty?
|
719
665
|
section = opt[:section] ? guess_section(opt[:section]) : 'All'
|
720
666
|
|
721
|
-
items =
|
722
|
-
@content.each_with_object([]) { |(_k, v), arr| arr.concat(v[:items].dup) }
|
723
|
-
else
|
724
|
-
@content[section][:items].dup
|
725
|
-
end
|
667
|
+
items = section =~ /^all$/i ? @content.dup : @content.in_section(section)
|
726
668
|
end
|
727
669
|
|
728
670
|
items.sort_by! { |item| [item.date, item.title.downcase] }.reverse
|
@@ -797,14 +739,17 @@ module Doing
|
|
797
739
|
|
798
740
|
keep
|
799
741
|
end
|
800
|
-
count = opt[:count]
|
742
|
+
count = opt[:count]&.positive? ? opt[:count] : filtered_items.length
|
743
|
+
|
744
|
+
output = Items.new
|
801
745
|
|
802
746
|
if opt[:age] =~ /^o/i
|
803
|
-
filtered_items.slice(0, count).reverse
|
747
|
+
output.concat(filtered_items.slice(0, count).reverse)
|
804
748
|
else
|
805
|
-
filtered_items.reverse.slice(0, count)
|
749
|
+
output.concat(filtered_items.reverse.slice(0, count))
|
806
750
|
end
|
807
751
|
|
752
|
+
output
|
808
753
|
end
|
809
754
|
|
810
755
|
##
|
@@ -829,94 +774,15 @@ module Doing
|
|
829
774
|
opt[:query] = "!#{opt[:query]}" if opt[:not]
|
830
775
|
opt[:multiple] = true
|
831
776
|
opt[:show_if_single] = true
|
832
|
-
items = filter_items(
|
777
|
+
items = filter_items(Items.new, opt: { section: section, search: opt[:search], fuzzy: opt[:fuzzy], case: opt[:case], not: opt[:not] })
|
833
778
|
|
834
|
-
selection = choose_from_items(items,
|
779
|
+
selection = Prompt.choose_from_items(items, include_section: section =~ /^all$/i, **opt)
|
835
780
|
|
836
781
|
raise NoResults, 'no items selected' if selection.nil? || selection.empty?
|
837
782
|
|
838
783
|
act_on(selection, opt)
|
839
784
|
end
|
840
785
|
|
841
|
-
##
|
842
|
-
## Create an interactive menu to select from a set of Items
|
843
|
-
##
|
844
|
-
## @param items [Array] list of items
|
845
|
-
## @param opt [Hash] options
|
846
|
-
## @param include_section [Boolean] include section
|
847
|
-
##
|
848
|
-
## @option opt [String] :header
|
849
|
-
## @option opt [String] :prompt
|
850
|
-
## @option opt [String] :query
|
851
|
-
## @option opt [Boolean] :show_if_single
|
852
|
-
## @option opt [Boolean] :menu
|
853
|
-
## @option opt [Boolean] :sort
|
854
|
-
## @option opt [Boolean] :multiple
|
855
|
-
## @option opt [Symbol] :case (:sensitive, :ignore, :smart)
|
856
|
-
##
|
857
|
-
def choose_from_items(items, opt = {}, include_section: false)
|
858
|
-
return items unless $stdout.isatty
|
859
|
-
|
860
|
-
return nil unless items.count.positive?
|
861
|
-
|
862
|
-
opt[:case] ||= :smart
|
863
|
-
opt[:header] ||= "Arrows: navigate, tab: mark for selection, ctrl-a: select all, enter: commit"
|
864
|
-
opt[:prompt] ||= "Select entries to act on > "
|
865
|
-
|
866
|
-
pad = items.length.to_s.length
|
867
|
-
options = items.map.with_index do |item, i|
|
868
|
-
out = [
|
869
|
-
format("%#{pad}d", i),
|
870
|
-
') ',
|
871
|
-
format('%13s', item.date.relative_date),
|
872
|
-
' | ',
|
873
|
-
item.title
|
874
|
-
]
|
875
|
-
if include_section
|
876
|
-
out.concat([
|
877
|
-
' (',
|
878
|
-
item.section,
|
879
|
-
') '
|
880
|
-
])
|
881
|
-
end
|
882
|
-
out.join('')
|
883
|
-
end
|
884
|
-
|
885
|
-
fzf_args = [
|
886
|
-
%(--header="#{opt[:header]}"),
|
887
|
-
%(--prompt="#{opt[:prompt].sub(/ *$/, ' ')}"),
|
888
|
-
opt[:multiple] ? '--multi' : '--no-multi',
|
889
|
-
'-0',
|
890
|
-
'--bind ctrl-a:select-all',
|
891
|
-
%(-q "#{opt[:query]}"),
|
892
|
-
'--info=inline'
|
893
|
-
]
|
894
|
-
fzf_args.push('-1') unless opt[:show_if_single]
|
895
|
-
fzf_args << case opt[:case].normalize_case
|
896
|
-
when :sensitive
|
897
|
-
'+i'
|
898
|
-
when :ignore
|
899
|
-
'-i'
|
900
|
-
end
|
901
|
-
fzf_args << '-e' if opt[:exact]
|
902
|
-
|
903
|
-
|
904
|
-
unless opt[:menu]
|
905
|
-
raise InvalidArgument, "Can't skip menu when no query is provided" unless opt[:query] && !opt[:query].empty?
|
906
|
-
|
907
|
-
fzf_args.concat([%(--filter="#{opt[:query]}"), opt[:sort] ? '' : '--no-sort'])
|
908
|
-
end
|
909
|
-
|
910
|
-
res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
|
911
|
-
selected = []
|
912
|
-
res.split(/\n/).each do |item|
|
913
|
-
idx = item.match(/^ *(\d+)\)/)[1].to_i
|
914
|
-
selected.push(items[idx])
|
915
|
-
end
|
916
|
-
|
917
|
-
opt[:multiple] ? selected : selected[0]
|
918
|
-
end
|
919
|
-
|
920
786
|
##
|
921
787
|
## Perform actions on a set of entries. If
|
922
788
|
## no valid action is included in the opt
|
@@ -966,11 +832,11 @@ module Doing
|
|
966
832
|
|
967
833
|
actions.concat(['resume/repeat', 'begin/reset']) if items.count == 1
|
968
834
|
|
969
|
-
choice = choose_from(actions,
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
835
|
+
choice = Prompt.choose_from(actions,
|
836
|
+
prompt: 'What do you want to do with the selected items? > ',
|
837
|
+
multiple: true,
|
838
|
+
sorted: false,
|
839
|
+
fzf_args: ["--height=#{actions.count + 3}", '--tac', '--no-sort', '--info=hidden'])
|
974
840
|
return unless choice
|
975
841
|
|
976
842
|
to_do = choice.strip.split(/\n/)
|
@@ -984,7 +850,7 @@ module Doing
|
|
984
850
|
type = action =~ /^add/ ? 'add' : 'remove'
|
985
851
|
raise InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
|
986
852
|
|
987
|
-
print "#{
|
853
|
+
print "#{yellow("Tag to #{type}: ")}#{reset}"
|
988
854
|
tag = $stdin.gets
|
989
855
|
next if tag =~ /^ *$/
|
990
856
|
|
@@ -992,17 +858,22 @@ module Doing
|
|
992
858
|
opt[:remove] = true if type == 'remove'
|
993
859
|
when /output formatted/
|
994
860
|
plugins = Plugins.available_plugins(type: :export).sort
|
995
|
-
output_format = choose_from(plugins,
|
996
|
-
|
997
|
-
|
861
|
+
output_format = Prompt.choose_from(plugins,
|
862
|
+
prompt: 'Which output format? > ',
|
863
|
+
fzf_args: [
|
864
|
+
"--height=#{plugins.count + 3}",
|
865
|
+
'--tac',
|
866
|
+
'--no-sort',
|
867
|
+
'--info=hidden'
|
868
|
+
])
|
998
869
|
next if tag =~ /^ *$/
|
999
870
|
|
1000
871
|
raise UserCancelled unless output_format
|
1001
872
|
|
1002
873
|
opt[:output] = output_format.strip
|
1003
|
-
res = opt[:force] ? false : yn('Save to file?', default_response: 'n')
|
874
|
+
res = opt[:force] ? false : Prompt.yn('Save to file?', default_response: 'n')
|
1004
875
|
if res
|
1005
|
-
print "#{
|
876
|
+
print "#{yellow('File path/name: ')}#{reset}"
|
1006
877
|
filename = $stdin.gets.strip
|
1007
878
|
next if filename.empty?
|
1008
879
|
|
@@ -1028,29 +899,28 @@ module Doing
|
|
1028
899
|
end
|
1029
900
|
|
1030
901
|
if opt[:resume] || opt[:reset]
|
1031
|
-
if items.count > 1
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
|
1042
|
-
|
1043
|
-
update_item(item, reset_item(item, resume: res))
|
1044
|
-
end
|
1045
|
-
write(@doing_file)
|
902
|
+
raise InvalidArgument, 'resume and restart can only be used on a single entry' if items.count > 1
|
903
|
+
|
904
|
+
item = items[0]
|
905
|
+
if opt[:resume] && !opt[:reset]
|
906
|
+
repeat_item(item, { editor: opt[:editor] })
|
907
|
+
elsif opt[:reset]
|
908
|
+
res = if item.tags?('done', :and) && !opt[:resume]
|
909
|
+
opt[:force] ? true : Prompt.yn('Remove @done tag?', default_response: 'y')
|
910
|
+
else
|
911
|
+
opt[:resume]
|
912
|
+
end
|
913
|
+
@content.update_item(item, reset_item(item, resume: res))
|
1046
914
|
end
|
915
|
+
write(@doing_file)
|
916
|
+
|
1047
917
|
return
|
1048
918
|
end
|
1049
919
|
|
1050
920
|
if opt[:delete]
|
1051
|
-
res = opt[:force] ? true : yn("Delete #{items.size} items?", default_response: 'y')
|
921
|
+
res = opt[:force] ? true : Prompt.yn("Delete #{items.size} items?", default_response: 'y')
|
1052
922
|
if res
|
1053
|
-
items.each { |
|
923
|
+
items.each { |i| @content.delete_item(i, single: items.count == 1) }
|
1054
924
|
write(@doing_file)
|
1055
925
|
end
|
1056
926
|
return
|
@@ -1058,31 +928,31 @@ module Doing
|
|
1058
928
|
|
1059
929
|
if opt[:flag]
|
1060
930
|
tag = @config['marker_tag'] || 'flagged'
|
1061
|
-
items.map! do |
|
1062
|
-
|
931
|
+
items.map! do |i|
|
932
|
+
i.tag(tag, date: false, remove: opt[:remove], single: single)
|
1063
933
|
end
|
1064
934
|
end
|
1065
935
|
|
1066
936
|
if opt[:finish] || opt[:cancel]
|
1067
937
|
tag = 'done'
|
1068
|
-
items.map! do |
|
1069
|
-
if
|
1070
|
-
should_date = !opt[:cancel] &&
|
1071
|
-
|
938
|
+
items.map! do |i|
|
939
|
+
if i.should_finish?
|
940
|
+
should_date = !opt[:cancel] && i.should_time?
|
941
|
+
i.tag(tag, date: should_date, remove: opt[:remove], single: single)
|
1072
942
|
end
|
1073
943
|
end
|
1074
944
|
end
|
1075
945
|
|
1076
946
|
if opt[:tag]
|
1077
947
|
tag = opt[:tag]
|
1078
|
-
items.map! do |
|
1079
|
-
|
948
|
+
items.map! do |i|
|
949
|
+
i.tag(tag, date: false, remove: opt[:remove], single: single)
|
1080
950
|
end
|
1081
951
|
end
|
1082
952
|
|
1083
953
|
if opt[:archive] || opt[:move]
|
1084
954
|
section = opt[:archive] ? 'Archive' : guess_section(opt[:move])
|
1085
|
-
items.map! {|
|
955
|
+
items.map! { |i| i.move_to(section, label: true) }
|
1086
956
|
end
|
1087
957
|
|
1088
958
|
write(@doing_file)
|
@@ -1091,111 +961,88 @@ module Doing
|
|
1091
961
|
|
1092
962
|
editable_items = []
|
1093
963
|
|
1094
|
-
items.each do |
|
1095
|
-
editable = "#{
|
1096
|
-
old_note =
|
964
|
+
items.each do |i|
|
965
|
+
editable = "#{i.date.strftime('%F %R')} | #{i.title}"
|
966
|
+
old_note = i.note ? i.note.strip_lines.join("\n") : nil
|
1097
967
|
editable += "\n#{old_note}" unless old_note.nil?
|
1098
968
|
editable_items << editable
|
1099
969
|
end
|
1100
970
|
divider = "\n-----------\n"
|
1101
|
-
|
971
|
+
notice =<<~EONOTICE
|
972
|
+
# - You may delete entries, but leave all divider lines (---) in place.
|
973
|
+
# - Start and @done dates replaced with a time string (yesterday 3pm) will
|
974
|
+
# be parsed automatically. Do not delete the pipe (|) between start date
|
975
|
+
# and entry title.
|
976
|
+
EONOTICE
|
977
|
+
input = "#{editable_items.map(&:strip).join(divider)}\n\n#{notice}"
|
1102
978
|
|
1103
979
|
new_items = fork_editor(input).split(/#{divider}/)
|
1104
980
|
|
1105
981
|
new_items.each_with_index do |new_item, i|
|
1106
|
-
|
1107
982
|
input_lines = new_item.split(/[\n\r]+/).delete_if(&:ignore?)
|
1108
|
-
|
983
|
+
first_line = input_lines[0]&.strip
|
1109
984
|
|
1110
|
-
if
|
1111
|
-
delete_item(items[i], single: new_items.count == 1)
|
985
|
+
if first_line.nil? || first_line =~ /^#{divider.strip}$/ || first_line.strip.empty?
|
986
|
+
@content.delete_item(items[i], single: new_items.count == 1)
|
987
|
+
Doing.logger.count(:deleted)
|
1112
988
|
else
|
1113
|
-
note =
|
989
|
+
date, title, note = format_input(new_item)
|
1114
990
|
|
1115
991
|
note.map!(&:strip)
|
1116
992
|
note.delete_if(&:ignore?)
|
1117
|
-
|
1118
|
-
date = title.match(/^([\d\-: ]+) \| /)[1]
|
1119
|
-
title.sub!(/^([\d\-: ]+) \| /, '')
|
1120
|
-
|
1121
993
|
item = items[i]
|
994
|
+
old_item = item.dup
|
995
|
+
item.date = date || items[i].date
|
1122
996
|
item.title = title
|
1123
997
|
item.note = note
|
1124
|
-
item.
|
998
|
+
if (item.equal?(old_item))
|
999
|
+
Doing.logger.count(:skipped, level: :debug)
|
1000
|
+
else
|
1001
|
+
Doing.logger.count(:updated)
|
1002
|
+
end
|
1125
1003
|
end
|
1126
1004
|
end
|
1127
1005
|
|
1128
1006
|
write(@doing_file)
|
1129
1007
|
end
|
1130
1008
|
|
1131
|
-
|
1132
|
-
items.map! do |item|
|
1133
|
-
item.title = "#{item.title} @project(#{item.section})"
|
1134
|
-
item
|
1135
|
-
end
|
1136
|
-
|
1137
|
-
@content = { 'Export' => { :original => 'Export:', :items => items } }
|
1138
|
-
options = { section: 'Export' }
|
1139
|
-
|
1140
|
-
|
1141
|
-
if opt[:output] =~ /doing/
|
1142
|
-
options[:output] = 'template'
|
1143
|
-
options[:template] = '- %date | %title%note'
|
1144
|
-
else
|
1145
|
-
options[:output] = opt[:output]
|
1146
|
-
options[:template] = opt[:template] || nil
|
1147
|
-
end
|
1148
|
-
|
1149
|
-
output = list_section(options)
|
1009
|
+
return unless opt[:output]
|
1150
1010
|
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1154
|
-
# Create a backup copy for the undo command
|
1155
|
-
FileUtils.cp(file, "#{file}~")
|
1156
|
-
end
|
1157
|
-
|
1158
|
-
File.open(file, 'w+') do |f|
|
1159
|
-
f.puts output
|
1160
|
-
end
|
1161
|
-
|
1162
|
-
logger.warn('File written:', file)
|
1163
|
-
else
|
1164
|
-
Doing::Pager.page output
|
1165
|
-
end
|
1011
|
+
items.map! do |i|
|
1012
|
+
i.title = "#{i.title} @project(#{i.section})"
|
1013
|
+
i
|
1166
1014
|
end
|
1167
|
-
end
|
1168
1015
|
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
## @param tags [String] The tag to apply
|
1174
|
-
## @param remove [Boolean] remove tags?
|
1175
|
-
## @param date [Boolean] Include timestamp?
|
1176
|
-
## @param single [Boolean] Log as a single change?
|
1177
|
-
##
|
1178
|
-
## @return [Item] updated item
|
1179
|
-
##
|
1180
|
-
def tag_item(item, tags, remove: false, date: false, single: false)
|
1181
|
-
added = []
|
1182
|
-
removed = []
|
1016
|
+
@content = Items.new
|
1017
|
+
@content.concat(items)
|
1018
|
+
@content.add_section(Section.new('Export'), log: false)
|
1019
|
+
options = { section: 'Export' }
|
1183
1020
|
|
1184
|
-
|
1021
|
+
if opt[:output] =~ /doing/
|
1022
|
+
options[:output] = 'template'
|
1023
|
+
options[:template] = '- %date | %title%note'
|
1024
|
+
else
|
1025
|
+
options[:output] = opt[:output]
|
1026
|
+
options[:template] = opt[:template] || nil
|
1027
|
+
end
|
1185
1028
|
|
1186
|
-
|
1029
|
+
output = list_section(options)
|
1187
1030
|
|
1188
|
-
|
1189
|
-
|
1190
|
-
if
|
1191
|
-
|
1192
|
-
|
1031
|
+
if opt[:save_to]
|
1032
|
+
file = File.expand_path(opt[:save_to])
|
1033
|
+
if File.exist?(file)
|
1034
|
+
# Create a backup copy for the undo command
|
1035
|
+
FileUtils.cp(file, "#{file}~")
|
1193
1036
|
end
|
1194
|
-
end
|
1195
1037
|
|
1196
|
-
|
1038
|
+
File.open(file, 'w+') do |f|
|
1039
|
+
f.puts output
|
1040
|
+
end
|
1197
1041
|
|
1198
|
-
|
1042
|
+
logger.warn('File written:', file)
|
1043
|
+
else
|
1044
|
+
Doing::Pager.page output
|
1045
|
+
end
|
1199
1046
|
end
|
1200
1047
|
|
1201
1048
|
##
|
@@ -1219,17 +1066,15 @@ module Doing
|
|
1219
1066
|
opt[:unfinished] ||= false
|
1220
1067
|
opt[:section] = opt[:section] ? guess_section(opt[:section]) : 'All'
|
1221
1068
|
|
1222
|
-
items = filter_items(
|
1069
|
+
items = filter_items(Items.new, opt: opt)
|
1223
1070
|
|
1224
1071
|
if opt[:interactive]
|
1225
|
-
items = choose_from_items(items,
|
1226
|
-
menu: true,
|
1072
|
+
items = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i, menu: true,
|
1227
1073
|
header: '',
|
1228
1074
|
prompt: 'Select entries to tag > ',
|
1229
1075
|
multiple: true,
|
1230
1076
|
sort: true,
|
1231
|
-
show_if_single: true
|
1232
|
-
}, include_section: opt[:section] =~ /^all$/i)
|
1077
|
+
show_if_single: true)
|
1233
1078
|
|
1234
1079
|
raise NoResults, 'no items selected' if items.empty?
|
1235
1080
|
|
@@ -1310,12 +1155,12 @@ module Doing
|
|
1310
1155
|
end
|
1311
1156
|
end
|
1312
1157
|
|
1313
|
-
log_change(tags_added: added, tags_removed: removed, item: item, single: items.count == 1)
|
1158
|
+
logger.log_change(tags_added: added, tags_removed: removed, item: item, single: items.count == 1)
|
1314
1159
|
|
1315
1160
|
item.note.add(opt[:note]) if opt[:note]
|
1316
1161
|
|
1317
1162
|
if opt[:archive] && opt[:section] != 'Archive' && (opt[:count]).positive?
|
1318
|
-
|
1163
|
+
item.move_to('Archive', label: true)
|
1319
1164
|
elsif opt[:archive] && opt[:count].zero?
|
1320
1165
|
logger.warn('Skipped:', 'Archiving is skipped when operating on all entries')
|
1321
1166
|
end
|
@@ -1324,29 +1169,6 @@ module Doing
|
|
1324
1169
|
write(@doing_file)
|
1325
1170
|
end
|
1326
1171
|
|
1327
|
-
##
|
1328
|
-
## Move item from current section to
|
1329
|
-
## destination section
|
1330
|
-
##
|
1331
|
-
## @param item [Item] The item to move
|
1332
|
-
## @param section [String] The destination section
|
1333
|
-
##
|
1334
|
-
## @return [Item] Updated item
|
1335
|
-
##
|
1336
|
-
def move_item(item, section, label: true)
|
1337
|
-
from = item.section
|
1338
|
-
new_item = @content[item.section][:items].delete(item)
|
1339
|
-
new_item.title.sub!(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{from})") if label
|
1340
|
-
new_item.section = section
|
1341
|
-
|
1342
|
-
@content[section][:items].concat([new_item])
|
1343
|
-
|
1344
|
-
logger.count(section == 'Archive' ? :archived : :moved)
|
1345
|
-
logger.debug("#{section == 'Archive' ? 'Archived' : 'Moved'}:",
|
1346
|
-
"#{new_item.title.truncate(60)} from #{from} to #{section}")
|
1347
|
-
new_item
|
1348
|
-
end
|
1349
|
-
|
1350
1172
|
##
|
1351
1173
|
## Get next item in the index
|
1352
1174
|
##
|
@@ -1357,49 +1179,13 @@ module Doing
|
|
1357
1179
|
## @return [Item] the next chronological item in the index
|
1358
1180
|
##
|
1359
1181
|
def next_item(item, options = {})
|
1360
|
-
items = filter_items(
|
1182
|
+
items = filter_items(Items.new, opt: options)
|
1361
1183
|
|
1362
1184
|
idx = items.index(item)
|
1363
1185
|
|
1364
1186
|
idx.positive? ? items[idx - 1] : nil
|
1365
1187
|
end
|
1366
1188
|
|
1367
|
-
##
|
1368
|
-
## Delete an item from the index
|
1369
|
-
##
|
1370
|
-
## @param item The item
|
1371
|
-
##
|
1372
|
-
def delete_item(item, single: false)
|
1373
|
-
section = item.section
|
1374
|
-
|
1375
|
-
section_items = @content[section][:items]
|
1376
|
-
deleted = section_items.delete(item)
|
1377
|
-
logger.count(:deleted)
|
1378
|
-
logger.info('Entry deleted:', deleted.title) if single
|
1379
|
-
end
|
1380
|
-
|
1381
|
-
##
|
1382
|
-
## Update an item in the index with a modified item
|
1383
|
-
##
|
1384
|
-
## @param old_item The old item
|
1385
|
-
## @param new_item The new item
|
1386
|
-
##
|
1387
|
-
def update_item(old_item, new_item)
|
1388
|
-
section = old_item.section
|
1389
|
-
|
1390
|
-
section_items = @content[section][:items]
|
1391
|
-
s_idx = section_items.index { |item| item.equal?(old_item) }
|
1392
|
-
|
1393
|
-
raise ItemNotFound, 'Unable to find item in index, did it mutate?' unless s_idx
|
1394
|
-
|
1395
|
-
return if section_items[s_idx].equal?(new_item)
|
1396
|
-
|
1397
|
-
section_items[s_idx] = new_item
|
1398
|
-
logger.count(:updated)
|
1399
|
-
logger.info('Entry updated:', section_items[s_idx].title.truncate(60))
|
1400
|
-
new_item
|
1401
|
-
end
|
1402
|
-
|
1403
1189
|
##
|
1404
1190
|
## Edit the last entry
|
1405
1191
|
##
|
@@ -1415,16 +1201,18 @@ module Doing
|
|
1415
1201
|
return
|
1416
1202
|
end
|
1417
1203
|
|
1418
|
-
content = [item.title.dup]
|
1419
|
-
content << item.note.
|
1204
|
+
content = ["#{item.date.strftime('%F %R')} | #{item.title.dup}"]
|
1205
|
+
content << item.note.strip_lines.join("\n") unless item.note.empty?
|
1420
1206
|
new_item = fork_editor(content.join("\n"))
|
1421
|
-
title, note = format_input(new_item)
|
1207
|
+
date, title, note = format_input(new_item)
|
1208
|
+
date ||= item.date
|
1422
1209
|
|
1423
1210
|
if title.nil? || title.empty?
|
1424
1211
|
logger.debug('Skipped:', 'No content provided')
|
1425
|
-
elsif title == item.title && note.equal?(item.note)
|
1212
|
+
elsif title == item.title && note.equal?(item.note) && date.equal?(item.date)
|
1426
1213
|
logger.debug('Skipped:', 'No change in content')
|
1427
1214
|
else
|
1215
|
+
item.date = date unless date.nil?
|
1428
1216
|
item.title = title
|
1429
1217
|
item.note.add(note, replace: true)
|
1430
1218
|
logger.info('Edited:', item.title)
|
@@ -1443,6 +1231,11 @@ module Doing
|
|
1443
1231
|
## @param target_tag [String] Tag to replace
|
1444
1232
|
## @param opt [Hash] Additional Options
|
1445
1233
|
##
|
1234
|
+
## @option opt :section [String] target section
|
1235
|
+
## @option opt :archive [Boolean] archive old item
|
1236
|
+
## @option opt :back [Date] backdate new item
|
1237
|
+
## @option opt :new_item [String] content to use for new item
|
1238
|
+
## @option opt :note [Array] note content for new item
|
1446
1239
|
def stop_start(target_tag, opt = {})
|
1447
1240
|
tag = target_tag.dup
|
1448
1241
|
opt[:section] ||= @config['current_section']
|
@@ -1457,7 +1250,9 @@ module Doing
|
|
1457
1250
|
|
1458
1251
|
found_items = 0
|
1459
1252
|
|
1460
|
-
@content
|
1253
|
+
@content.each_with_index do |item, i|
|
1254
|
+
next unless item.section == opt[:section] || opt[:section] =~ /all/i
|
1255
|
+
|
1461
1256
|
next unless item.title =~ /@#{tag}/
|
1462
1257
|
|
1463
1258
|
item.title.add_tags!([tag, 'done'], remove: true)
|
@@ -1467,7 +1262,7 @@ module Doing
|
|
1467
1262
|
|
1468
1263
|
if opt[:archive] && opt[:section] != 'Archive'
|
1469
1264
|
item.title = item.title.sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{item.section})")
|
1470
|
-
|
1265
|
+
item.move_to('Archive', label: false, log: false)
|
1471
1266
|
logger.count(:completed_archived)
|
1472
1267
|
logger.info('Completed/archived:', item.title)
|
1473
1268
|
else
|
@@ -1479,7 +1274,8 @@ module Doing
|
|
1479
1274
|
logger.debug('Skipped:', "No active @#{tag} tasks found.") if found_items.zero?
|
1480
1275
|
|
1481
1276
|
if opt[:new_item]
|
1482
|
-
title, note = format_input(opt[:new_item])
|
1277
|
+
date, title, note = format_input(opt[:new_item])
|
1278
|
+
opt[:back] = date unless date.nil?
|
1483
1279
|
note.add(opt[:note]) if opt[:note]
|
1484
1280
|
title.tag!(tag)
|
1485
1281
|
add_item(title.cap_first, opt[:section], { note: note, back: opt[:back] })
|
@@ -1496,7 +1292,6 @@ module Doing
|
|
1496
1292
|
def write(file = nil, backup: true)
|
1497
1293
|
Hooks.trigger :pre_write, self, file
|
1498
1294
|
output = combined_content
|
1499
|
-
|
1500
1295
|
if file.nil?
|
1501
1296
|
$stdout.puts output
|
1502
1297
|
else
|
@@ -1513,7 +1308,7 @@ module Doing
|
|
1513
1308
|
def restore_backup(file)
|
1514
1309
|
if File.exist?("#{file}~")
|
1515
1310
|
FileUtils.cp("#{file}~", file)
|
1516
|
-
logger.warn('File update:', "Restored #{file.sub(/^#{
|
1311
|
+
logger.warn('File update:', "Restored #{file.sub(/^#{Util.user_home}/, '~')}")
|
1517
1312
|
else
|
1518
1313
|
logger.error('Restore error:', 'No backup file found')
|
1519
1314
|
end
|
@@ -1529,70 +1324,46 @@ module Doing
|
|
1529
1324
|
bool = opt[:bool] || :and
|
1530
1325
|
sect = opt[:section] !~ /^all$/i ? guess_section(opt[:section]) : 'all'
|
1531
1326
|
|
1532
|
-
|
1533
|
-
all_sections = sections.dup
|
1534
|
-
else
|
1535
|
-
all_sections = [sect]
|
1536
|
-
end
|
1537
|
-
|
1538
|
-
counter = 0
|
1539
|
-
new_content = {}
|
1540
|
-
|
1541
|
-
|
1542
|
-
all_sections.each do |section|
|
1543
|
-
items = @content[section][:items].dup
|
1544
|
-
new_content[section] = {}
|
1545
|
-
new_content[section][:original] = @content[section][:original]
|
1546
|
-
new_content[section][:items] = []
|
1547
|
-
|
1548
|
-
moved_items = []
|
1549
|
-
if !tags.empty? || opt[:search] || opt[:before]
|
1550
|
-
if opt[:before]
|
1551
|
-
time_string = opt[:before]
|
1552
|
-
cutoff = chronify(time_string, guess: :begin)
|
1553
|
-
end
|
1327
|
+
section = guess_section(sect)
|
1554
1328
|
|
1555
|
-
|
1556
|
-
|
1557
|
-
moved_items.push(item)
|
1558
|
-
counter += 1
|
1559
|
-
true
|
1560
|
-
else
|
1561
|
-
false
|
1562
|
-
end
|
1563
|
-
end
|
1564
|
-
@content[section][:items] = items
|
1565
|
-
new_content[section][:items] = moved_items
|
1566
|
-
logger.warn('Rotated:', "#{moved_items.length} items from #{section}")
|
1567
|
-
else
|
1568
|
-
new_content[section][:items] = []
|
1569
|
-
moved_items = []
|
1329
|
+
section_items = @content.in_section(section)
|
1330
|
+
max = section_items.count - keep.to_i
|
1570
1331
|
|
1571
|
-
|
1332
|
+
counter = 0
|
1333
|
+
new_content = Items.new
|
1572
1334
|
|
1573
|
-
|
1574
|
-
|
1575
|
-
|
1576
|
-
|
1577
|
-
|
1335
|
+
@content.each do |item|
|
1336
|
+
break if counter >= max
|
1337
|
+
if opt[:before]
|
1338
|
+
time_string = opt[:before]
|
1339
|
+
cutoff = chronify(time_string, guess: :begin)
|
1340
|
+
end
|
1578
1341
|
|
1579
|
-
|
1580
|
-
|
1581
|
-
|
1582
|
-
items[0..count - 1]
|
1583
|
-
end
|
1584
|
-
new_content[section][:items] = moved_items
|
1342
|
+
unless ((!tags.empty? && !item.tags?(tags, bool)) || (opt[:search] && !item.search(opt[:search].to_s)) || (opt[:before] && item.date >= cutoff))
|
1343
|
+
new_item = @content.delete(item)
|
1344
|
+
raise DoingRuntimeError, "Error deleting item: #{item}" if new_item.nil?
|
1585
1345
|
|
1586
|
-
|
1346
|
+
new_content.add_section(new_item.section, log: false)
|
1347
|
+
new_content.push(new_item)
|
1348
|
+
counter += 1
|
1587
1349
|
end
|
1588
1350
|
end
|
1589
1351
|
|
1352
|
+
if counter.positive?
|
1353
|
+
logger.count(:rotated,
|
1354
|
+
level: :info,
|
1355
|
+
count: counter,
|
1356
|
+
message: "Rotated %count %items")
|
1357
|
+
else
|
1358
|
+
logger.info('Skipped:', 'No items were rotated')
|
1359
|
+
end
|
1360
|
+
|
1590
1361
|
write(@doing_file)
|
1591
1362
|
|
1592
1363
|
file = @doing_file.sub(/(\.\w+)$/, "_#{Time.now.strftime('%Y-%m-%d')}\\1")
|
1593
1364
|
if File.exist?(file)
|
1594
1365
|
init_doing_file(file)
|
1595
|
-
@content.
|
1366
|
+
@content.concat(new_content).uniq!
|
1596
1367
|
logger.warn('File update:', "added entries to existing file: #{file}")
|
1597
1368
|
else
|
1598
1369
|
@content = new_content
|
@@ -1608,7 +1379,7 @@ module Doing
|
|
1608
1379
|
## @return [String] The selected section name
|
1609
1380
|
##
|
1610
1381
|
def choose_section
|
1611
|
-
choice = choose_from(
|
1382
|
+
choice = Prompt.choose_from(@content.section_titles.sort, prompt: 'Choose a section > ', fzf_args: ['--height=60%'])
|
1612
1383
|
choice ? choice.strip : choice
|
1613
1384
|
end
|
1614
1385
|
|
@@ -1627,7 +1398,7 @@ module Doing
|
|
1627
1398
|
## @return [String] The selected view name
|
1628
1399
|
##
|
1629
1400
|
def choose_view
|
1630
|
-
choice = choose_from(views.sort, prompt: 'Choose a view > ', fzf_args: ['--height=60%'])
|
1401
|
+
choice = Prompt.choose_from(views.sort, prompt: 'Choose a view > ', fzf_args: ['--height=60%'])
|
1631
1402
|
choice ? choice.strip : choice
|
1632
1403
|
end
|
1633
1404
|
|
@@ -1685,7 +1456,7 @@ module Doing
|
|
1685
1456
|
end
|
1686
1457
|
end
|
1687
1458
|
|
1688
|
-
items = filter_items(
|
1459
|
+
items = filter_items(Items.new, opt: opt).reverse
|
1689
1460
|
|
1690
1461
|
items.reverse! if opt[:order] =~ /^d/i
|
1691
1462
|
|
@@ -1693,7 +1464,7 @@ module Doing
|
|
1693
1464
|
opt[:menu] = !opt[:force]
|
1694
1465
|
opt[:query] = '' # opt[:search]
|
1695
1466
|
opt[:multiple] = true
|
1696
|
-
selected = choose_from_items(items,
|
1467
|
+
selected = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i, **opt)
|
1697
1468
|
|
1698
1469
|
raise NoResults, 'no items selected' if selected.empty?
|
1699
1470
|
|
@@ -1701,11 +1472,8 @@ module Doing
|
|
1701
1472
|
return
|
1702
1473
|
end
|
1703
1474
|
|
1704
|
-
|
1705
1475
|
opt[:output] ||= 'template'
|
1706
|
-
|
1707
1476
|
opt[:wrap_width] ||= @config['templates']['default']['wrap_width'] || 0
|
1708
|
-
|
1709
1477
|
output(items, title, is_single, opt)
|
1710
1478
|
end
|
1711
1479
|
|
@@ -1726,11 +1494,12 @@ module Doing
|
|
1726
1494
|
archive_all = section =~ /^all$/i # && !(tags.nil? || tags.empty?)
|
1727
1495
|
section = guess_section(section) unless archive_all
|
1728
1496
|
|
1729
|
-
add_section(
|
1497
|
+
@content.add_section(destination, log: true)
|
1498
|
+
# add_section(Section.new('Archive')) if destination =~ /^archive$/i && !@content.section?('Archive')
|
1730
1499
|
|
1731
1500
|
destination = guess_section(destination)
|
1732
1501
|
|
1733
|
-
if
|
1502
|
+
if @content.section?(destination) && (@content.section?(section) || archive_all)
|
1734
1503
|
do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label], before: options[:before] })
|
1735
1504
|
write(doing_file)
|
1736
1505
|
else
|
@@ -1970,7 +1739,6 @@ module Doing
|
|
1970
1739
|
end
|
1971
1740
|
end
|
1972
1741
|
|
1973
|
-
|
1974
1742
|
logger.debug('Autotag:', "whitelisted tags: #{tagged[:whitelisted].log_tags}") unless tagged[:whitelisted].empty?
|
1975
1743
|
logger.debug('Autotag:', "synonyms: #{tagged[:synonyms].log_tags}") unless tagged[:synonyms].empty?
|
1976
1744
|
logger.debug('Autotag:', "transforms: #{tagged[:transformed].log_tags}") unless tagged[:transformed].empty?
|
@@ -1983,10 +1751,10 @@ module Doing
|
|
1983
1751
|
text.add_tags!(tail_tags) unless tail_tags.empty?
|
1984
1752
|
|
1985
1753
|
if text == original
|
1986
|
-
logger.debug('Autotag:', "no change to \"#{text}\"")
|
1754
|
+
logger.debug('Autotag:', "no change to \"#{text.strip}\"")
|
1987
1755
|
else
|
1988
1756
|
new_tags = tagged[:whitelisted].concat(tail_tags).concat(tagged[:replaced])
|
1989
|
-
logger.debug('Autotag:', "added #{new_tags.log_tags} to \"#{text}\"")
|
1757
|
+
logger.debug('Autotag:', "added #{new_tags.log_tags} to \"#{text.strip}\"")
|
1990
1758
|
logger.count(:autotag, level: :info, count: 1, message: 'autotag updated %count %items')
|
1991
1759
|
end
|
1992
1760
|
|
@@ -2163,7 +1931,7 @@ EOS
|
|
2163
1931
|
def format_time(seconds, human: false)
|
2164
1932
|
return [0, 0, 0] if seconds.nil?
|
2165
1933
|
|
2166
|
-
if seconds.
|
1934
|
+
if seconds.instance_of?(String) && seconds =~ /(\d+):(\d+):(\d+)/
|
2167
1935
|
h = Regexp.last_match(1)
|
2168
1936
|
m = Regexp.last_match(2)
|
2169
1937
|
s = Regexp.last_match(3)
|
@@ -2192,13 +1960,13 @@ EOS
|
|
2192
1960
|
##
|
2193
1961
|
def combined_content
|
2194
1962
|
output = @other_content_top ? "#{@other_content_top.join("\n")}\n" : ''
|
2195
|
-
|
2196
|
-
|
2197
|
-
|
2198
|
-
output += list_section({ section: title, template: "\t- %date | %title%t2note", highlight: false, wrap_width: 0, tags_color: false })
|
2199
|
-
end
|
2200
|
-
|
1963
|
+
was_color = Color.coloring?
|
1964
|
+
Color.coloring = false
|
1965
|
+
output += @content.to_s
|
2201
1966
|
output += @other_content_bottom.join("\n") unless @other_content_bottom.nil?
|
1967
|
+
# Just strip all ANSI colors from the content before writing to doing file
|
1968
|
+
Color.coloring = was_color
|
1969
|
+
|
2202
1970
|
output.uncolor
|
2203
1971
|
end
|
2204
1972
|
|
@@ -2253,99 +2021,50 @@ EOS
|
|
2253
2021
|
##
|
2254
2022
|
## Helper function, performs the actual archiving
|
2255
2023
|
##
|
2256
|
-
## @param
|
2024
|
+
## @param section [String] The source section
|
2257
2025
|
## @param destination [String] The destination
|
2258
2026
|
## section
|
2259
2027
|
## @param opt [Hash] Additional Options
|
2260
2028
|
##
|
2261
|
-
def do_archive(
|
2029
|
+
def do_archive(section, destination, opt = {})
|
2262
2030
|
count = opt[:count] || 0
|
2263
2031
|
tags = opt[:tags] || []
|
2264
2032
|
bool = opt[:bool] || :and
|
2265
2033
|
label = opt[:label] || true
|
2266
2034
|
|
2267
|
-
|
2268
|
-
|
2269
|
-
all_sections.delete(destination)
|
2270
|
-
else
|
2271
|
-
all_sections = [sect]
|
2272
|
-
end
|
2273
|
-
|
2274
|
-
counter = 0
|
2035
|
+
section = guess_section(section)
|
2036
|
+
destination = guess_section(destination)
|
2275
2037
|
|
2276
|
-
|
2277
|
-
|
2038
|
+
section_items = @content.in_section(section)
|
2039
|
+
max = section_items.count - count.to_i
|
2278
2040
|
|
2279
|
-
|
2280
|
-
if !tags.empty? || opt[:search] || opt[:before]
|
2281
|
-
if opt[:before]
|
2282
|
-
time_string = opt[:before]
|
2283
|
-
cutoff = chronify(time_string, guess: :begin)
|
2284
|
-
end
|
2041
|
+
counter = 0
|
2285
2042
|
|
2286
|
-
|
2287
|
-
|
2288
|
-
|
2289
|
-
|
2290
|
-
|
2291
|
-
|
2292
|
-
false
|
2293
|
-
end
|
2294
|
-
end
|
2295
|
-
moved_items.each do |item|
|
2296
|
-
if label
|
2297
|
-
item.title = if section == @config['current_section']
|
2298
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
|
2299
|
-
else
|
2300
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
2301
|
-
end
|
2302
|
-
logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
|
2303
|
-
end
|
2304
|
-
end
|
2043
|
+
@content.map! do |item|
|
2044
|
+
break if counter >= max
|
2045
|
+
if opt[:before]
|
2046
|
+
time_string = opt[:before]
|
2047
|
+
cutoff = chronify(time_string, guess: :begin)
|
2048
|
+
end
|
2305
2049
|
|
2306
|
-
|
2307
|
-
|
2308
|
-
|
2309
|
-
|
2310
|
-
level: :info,
|
2311
|
-
count: moved_items.length,
|
2312
|
-
message: "%count %items from #{section} to #{destination}")
|
2313
|
-
else
|
2314
|
-
logger.info('Skipped:', 'No items were moved')
|
2315
|
-
end
|
2050
|
+
if (item.section.downcase != section.downcase && section != /^all$/i) || item.section.downcase == destination.downcase
|
2051
|
+
item
|
2052
|
+
elsif ((!tags.empty? && !item.tags?(tags, bool)) || (opt[:search] && !item.search(opt[:search].to_s)) || (opt[:before] && item.date >= cutoff))
|
2053
|
+
item
|
2316
2054
|
else
|
2317
|
-
|
2318
|
-
|
2319
|
-
items.map! do |item|
|
2320
|
-
if label
|
2321
|
-
item.title = if section == @config['current_section']
|
2322
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
|
2323
|
-
else
|
2324
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
2325
|
-
end
|
2326
|
-
logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
|
2327
|
-
end
|
2328
|
-
item
|
2329
|
-
end
|
2330
|
-
|
2331
|
-
if items.count > count
|
2332
|
-
@content[destination][:items].concat(items[count..-1])
|
2333
|
-
else
|
2334
|
-
@content[destination][:items].concat(items)
|
2335
|
-
end
|
2336
|
-
|
2337
|
-
@content[section][:items] = if count.zero?
|
2338
|
-
[]
|
2339
|
-
else
|
2340
|
-
items[0..count - 1]
|
2341
|
-
end
|
2342
|
-
|
2343
|
-
logger.count(destination == 'Archive' ? :archived : :moved,
|
2344
|
-
level: :info,
|
2345
|
-
count: items.length - count,
|
2346
|
-
message: "%count %items from #{section} to #{destination}")
|
2055
|
+
counter += 1
|
2056
|
+
item.move_to(destination, label: label, log: false)
|
2347
2057
|
end
|
2348
2058
|
end
|
2059
|
+
|
2060
|
+
if counter.positive?
|
2061
|
+
logger.count(destination == 'Archive' ? :archived : :moved,
|
2062
|
+
level: :info,
|
2063
|
+
count: counter,
|
2064
|
+
message: "%count %items from #{section} to #{destination}")
|
2065
|
+
else
|
2066
|
+
logger.info('Skipped:', 'No items were moved')
|
2067
|
+
end
|
2349
2068
|
end
|
2350
2069
|
|
2351
2070
|
def run_after
|
@@ -2357,31 +2076,5 @@ EOS
|
|
2357
2076
|
logger.log_now(:error, 'Script error:', "Error running #{@config['run_after']}")
|
2358
2077
|
logger.log_now(:error, 'STDERR output:', stderr)
|
2359
2078
|
end
|
2360
|
-
|
2361
|
-
def log_change(tags_added: [], tags_removed: [], count: 1, item: nil, single: false)
|
2362
|
-
if tags_added.empty? && tags_removed.empty?
|
2363
|
-
logger.count(:skipped, level: :debug, message: '%count %items with no change', count: count)
|
2364
|
-
else
|
2365
|
-
if tags_added.empty?
|
2366
|
-
logger.count(:skipped, level: :debug, message: 'no tags added to %count %items')
|
2367
|
-
else
|
2368
|
-
if single && item
|
2369
|
-
logger.info('Tagged:', %(added #{tags_added.count == 1 ? 'tag' : 'tags'} #{tags_added.map {|t| "@#{t}"}.join(', ')} to #{item.title}))
|
2370
|
-
else
|
2371
|
-
logger.count(:added_tags, level: :info, tag: tags_added, message: '%tags added to %count %items')
|
2372
|
-
end
|
2373
|
-
end
|
2374
|
-
|
2375
|
-
if tags_removed.empty?
|
2376
|
-
logger.count(:skipped, level: :debug, message: 'no tags removed from %count %items')
|
2377
|
-
else
|
2378
|
-
if single && item
|
2379
|
-
logger.info('Untagged:', %(removed #{tags_removed.count == 1 ? 'tag' : 'tags'} #{tags_added.map {|t| "@#{t}"}.join(', ')} from #{item.title}))
|
2380
|
-
else
|
2381
|
-
logger.count(:removed_tags, level: :info, tag: tags_removed, message: '%tags removed from %count %items')
|
2382
|
-
end
|
2383
|
-
end
|
2384
|
-
end
|
2385
|
-
end
|
2386
2079
|
end
|
2387
2080
|
end
|