doing 2.0.25 → 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 +28 -0
- data/Gemfile.lock +8 -1
- data/README.md +1 -1
- data/Rakefile +23 -4
- data/bin/doing +205 -127
- 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 +40 -12
- data/example_plugin.rb +3 -3
- data/lib/completion/_doing.zsh +1 -1
- data/lib/completion/doing.bash +8 -8
- data/lib/completion/doing.fish +1 -1
- data/lib/doing/array.rb +36 -0
- data/lib/doing/colors.rb +70 -66
- data/lib/doing/completion.rb +6 -0
- data/lib/doing/configuration.rb +69 -28
- 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 +55 -3
- 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 +103 -27
- data/lib/doing/util.rb +14 -6
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +306 -621
- data/lib/doing.rb +6 -2
- data/lib/examples/plugins/capture_thing_import.rb +162 -0
- 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,7 +25,7 @@ module Doing
|
|
24
25
|
def initialize
|
25
26
|
@timers = {}
|
26
27
|
@recorded_items = []
|
27
|
-
@content =
|
28
|
+
@content = Items.new
|
28
29
|
@auto_tag = true
|
29
30
|
end
|
30
31
|
|
@@ -53,52 +54,56 @@ module Doing
|
|
53
54
|
create(@doing_file) unless File.exist?(@doing_file)
|
54
55
|
input = IO.read(@doing_file)
|
55
56
|
input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
|
57
|
+
logger.debug('Read:', "read file #{@doing_file}")
|
56
58
|
elsif File.exist?(File.expand_path(path)) && File.file?(File.expand_path(path)) && File.stat(File.expand_path(path)).size.positive?
|
57
59
|
@doing_file = File.expand_path(path)
|
58
60
|
input = IO.read(File.expand_path(path))
|
59
61
|
input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
|
62
|
+
logger.debug('Read:', "read file #{File.expand_path(path)}")
|
60
63
|
elsif path.length < 256
|
61
64
|
@doing_file = File.expand_path(path)
|
62
65
|
create(path)
|
63
66
|
input = IO.read(File.expand_path(path))
|
64
67
|
input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
|
68
|
+
logger.debug('Read:', "read file #{File.expand_path(path)}")
|
65
69
|
end
|
66
70
|
|
67
71
|
@other_content_top = []
|
68
72
|
@other_content_bottom = []
|
69
73
|
|
70
|
-
section =
|
74
|
+
section = nil
|
71
75
|
lines = input.split(/[\n\r]/)
|
72
|
-
current = 0
|
73
76
|
|
74
77
|
lines.each do |line|
|
75
78
|
next if line =~ /^\s*$/
|
76
79
|
|
77
80
|
if line =~ /^(\S[\S ]+):\s*(@\S+\s*)*$/
|
78
81
|
section = Regexp.last_match(1)
|
79
|
-
@content
|
80
|
-
@content[section][:original] = line
|
81
|
-
@content[section][:items] = []
|
82
|
-
current = 0
|
82
|
+
@content.add_section(Section.new(section, original: line), log: false)
|
83
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
|
+
|
84
89
|
date = Regexp.last_match(1).strip
|
85
90
|
title = Regexp.last_match(2).strip
|
86
91
|
item = Item.new(date, title, section)
|
87
|
-
@content
|
88
|
-
|
89
|
-
|
90
|
-
# if content[section][:items].length - 1 == current
|
92
|
+
@content.push(item)
|
93
|
+
elsif @content.count.zero?
|
94
|
+
# if content[section].items.length - 1 == current
|
91
95
|
@other_content_top.push(line)
|
92
96
|
elsif line =~ /^\S/
|
93
97
|
@other_content_bottom.push(line)
|
94
98
|
else
|
95
|
-
prev_item = @content
|
99
|
+
prev_item = @content.last
|
96
100
|
prev_item.note = Note.new unless prev_item.note
|
97
101
|
|
98
102
|
prev_item.note.add(line)
|
99
103
|
# end
|
100
104
|
end
|
101
105
|
end
|
106
|
+
|
102
107
|
Hooks.trigger :post_read, self
|
103
108
|
end
|
104
109
|
|
@@ -119,7 +124,7 @@ module Doing
|
|
119
124
|
##
|
120
125
|
## @param input [String] Text input for editor
|
121
126
|
##
|
122
|
-
def fork_editor(input = '')
|
127
|
+
def fork_editor(input = '', message: :default)
|
123
128
|
# raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
124
129
|
|
125
130
|
raise MissingEditor, 'No EDITOR variable defined in environment' if Util.default_editor.nil?
|
@@ -128,7 +133,9 @@ module Doing
|
|
128
133
|
|
129
134
|
File.open(tmpfile.path, 'w+') do |f|
|
130
135
|
f.puts input
|
131
|
-
|
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
|
132
139
|
end
|
133
140
|
|
134
141
|
pid = Process.fork { system("#{Util.editor_with_args} #{tmpfile.path}") }
|
@@ -174,13 +181,37 @@ module Doing
|
|
174
181
|
title = input_lines[0]&.strip
|
175
182
|
raise EmptyInput, 'No content in first line' if title.nil? || title.strip.empty?
|
176
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
|
+
|
177
208
|
note = Note.new
|
178
209
|
note.add(input_lines[1..-1]) if input_lines.length > 1
|
179
210
|
# If title line ends in a parenthetical, use that as the note
|
180
211
|
if note.empty? && title =~ /\s+\(.*?\)$/
|
181
|
-
title.sub!(/\s+\((
|
212
|
+
title.sub!(/\s+\((?<note>.*?)\)$/) do
|
182
213
|
m = Regexp.last_match
|
183
|
-
note.add(m[
|
214
|
+
note.add(m['note'])
|
184
215
|
''
|
185
216
|
end
|
186
217
|
end
|
@@ -188,7 +219,7 @@ module Doing
|
|
188
219
|
note.strip_lines!
|
189
220
|
note.compress
|
190
221
|
|
191
|
-
[title, note]
|
222
|
+
[date, title, note]
|
192
223
|
end
|
193
224
|
|
194
225
|
##
|
@@ -262,21 +293,7 @@ module Doing
|
|
262
293
|
## @return [Array] section titles
|
263
294
|
##
|
264
295
|
def sections
|
265
|
-
@content.
|
266
|
-
end
|
267
|
-
|
268
|
-
##
|
269
|
-
## Adds a section.
|
270
|
-
##
|
271
|
-
## @param title [String] The new section title
|
272
|
-
##
|
273
|
-
def add_section(title)
|
274
|
-
if @content.key?(title.cap_first)
|
275
|
-
raise InvalidSection, %(section "#{title.cap_first}" already exists)
|
276
|
-
end
|
277
|
-
|
278
|
-
@content[title.cap_first] = { :original => "#{title}:", :items => [] }
|
279
|
-
logger.info('New section:', %("#{title.cap_first}"))
|
296
|
+
@content.section_titles
|
280
297
|
end
|
281
298
|
|
282
299
|
##
|
@@ -289,8 +306,9 @@ module Doing
|
|
289
306
|
return 'All' if frag =~ /^all$/i
|
290
307
|
frag ||= @config['current_section']
|
291
308
|
|
292
|
-
|
293
|
-
|
309
|
+
return frag.cap_first if @content.section?(frag)
|
310
|
+
|
311
|
+
section = nil
|
294
312
|
re = frag.split('').join('.*?')
|
295
313
|
sections.each do |sect|
|
296
314
|
next unless sect =~ /#{re}/i
|
@@ -305,79 +323,25 @@ module Doing
|
|
305
323
|
unless section || guessed
|
306
324
|
alt = guess_view(frag, guessed: true, suggest: true)
|
307
325
|
if alt
|
308
|
-
meant_view = yn("#{
|
326
|
+
meant_view = Prompt.yn("#{boldwhite("Did you mean")} `#{yellow("doing view #{alt}")}#{boldwhite}`?", default_response: 'n')
|
309
327
|
|
310
328
|
raise WrongCommand.new("run again with #{"doing view #{alt}".boldwhite}", topic: 'Try again:') if meant_view
|
311
329
|
|
312
330
|
end
|
313
331
|
|
314
|
-
res = yn("#{
|
332
|
+
res = Prompt.yn("#{boldwhite}Section #{frag.yellow}#{boldwhite} not found, create it", default_response: 'n')
|
315
333
|
|
316
334
|
if res
|
317
|
-
add_section(frag.cap_first)
|
335
|
+
@content.add_section(frag.cap_first, log: true)
|
318
336
|
write(@doing_file)
|
319
337
|
return frag.cap_first
|
320
338
|
end
|
321
339
|
|
322
|
-
raise InvalidSection.new("unknown section #{frag.
|
340
|
+
raise InvalidSection.new("unknown section #{frag.bold.white}", topic: 'Missing:')
|
323
341
|
end
|
324
342
|
section ? section.cap_first : guessed
|
325
343
|
end
|
326
344
|
|
327
|
-
##
|
328
|
-
## Ask a yes or no question in the terminal
|
329
|
-
##
|
330
|
-
## @param question [String] The question
|
331
|
-
## to ask
|
332
|
-
## @param default_response (Bool) default
|
333
|
-
## response if no input
|
334
|
-
##
|
335
|
-
## @return (Bool) yes or no
|
336
|
-
##
|
337
|
-
def yn(question, default_response: false)
|
338
|
-
if default_response.is_a?(String)
|
339
|
-
default = default_response =~ /y/i ? true : false
|
340
|
-
else
|
341
|
-
default = default_response
|
342
|
-
end
|
343
|
-
|
344
|
-
# if global --default is set, answer default
|
345
|
-
return default if @default_option
|
346
|
-
|
347
|
-
# if this isn't an interactive shell, answer default
|
348
|
-
return default unless $stdout.isatty
|
349
|
-
|
350
|
-
# clear the buffer
|
351
|
-
if ARGV&.length
|
352
|
-
ARGV.length.times do
|
353
|
-
ARGV.shift
|
354
|
-
end
|
355
|
-
end
|
356
|
-
system 'stty cbreak'
|
357
|
-
|
358
|
-
cw = Color.white
|
359
|
-
cbw = Color.boldwhite
|
360
|
-
cbg = Color.boldgreen
|
361
|
-
cd = Color.default
|
362
|
-
|
363
|
-
options = unless default.nil?
|
364
|
-
"#{cw}[#{default ? "#{cbg}Y#{cw}/#{cbw}n" : "#{cbw}y#{cw}/#{cbg}N"}#{cw}]#{cd}"
|
365
|
-
else
|
366
|
-
"#{cw}[#{cbw}y#{cw}/#{cbw}n#{cw}]#{cd}"
|
367
|
-
end
|
368
|
-
$stdout.syswrite "#{cbw}#{question.sub(/\?$/, '')} #{options}#{cbw}?#{cd} "
|
369
|
-
res = $stdin.sysread 1
|
370
|
-
puts
|
371
|
-
system 'stty cooked'
|
372
|
-
|
373
|
-
res.chomp!
|
374
|
-
res.downcase!
|
375
|
-
|
376
|
-
return default if res.empty?
|
377
|
-
|
378
|
-
res =~ /y/i ? true : false
|
379
|
-
end
|
380
|
-
|
381
345
|
##
|
382
346
|
## Attempt to match a string with an existing view
|
383
347
|
##
|
@@ -397,11 +361,14 @@ module Doing
|
|
397
361
|
end
|
398
362
|
unless view || guessed
|
399
363
|
alt = guess_section(frag, guessed: true, suggest: true)
|
400
|
-
|
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')
|
401
368
|
|
402
369
|
raise WrongCommand.new("run again with #{"doing show #{alt}".yellow}", topic: 'Try again:') if meant_view
|
403
370
|
|
404
|
-
raise InvalidView.new(%(
|
371
|
+
raise InvalidView.new(%(unknown view #{alt.bold.white}), topic: 'Missing:')
|
405
372
|
end
|
406
373
|
view
|
407
374
|
end
|
@@ -411,17 +378,22 @@ module Doing
|
|
411
378
|
##
|
412
379
|
## @param title [String] The entry title
|
413
380
|
## @param section [String] The section to add to
|
414
|
-
## @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
|
415
387
|
##
|
416
388
|
def add_item(title, section = nil, opt = {})
|
417
389
|
section ||= @config['current_section']
|
418
|
-
add_section(section
|
390
|
+
@content.add_section(section, log: false)
|
391
|
+
opt[:back] ||= opt[:date] ? opt[:date] : Time.now
|
419
392
|
opt[:date] ||= Time.now
|
420
|
-
|
421
|
-
opt[:back] ||= Time.now
|
393
|
+
note = Note.new
|
422
394
|
opt[:timed] ||= false
|
423
395
|
|
424
|
-
|
396
|
+
note.add(opt[:note]) if opt[:note]
|
425
397
|
|
426
398
|
title = [title.strip.cap_first]
|
427
399
|
title = title.join(' ')
|
@@ -431,10 +403,11 @@ module Doing
|
|
431
403
|
title.add_tags!(@config['default_tags']) unless @config['default_tags'].empty?
|
432
404
|
end
|
433
405
|
|
434
|
-
title.
|
406
|
+
title.compress!
|
435
407
|
entry = Item.new(opt[:back], title.strip, section)
|
436
|
-
entry.note =
|
437
|
-
|
408
|
+
entry.note = note
|
409
|
+
|
410
|
+
items = @content.dup
|
438
411
|
if opt[:timed]
|
439
412
|
items.reverse!
|
440
413
|
items.each_with_index do |i, x|
|
@@ -443,10 +416,9 @@ module Doing
|
|
443
416
|
items[x].title = "#{i.title} @done(#{opt[:back].strftime('%F %R')})"
|
444
417
|
break
|
445
418
|
end
|
446
|
-
items.reverse!
|
447
419
|
end
|
448
420
|
|
449
|
-
|
421
|
+
@content.push(entry)
|
450
422
|
# logger.count(:added, level: :debug)
|
451
423
|
logger.info('New entry:', %(added "#{entry.title}" to #{section}))
|
452
424
|
end
|
@@ -457,16 +429,10 @@ module Doing
|
|
457
429
|
## @param items [Array] The items to deduplicate
|
458
430
|
## @param no_overlap [Boolean] Remove items with overlapping time spans
|
459
431
|
##
|
460
|
-
def dedup(items, no_overlap
|
461
|
-
|
462
|
-
combined = []
|
463
|
-
@content.each do |_k, v|
|
464
|
-
combined += v[:items]
|
465
|
-
end
|
466
|
-
|
432
|
+
def dedup(items, no_overlap: false)
|
467
433
|
items.delete_if do |item|
|
468
434
|
duped = false
|
469
|
-
|
435
|
+
@content.each do |comp|
|
470
436
|
duped = no_overlap ? item.overlapping_time?(comp) : item.same_time?(comp)
|
471
437
|
break if duped
|
472
438
|
end
|
@@ -516,17 +482,32 @@ module Doing
|
|
516
482
|
"#{last_item.title}\n# EDIT BELOW THIS LINE ------------\n#{note}"
|
517
483
|
end
|
518
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
|
+
#
|
519
491
|
def reset_item(item, resume: false)
|
520
492
|
item.date = Time.now
|
521
|
-
if resume
|
522
|
-
item.tag('done', remove: true)
|
523
|
-
end
|
493
|
+
item.tag('done', remove: true) if resume
|
524
494
|
logger.info('Reset:', %(Reset #{resume ? 'and resumed ' : ''} "#{item.title}" in #{item.section}))
|
525
495
|
item
|
526
496
|
end
|
527
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
|
+
#
|
528
510
|
def repeat_item(item, opt = {})
|
529
|
-
original = item.dup
|
530
511
|
if item.should_finish?
|
531
512
|
if item.should_time?
|
532
513
|
item.title.tag!('done', value: Time.now.strftime('%F %R'))
|
@@ -543,10 +524,13 @@ module Doing
|
|
543
524
|
note = opt[:note] || Note.new
|
544
525
|
|
545
526
|
if opt[:editor]
|
546
|
-
|
547
|
-
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?
|
548
530
|
new_item = fork_editor(to_edit)
|
549
|
-
title, note = format_input(new_item)
|
531
|
+
date, title, note = format_input(new_item)
|
532
|
+
|
533
|
+
opt[:date] = date unless date.nil?
|
550
534
|
|
551
535
|
if title.nil? || title.empty?
|
552
536
|
logger.warn('Skipped:', 'No content provided')
|
@@ -554,9 +538,8 @@ module Doing
|
|
554
538
|
end
|
555
539
|
end
|
556
540
|
|
557
|
-
update_item(original, item)
|
541
|
+
# @content.update_item(original, item)
|
558
542
|
add_item(title, section, { note: note, back: opt[:date], timed: true })
|
559
|
-
write(@doing_file)
|
560
543
|
end
|
561
544
|
|
562
545
|
##
|
@@ -566,6 +549,7 @@ module Doing
|
|
566
549
|
##
|
567
550
|
def repeat_last(opt = {})
|
568
551
|
opt[:section] ||= 'all'
|
552
|
+
opt[:section] = guess_section(opt[:section])
|
569
553
|
opt[:note] ||= []
|
570
554
|
opt[:tag] ||= []
|
571
555
|
opt[:tag_bool] ||= :and
|
@@ -577,6 +561,7 @@ module Doing
|
|
577
561
|
end
|
578
562
|
|
579
563
|
repeat_item(last, opt)
|
564
|
+
write(@doing_file)
|
580
565
|
end
|
581
566
|
|
582
567
|
##
|
@@ -588,19 +573,19 @@ module Doing
|
|
588
573
|
opt[:tag_bool] ||= :and
|
589
574
|
opt[:section] ||= @config['current_section']
|
590
575
|
|
591
|
-
items = filter_items(
|
576
|
+
items = filter_items(Items.new, opt: opt)
|
592
577
|
|
593
578
|
logger.debug('Filtered:', "Parameters matched #{items.count} entries")
|
594
579
|
|
595
580
|
if opt[:interactive]
|
596
|
-
last_entry = choose_from_items(items,
|
581
|
+
last_entry = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i,
|
597
582
|
menu: true,
|
598
583
|
header: '',
|
599
584
|
prompt: 'Select an entry > ',
|
600
585
|
multiple: false,
|
601
586
|
sort: false,
|
602
587
|
show_if_single: true
|
603
|
-
|
588
|
+
)
|
604
589
|
else
|
605
590
|
last_entry = items.max_by { |item| item.date }
|
606
591
|
end
|
@@ -608,53 +593,6 @@ module Doing
|
|
608
593
|
last_entry
|
609
594
|
end
|
610
595
|
|
611
|
-
def fzf
|
612
|
-
@fzf ||= install_fzf
|
613
|
-
end
|
614
|
-
|
615
|
-
def install_fzf
|
616
|
-
fzf_dir = File.join(File.dirname(__FILE__), '../helpers/fzf')
|
617
|
-
FileUtils.mkdir_p(fzf_dir) unless File.directory?(fzf_dir)
|
618
|
-
fzf_bin = File.join(fzf_dir, 'bin/fzf')
|
619
|
-
return fzf_bin if File.exist?(fzf_bin)
|
620
|
-
|
621
|
-
prev_level = Doing.logger.level
|
622
|
-
Doing.logger.adjust_verbosity({ log_level: :info })
|
623
|
-
Doing.logger.log_now(:warn, 'Compiling and installing fzf -- this will only happen once')
|
624
|
-
Doing.logger.log_now(:warn, 'fzf is copyright Junegunn Choi, MIT License <https://github.com/junegunn/fzf/blob/master/LICENSE>')
|
625
|
-
|
626
|
-
system("'#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null")
|
627
|
-
unless File.exist?(fzf_bin)
|
628
|
-
Doing.logger.log_now(:warn, 'Error installing, trying again as root')
|
629
|
-
system("sudo '#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null")
|
630
|
-
end
|
631
|
-
raise RuntimeError.new('Error installing fzf, please report at https://github.com/ttscoff/doing/issues') unless File.exist?(fzf_bin)
|
632
|
-
|
633
|
-
Doing.logger.info("fzf installed to #{fzf}")
|
634
|
-
Doing.logger.adjust_verbosity({ log_level: prev_level })
|
635
|
-
fzf_bin
|
636
|
-
end
|
637
|
-
|
638
|
-
##
|
639
|
-
## Generate a menu of options and allow user selection
|
640
|
-
##
|
641
|
-
## @return [String] The selected option
|
642
|
-
##
|
643
|
-
def choose_from(options, prompt: 'Make a selection: ', multiple: false, sorted: true, fzf_args: [])
|
644
|
-
return nil unless $stdout.isatty
|
645
|
-
|
646
|
-
# fzf_args << '-1' # User is expecting a menu, and even if only one it seves as confirmation
|
647
|
-
fzf_args << %(--prompt "#{prompt}")
|
648
|
-
fzf_args << '--multi' if multiple
|
649
|
-
header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
|
650
|
-
fzf_args << %(--header "#{header}")
|
651
|
-
options.sort! if sorted
|
652
|
-
res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
|
653
|
-
return false if res.strip.size.zero?
|
654
|
-
|
655
|
-
res
|
656
|
-
end
|
657
|
-
|
658
596
|
def all_tags(items, opt: {})
|
659
597
|
all_tags = []
|
660
598
|
items.each { |item| all_tags.concat(item.tags).uniq! }
|
@@ -693,8 +631,8 @@ module Doing
|
|
693
631
|
end
|
694
632
|
# fzf_args << '-e' if opt[:exact]
|
695
633
|
# puts fzf_args.join(' ')
|
696
|
-
res = `echo #{Shellwords.escape(scannable)}|#{fzf} #{fzf_args.join(' ')}`
|
697
|
-
selected =
|
634
|
+
res = `echo #{Shellwords.escape(scannable)}|#{Prompt.fzf} #{fzf_args.join(' ')}`
|
635
|
+
selected = Items.new
|
698
636
|
res.split(/\n/).each do |item|
|
699
637
|
idx = item.match(/\|(\d+)$/)[1].to_i
|
700
638
|
selected.push(items[idx])
|
@@ -722,15 +660,11 @@ module Doing
|
|
722
660
|
## @option opt [Number] :count (Number to return)
|
723
661
|
## @option opt [String] :age ('old' or 'new')
|
724
662
|
##
|
725
|
-
def filter_items(items =
|
663
|
+
def filter_items(items = Items.new, opt: {})
|
726
664
|
if items.nil? || items.empty?
|
727
665
|
section = opt[:section] ? guess_section(opt[:section]) : 'All'
|
728
666
|
|
729
|
-
items =
|
730
|
-
@content.each_with_object([]) { |(_k, v), arr| arr.concat(v[:items].dup) }
|
731
|
-
else
|
732
|
-
@content[section][:items].dup
|
733
|
-
end
|
667
|
+
items = section =~ /^all$/i ? @content.dup : @content.in_section(section)
|
734
668
|
end
|
735
669
|
|
736
670
|
items.sort_by! { |item| [item.date, item.title.downcase] }.reverse
|
@@ -805,14 +739,17 @@ module Doing
|
|
805
739
|
|
806
740
|
keep
|
807
741
|
end
|
808
|
-
count = opt[:count]
|
742
|
+
count = opt[:count]&.positive? ? opt[:count] : filtered_items.length
|
743
|
+
|
744
|
+
output = Items.new
|
809
745
|
|
810
746
|
if opt[:age] =~ /^o/i
|
811
|
-
filtered_items.slice(0, count).reverse
|
747
|
+
output.concat(filtered_items.slice(0, count).reverse)
|
812
748
|
else
|
813
|
-
filtered_items.reverse.slice(0, count)
|
749
|
+
output.concat(filtered_items.reverse.slice(0, count))
|
814
750
|
end
|
815
751
|
|
752
|
+
output
|
816
753
|
end
|
817
754
|
|
818
755
|
##
|
@@ -837,94 +774,15 @@ module Doing
|
|
837
774
|
opt[:query] = "!#{opt[:query]}" if opt[:not]
|
838
775
|
opt[:multiple] = true
|
839
776
|
opt[:show_if_single] = true
|
840
|
-
items = filter_items(
|
777
|
+
items = filter_items(Items.new, opt: { section: section, search: opt[:search], fuzzy: opt[:fuzzy], case: opt[:case], not: opt[:not] })
|
841
778
|
|
842
|
-
selection = choose_from_items(items,
|
779
|
+
selection = Prompt.choose_from_items(items, include_section: section =~ /^all$/i, **opt)
|
843
780
|
|
844
781
|
raise NoResults, 'no items selected' if selection.nil? || selection.empty?
|
845
782
|
|
846
783
|
act_on(selection, opt)
|
847
784
|
end
|
848
785
|
|
849
|
-
##
|
850
|
-
## Create an interactive menu to select from a set of Items
|
851
|
-
##
|
852
|
-
## @param items [Array] list of items
|
853
|
-
## @param opt [Hash] options
|
854
|
-
## @param include_section [Boolean] include section
|
855
|
-
##
|
856
|
-
## @option opt [String] :header
|
857
|
-
## @option opt [String] :prompt
|
858
|
-
## @option opt [String] :query
|
859
|
-
## @option opt [Boolean] :show_if_single
|
860
|
-
## @option opt [Boolean] :menu
|
861
|
-
## @option opt [Boolean] :sort
|
862
|
-
## @option opt [Boolean] :multiple
|
863
|
-
## @option opt [Symbol] :case (:sensitive, :ignore, :smart)
|
864
|
-
##
|
865
|
-
def choose_from_items(items, opt = {}, include_section: false)
|
866
|
-
return items unless $stdout.isatty
|
867
|
-
|
868
|
-
return nil unless items.count.positive?
|
869
|
-
|
870
|
-
opt[:case] ||= :smart
|
871
|
-
opt[:header] ||= "Arrows: navigate, tab: mark for selection, ctrl-a: select all, enter: commit"
|
872
|
-
opt[:prompt] ||= "Select entries to act on > "
|
873
|
-
|
874
|
-
pad = items.length.to_s.length
|
875
|
-
options = items.map.with_index do |item, i|
|
876
|
-
out = [
|
877
|
-
format("%#{pad}d", i),
|
878
|
-
') ',
|
879
|
-
format('%13s', item.date.relative_date),
|
880
|
-
' | ',
|
881
|
-
item.title
|
882
|
-
]
|
883
|
-
if include_section
|
884
|
-
out.concat([
|
885
|
-
' (',
|
886
|
-
item.section,
|
887
|
-
') '
|
888
|
-
])
|
889
|
-
end
|
890
|
-
out.join('')
|
891
|
-
end
|
892
|
-
|
893
|
-
fzf_args = [
|
894
|
-
%(--header="#{opt[:header]}"),
|
895
|
-
%(--prompt="#{opt[:prompt].sub(/ *$/, ' ')}"),
|
896
|
-
opt[:multiple] ? '--multi' : '--no-multi',
|
897
|
-
'-0',
|
898
|
-
'--bind ctrl-a:select-all',
|
899
|
-
%(-q "#{opt[:query]}"),
|
900
|
-
'--info=inline'
|
901
|
-
]
|
902
|
-
fzf_args.push('-1') unless opt[:show_if_single]
|
903
|
-
fzf_args << case opt[:case].normalize_case
|
904
|
-
when :sensitive
|
905
|
-
'+i'
|
906
|
-
when :ignore
|
907
|
-
'-i'
|
908
|
-
end
|
909
|
-
fzf_args << '-e' if opt[:exact]
|
910
|
-
|
911
|
-
|
912
|
-
unless opt[:menu]
|
913
|
-
raise InvalidArgument, "Can't skip menu when no query is provided" unless opt[:query] && !opt[:query].empty?
|
914
|
-
|
915
|
-
fzf_args.concat([%(--filter="#{opt[:query]}"), opt[:sort] ? '' : '--no-sort'])
|
916
|
-
end
|
917
|
-
|
918
|
-
res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
|
919
|
-
selected = []
|
920
|
-
res.split(/\n/).each do |item|
|
921
|
-
idx = item.match(/^ *(\d+)\)/)[1].to_i
|
922
|
-
selected.push(items[idx])
|
923
|
-
end
|
924
|
-
|
925
|
-
opt[:multiple] ? selected : selected[0]
|
926
|
-
end
|
927
|
-
|
928
786
|
##
|
929
787
|
## Perform actions on a set of entries. If
|
930
788
|
## no valid action is included in the opt
|
@@ -974,11 +832,11 @@ module Doing
|
|
974
832
|
|
975
833
|
actions.concat(['resume/repeat', 'begin/reset']) if items.count == 1
|
976
834
|
|
977
|
-
choice = choose_from(actions,
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
|
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'])
|
982
840
|
return unless choice
|
983
841
|
|
984
842
|
to_do = choice.strip.split(/\n/)
|
@@ -992,7 +850,7 @@ module Doing
|
|
992
850
|
type = action =~ /^add/ ? 'add' : 'remove'
|
993
851
|
raise InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
|
994
852
|
|
995
|
-
print "#{
|
853
|
+
print "#{yellow("Tag to #{type}: ")}#{reset}"
|
996
854
|
tag = $stdin.gets
|
997
855
|
next if tag =~ /^ *$/
|
998
856
|
|
@@ -1000,17 +858,22 @@ module Doing
|
|
1000
858
|
opt[:remove] = true if type == 'remove'
|
1001
859
|
when /output formatted/
|
1002
860
|
plugins = Plugins.available_plugins(type: :export).sort
|
1003
|
-
output_format = choose_from(plugins,
|
1004
|
-
|
1005
|
-
|
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
|
+
])
|
1006
869
|
next if tag =~ /^ *$/
|
1007
870
|
|
1008
871
|
raise UserCancelled unless output_format
|
1009
872
|
|
1010
873
|
opt[:output] = output_format.strip
|
1011
|
-
res = opt[:force] ? false : yn('Save to file?', default_response: 'n')
|
874
|
+
res = opt[:force] ? false : Prompt.yn('Save to file?', default_response: 'n')
|
1012
875
|
if res
|
1013
|
-
print "#{
|
876
|
+
print "#{yellow('File path/name: ')}#{reset}"
|
1014
877
|
filename = $stdin.gets.strip
|
1015
878
|
next if filename.empty?
|
1016
879
|
|
@@ -1036,29 +899,28 @@ module Doing
|
|
1036
899
|
end
|
1037
900
|
|
1038
901
|
if opt[:resume] || opt[:reset]
|
1039
|
-
if items.count > 1
|
1040
|
-
|
1041
|
-
|
1042
|
-
|
1043
|
-
|
1044
|
-
|
1045
|
-
|
1046
|
-
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
update_item(item, reset_item(item, resume: res))
|
1052
|
-
end
|
1053
|
-
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))
|
1054
914
|
end
|
915
|
+
write(@doing_file)
|
916
|
+
|
1055
917
|
return
|
1056
918
|
end
|
1057
919
|
|
1058
920
|
if opt[:delete]
|
1059
|
-
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')
|
1060
922
|
if res
|
1061
|
-
items.each { |
|
923
|
+
items.each { |i| @content.delete_item(i, single: items.count == 1) }
|
1062
924
|
write(@doing_file)
|
1063
925
|
end
|
1064
926
|
return
|
@@ -1066,31 +928,31 @@ module Doing
|
|
1066
928
|
|
1067
929
|
if opt[:flag]
|
1068
930
|
tag = @config['marker_tag'] || 'flagged'
|
1069
|
-
items.map! do |
|
1070
|
-
|
931
|
+
items.map! do |i|
|
932
|
+
i.tag(tag, date: false, remove: opt[:remove], single: single)
|
1071
933
|
end
|
1072
934
|
end
|
1073
935
|
|
1074
936
|
if opt[:finish] || opt[:cancel]
|
1075
937
|
tag = 'done'
|
1076
|
-
items.map! do |
|
1077
|
-
if
|
1078
|
-
should_date = !opt[:cancel] &&
|
1079
|
-
|
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)
|
1080
942
|
end
|
1081
943
|
end
|
1082
944
|
end
|
1083
945
|
|
1084
946
|
if opt[:tag]
|
1085
947
|
tag = opt[:tag]
|
1086
|
-
items.map! do |
|
1087
|
-
|
948
|
+
items.map! do |i|
|
949
|
+
i.tag(tag, date: false, remove: opt[:remove], single: single)
|
1088
950
|
end
|
1089
951
|
end
|
1090
952
|
|
1091
953
|
if opt[:archive] || opt[:move]
|
1092
954
|
section = opt[:archive] ? 'Archive' : guess_section(opt[:move])
|
1093
|
-
items.map! {|
|
955
|
+
items.map! { |i| i.move_to(section, label: true) }
|
1094
956
|
end
|
1095
957
|
|
1096
958
|
write(@doing_file)
|
@@ -1099,111 +961,88 @@ module Doing
|
|
1099
961
|
|
1100
962
|
editable_items = []
|
1101
963
|
|
1102
|
-
items.each do |
|
1103
|
-
editable = "#{
|
1104
|
-
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
|
1105
967
|
editable += "\n#{old_note}" unless old_note.nil?
|
1106
968
|
editable_items << editable
|
1107
969
|
end
|
1108
970
|
divider = "\n-----------\n"
|
1109
|
-
|
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}"
|
1110
978
|
|
1111
979
|
new_items = fork_editor(input).split(/#{divider}/)
|
1112
980
|
|
1113
981
|
new_items.each_with_index do |new_item, i|
|
1114
|
-
|
1115
982
|
input_lines = new_item.split(/[\n\r]+/).delete_if(&:ignore?)
|
1116
|
-
|
983
|
+
first_line = input_lines[0]&.strip
|
1117
984
|
|
1118
|
-
if
|
1119
|
-
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)
|
1120
988
|
else
|
1121
|
-
note =
|
989
|
+
date, title, note = format_input(new_item)
|
1122
990
|
|
1123
991
|
note.map!(&:strip)
|
1124
992
|
note.delete_if(&:ignore?)
|
1125
|
-
|
1126
|
-
date = title.match(/^([\d\-: ]+) \| /)[1]
|
1127
|
-
title.sub!(/^([\d\-: ]+) \| /, '')
|
1128
|
-
|
1129
993
|
item = items[i]
|
994
|
+
old_item = item.dup
|
995
|
+
item.date = date || items[i].date
|
1130
996
|
item.title = title
|
1131
997
|
item.note = note
|
1132
|
-
item.
|
998
|
+
if (item.equal?(old_item))
|
999
|
+
Doing.logger.count(:skipped, level: :debug)
|
1000
|
+
else
|
1001
|
+
Doing.logger.count(:updated)
|
1002
|
+
end
|
1133
1003
|
end
|
1134
1004
|
end
|
1135
1005
|
|
1136
1006
|
write(@doing_file)
|
1137
1007
|
end
|
1138
1008
|
|
1139
|
-
|
1140
|
-
items.map! do |item|
|
1141
|
-
item.title = "#{item.title} @project(#{item.section})"
|
1142
|
-
item
|
1143
|
-
end
|
1144
|
-
|
1145
|
-
@content = { 'Export' => { :original => 'Export:', :items => items } }
|
1146
|
-
options = { section: 'Export' }
|
1147
|
-
|
1148
|
-
|
1149
|
-
if opt[:output] =~ /doing/
|
1150
|
-
options[:output] = 'template'
|
1151
|
-
options[:template] = '- %date | %title%note'
|
1152
|
-
else
|
1153
|
-
options[:output] = opt[:output]
|
1154
|
-
options[:template] = opt[:template] || nil
|
1155
|
-
end
|
1009
|
+
return unless opt[:output]
|
1156
1010
|
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1160
|
-
file = File.expand_path(opt[:save_to])
|
1161
|
-
if File.exist?(file)
|
1162
|
-
# Create a backup copy for the undo command
|
1163
|
-
FileUtils.cp(file, "#{file}~")
|
1164
|
-
end
|
1165
|
-
|
1166
|
-
File.open(file, 'w+') do |f|
|
1167
|
-
f.puts output
|
1168
|
-
end
|
1169
|
-
|
1170
|
-
logger.warn('File written:', file)
|
1171
|
-
else
|
1172
|
-
Doing::Pager.page output
|
1173
|
-
end
|
1011
|
+
items.map! do |i|
|
1012
|
+
i.title = "#{i.title} @project(#{i.section})"
|
1013
|
+
i
|
1174
1014
|
end
|
1175
|
-
end
|
1176
1015
|
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
## @param tags [String] The tag to apply
|
1182
|
-
## @param remove [Boolean] remove tags?
|
1183
|
-
## @param date [Boolean] Include timestamp?
|
1184
|
-
## @param single [Boolean] Log as a single change?
|
1185
|
-
##
|
1186
|
-
## @return [Item] updated item
|
1187
|
-
##
|
1188
|
-
def tag_item(item, tags, remove: false, date: false, single: false)
|
1189
|
-
added = []
|
1190
|
-
removed = []
|
1016
|
+
@content = Items.new
|
1017
|
+
@content.concat(items)
|
1018
|
+
@content.add_section(Section.new('Export'), log: false)
|
1019
|
+
options = { section: 'Export' }
|
1191
1020
|
|
1192
|
-
|
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
|
1193
1028
|
|
1194
|
-
|
1029
|
+
output = list_section(options)
|
1195
1030
|
|
1196
|
-
|
1197
|
-
|
1198
|
-
if
|
1199
|
-
|
1200
|
-
|
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}~")
|
1201
1036
|
end
|
1202
|
-
end
|
1203
1037
|
|
1204
|
-
|
1038
|
+
File.open(file, 'w+') do |f|
|
1039
|
+
f.puts output
|
1040
|
+
end
|
1205
1041
|
|
1206
|
-
|
1042
|
+
logger.warn('File written:', file)
|
1043
|
+
else
|
1044
|
+
Doing::Pager.page output
|
1045
|
+
end
|
1207
1046
|
end
|
1208
1047
|
|
1209
1048
|
##
|
@@ -1227,17 +1066,15 @@ module Doing
|
|
1227
1066
|
opt[:unfinished] ||= false
|
1228
1067
|
opt[:section] = opt[:section] ? guess_section(opt[:section]) : 'All'
|
1229
1068
|
|
1230
|
-
items = filter_items(
|
1069
|
+
items = filter_items(Items.new, opt: opt)
|
1231
1070
|
|
1232
1071
|
if opt[:interactive]
|
1233
|
-
items = choose_from_items(items,
|
1234
|
-
menu: true,
|
1072
|
+
items = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i, menu: true,
|
1235
1073
|
header: '',
|
1236
1074
|
prompt: 'Select entries to tag > ',
|
1237
1075
|
multiple: true,
|
1238
1076
|
sort: true,
|
1239
|
-
show_if_single: true
|
1240
|
-
}, include_section: opt[:section] =~ /^all$/i)
|
1077
|
+
show_if_single: true)
|
1241
1078
|
|
1242
1079
|
raise NoResults, 'no items selected' if items.empty?
|
1243
1080
|
|
@@ -1318,12 +1155,12 @@ module Doing
|
|
1318
1155
|
end
|
1319
1156
|
end
|
1320
1157
|
|
1321
|
-
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)
|
1322
1159
|
|
1323
1160
|
item.note.add(opt[:note]) if opt[:note]
|
1324
1161
|
|
1325
1162
|
if opt[:archive] && opt[:section] != 'Archive' && (opt[:count]).positive?
|
1326
|
-
|
1163
|
+
item.move_to('Archive', label: true)
|
1327
1164
|
elsif opt[:archive] && opt[:count].zero?
|
1328
1165
|
logger.warn('Skipped:', 'Archiving is skipped when operating on all entries')
|
1329
1166
|
end
|
@@ -1332,29 +1169,6 @@ module Doing
|
|
1332
1169
|
write(@doing_file)
|
1333
1170
|
end
|
1334
1171
|
|
1335
|
-
##
|
1336
|
-
## Move item from current section to
|
1337
|
-
## destination section
|
1338
|
-
##
|
1339
|
-
## @param item [Item] The item to move
|
1340
|
-
## @param section [String] The destination section
|
1341
|
-
##
|
1342
|
-
## @return [Item] Updated item
|
1343
|
-
##
|
1344
|
-
def move_item(item, section, label: true)
|
1345
|
-
from = item.section
|
1346
|
-
new_item = @content[item.section][:items].delete(item)
|
1347
|
-
new_item.title.sub!(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{from})") if label
|
1348
|
-
new_item.section = section
|
1349
|
-
|
1350
|
-
@content[section][:items].concat([new_item])
|
1351
|
-
|
1352
|
-
logger.count(section == 'Archive' ? :archived : :moved)
|
1353
|
-
logger.debug("#{section == 'Archive' ? 'Archived' : 'Moved'}:",
|
1354
|
-
"#{new_item.title.truncate(60)} from #{from} to #{section}")
|
1355
|
-
new_item
|
1356
|
-
end
|
1357
|
-
|
1358
1172
|
##
|
1359
1173
|
## Get next item in the index
|
1360
1174
|
##
|
@@ -1365,49 +1179,13 @@ module Doing
|
|
1365
1179
|
## @return [Item] the next chronological item in the index
|
1366
1180
|
##
|
1367
1181
|
def next_item(item, options = {})
|
1368
|
-
items = filter_items(
|
1182
|
+
items = filter_items(Items.new, opt: options)
|
1369
1183
|
|
1370
1184
|
idx = items.index(item)
|
1371
1185
|
|
1372
1186
|
idx.positive? ? items[idx - 1] : nil
|
1373
1187
|
end
|
1374
1188
|
|
1375
|
-
##
|
1376
|
-
## Delete an item from the index
|
1377
|
-
##
|
1378
|
-
## @param item The item
|
1379
|
-
##
|
1380
|
-
def delete_item(item, single: false)
|
1381
|
-
section = item.section
|
1382
|
-
|
1383
|
-
section_items = @content[section][:items]
|
1384
|
-
deleted = section_items.delete(item)
|
1385
|
-
logger.count(:deleted)
|
1386
|
-
logger.info('Entry deleted:', deleted.title) if single
|
1387
|
-
end
|
1388
|
-
|
1389
|
-
##
|
1390
|
-
## Update an item in the index with a modified item
|
1391
|
-
##
|
1392
|
-
## @param old_item The old item
|
1393
|
-
## @param new_item The new item
|
1394
|
-
##
|
1395
|
-
def update_item(old_item, new_item)
|
1396
|
-
section = old_item.section
|
1397
|
-
|
1398
|
-
section_items = @content[section][:items]
|
1399
|
-
s_idx = section_items.index { |item| item.equal?(old_item) }
|
1400
|
-
|
1401
|
-
raise ItemNotFound, 'Unable to find item in index, did it mutate?' unless s_idx
|
1402
|
-
|
1403
|
-
return if section_items[s_idx].equal?(new_item)
|
1404
|
-
|
1405
|
-
section_items[s_idx] = new_item
|
1406
|
-
logger.count(:updated)
|
1407
|
-
logger.info('Entry updated:', section_items[s_idx].title.truncate(60))
|
1408
|
-
new_item
|
1409
|
-
end
|
1410
|
-
|
1411
1189
|
##
|
1412
1190
|
## Edit the last entry
|
1413
1191
|
##
|
@@ -1423,16 +1201,18 @@ module Doing
|
|
1423
1201
|
return
|
1424
1202
|
end
|
1425
1203
|
|
1426
|
-
content = [item.title.dup]
|
1427
|
-
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?
|
1428
1206
|
new_item = fork_editor(content.join("\n"))
|
1429
|
-
title, note = format_input(new_item)
|
1207
|
+
date, title, note = format_input(new_item)
|
1208
|
+
date ||= item.date
|
1430
1209
|
|
1431
1210
|
if title.nil? || title.empty?
|
1432
1211
|
logger.debug('Skipped:', 'No content provided')
|
1433
|
-
elsif title == item.title && note.equal?(item.note)
|
1212
|
+
elsif title == item.title && note.equal?(item.note) && date.equal?(item.date)
|
1434
1213
|
logger.debug('Skipped:', 'No change in content')
|
1435
1214
|
else
|
1215
|
+
item.date = date unless date.nil?
|
1436
1216
|
item.title = title
|
1437
1217
|
item.note.add(note, replace: true)
|
1438
1218
|
logger.info('Edited:', item.title)
|
@@ -1451,6 +1231,11 @@ module Doing
|
|
1451
1231
|
## @param target_tag [String] Tag to replace
|
1452
1232
|
## @param opt [Hash] Additional Options
|
1453
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
|
1454
1239
|
def stop_start(target_tag, opt = {})
|
1455
1240
|
tag = target_tag.dup
|
1456
1241
|
opt[:section] ||= @config['current_section']
|
@@ -1465,7 +1250,9 @@ module Doing
|
|
1465
1250
|
|
1466
1251
|
found_items = 0
|
1467
1252
|
|
1468
|
-
@content
|
1253
|
+
@content.each_with_index do |item, i|
|
1254
|
+
next unless item.section == opt[:section] || opt[:section] =~ /all/i
|
1255
|
+
|
1469
1256
|
next unless item.title =~ /@#{tag}/
|
1470
1257
|
|
1471
1258
|
item.title.add_tags!([tag, 'done'], remove: true)
|
@@ -1475,7 +1262,7 @@ module Doing
|
|
1475
1262
|
|
1476
1263
|
if opt[:archive] && opt[:section] != 'Archive'
|
1477
1264
|
item.title = item.title.sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{item.section})")
|
1478
|
-
|
1265
|
+
item.move_to('Archive', label: false, log: false)
|
1479
1266
|
logger.count(:completed_archived)
|
1480
1267
|
logger.info('Completed/archived:', item.title)
|
1481
1268
|
else
|
@@ -1487,7 +1274,8 @@ module Doing
|
|
1487
1274
|
logger.debug('Skipped:', "No active @#{tag} tasks found.") if found_items.zero?
|
1488
1275
|
|
1489
1276
|
if opt[:new_item]
|
1490
|
-
title, note = format_input(opt[:new_item])
|
1277
|
+
date, title, note = format_input(opt[:new_item])
|
1278
|
+
opt[:back] = date unless date.nil?
|
1491
1279
|
note.add(opt[:note]) if opt[:note]
|
1492
1280
|
title.tag!(tag)
|
1493
1281
|
add_item(title.cap_first, opt[:section], { note: note, back: opt[:back] })
|
@@ -1504,7 +1292,6 @@ module Doing
|
|
1504
1292
|
def write(file = nil, backup: true)
|
1505
1293
|
Hooks.trigger :pre_write, self, file
|
1506
1294
|
output = combined_content
|
1507
|
-
|
1508
1295
|
if file.nil?
|
1509
1296
|
$stdout.puts output
|
1510
1297
|
else
|
@@ -1537,70 +1324,46 @@ module Doing
|
|
1537
1324
|
bool = opt[:bool] || :and
|
1538
1325
|
sect = opt[:section] !~ /^all$/i ? guess_section(opt[:section]) : 'all'
|
1539
1326
|
|
1540
|
-
|
1541
|
-
all_sections = sections.dup
|
1542
|
-
else
|
1543
|
-
all_sections = [sect]
|
1544
|
-
end
|
1545
|
-
|
1546
|
-
counter = 0
|
1547
|
-
new_content = {}
|
1548
|
-
|
1549
|
-
|
1550
|
-
all_sections.each do |section|
|
1551
|
-
items = @content[section][:items].dup
|
1552
|
-
new_content[section] = {}
|
1553
|
-
new_content[section][:original] = @content[section][:original]
|
1554
|
-
new_content[section][:items] = []
|
1555
|
-
|
1556
|
-
moved_items = []
|
1557
|
-
if !tags.empty? || opt[:search] || opt[:before]
|
1558
|
-
if opt[:before]
|
1559
|
-
time_string = opt[:before]
|
1560
|
-
cutoff = chronify(time_string, guess: :begin)
|
1561
|
-
end
|
1327
|
+
section = guess_section(sect)
|
1562
1328
|
|
1563
|
-
|
1564
|
-
|
1565
|
-
moved_items.push(item)
|
1566
|
-
counter += 1
|
1567
|
-
true
|
1568
|
-
else
|
1569
|
-
false
|
1570
|
-
end
|
1571
|
-
end
|
1572
|
-
@content[section][:items] = items
|
1573
|
-
new_content[section][:items] = moved_items
|
1574
|
-
logger.warn('Rotated:', "#{moved_items.length} items from #{section}")
|
1575
|
-
else
|
1576
|
-
new_content[section][:items] = []
|
1577
|
-
moved_items = []
|
1329
|
+
section_items = @content.in_section(section)
|
1330
|
+
max = section_items.count - keep.to_i
|
1578
1331
|
|
1579
|
-
|
1332
|
+
counter = 0
|
1333
|
+
new_content = Items.new
|
1580
1334
|
|
1581
|
-
|
1582
|
-
|
1583
|
-
|
1584
|
-
|
1585
|
-
|
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
|
1586
1341
|
|
1587
|
-
|
1588
|
-
|
1589
|
-
|
1590
|
-
items[0..count - 1]
|
1591
|
-
end
|
1592
|
-
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?
|
1593
1345
|
|
1594
|
-
|
1346
|
+
new_content.add_section(new_item.section, log: false)
|
1347
|
+
new_content.push(new_item)
|
1348
|
+
counter += 1
|
1595
1349
|
end
|
1596
1350
|
end
|
1597
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
|
+
|
1598
1361
|
write(@doing_file)
|
1599
1362
|
|
1600
1363
|
file = @doing_file.sub(/(\.\w+)$/, "_#{Time.now.strftime('%Y-%m-%d')}\\1")
|
1601
1364
|
if File.exist?(file)
|
1602
1365
|
init_doing_file(file)
|
1603
|
-
@content.
|
1366
|
+
@content.concat(new_content).uniq!
|
1604
1367
|
logger.warn('File update:', "added entries to existing file: #{file}")
|
1605
1368
|
else
|
1606
1369
|
@content = new_content
|
@@ -1616,7 +1379,7 @@ module Doing
|
|
1616
1379
|
## @return [String] The selected section name
|
1617
1380
|
##
|
1618
1381
|
def choose_section
|
1619
|
-
choice = choose_from(
|
1382
|
+
choice = Prompt.choose_from(@content.section_titles.sort, prompt: 'Choose a section > ', fzf_args: ['--height=60%'])
|
1620
1383
|
choice ? choice.strip : choice
|
1621
1384
|
end
|
1622
1385
|
|
@@ -1635,7 +1398,7 @@ module Doing
|
|
1635
1398
|
## @return [String] The selected view name
|
1636
1399
|
##
|
1637
1400
|
def choose_view
|
1638
|
-
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%'])
|
1639
1402
|
choice ? choice.strip : choice
|
1640
1403
|
end
|
1641
1404
|
|
@@ -1693,7 +1456,7 @@ module Doing
|
|
1693
1456
|
end
|
1694
1457
|
end
|
1695
1458
|
|
1696
|
-
items = filter_items(
|
1459
|
+
items = filter_items(Items.new, opt: opt).reverse
|
1697
1460
|
|
1698
1461
|
items.reverse! if opt[:order] =~ /^d/i
|
1699
1462
|
|
@@ -1701,7 +1464,7 @@ module Doing
|
|
1701
1464
|
opt[:menu] = !opt[:force]
|
1702
1465
|
opt[:query] = '' # opt[:search]
|
1703
1466
|
opt[:multiple] = true
|
1704
|
-
selected = choose_from_items(items,
|
1467
|
+
selected = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i, **opt)
|
1705
1468
|
|
1706
1469
|
raise NoResults, 'no items selected' if selected.empty?
|
1707
1470
|
|
@@ -1709,11 +1472,8 @@ module Doing
|
|
1709
1472
|
return
|
1710
1473
|
end
|
1711
1474
|
|
1712
|
-
|
1713
1475
|
opt[:output] ||= 'template'
|
1714
|
-
|
1715
1476
|
opt[:wrap_width] ||= @config['templates']['default']['wrap_width'] || 0
|
1716
|
-
|
1717
1477
|
output(items, title, is_single, opt)
|
1718
1478
|
end
|
1719
1479
|
|
@@ -1734,11 +1494,12 @@ module Doing
|
|
1734
1494
|
archive_all = section =~ /^all$/i # && !(tags.nil? || tags.empty?)
|
1735
1495
|
section = guess_section(section) unless archive_all
|
1736
1496
|
|
1737
|
-
add_section(
|
1497
|
+
@content.add_section(destination, log: true)
|
1498
|
+
# add_section(Section.new('Archive')) if destination =~ /^archive$/i && !@content.section?('Archive')
|
1738
1499
|
|
1739
1500
|
destination = guess_section(destination)
|
1740
1501
|
|
1741
|
-
if
|
1502
|
+
if @content.section?(destination) && (@content.section?(section) || archive_all)
|
1742
1503
|
do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label], before: options[:before] })
|
1743
1504
|
write(doing_file)
|
1744
1505
|
else
|
@@ -1978,7 +1739,6 @@ module Doing
|
|
1978
1739
|
end
|
1979
1740
|
end
|
1980
1741
|
|
1981
|
-
|
1982
1742
|
logger.debug('Autotag:', "whitelisted tags: #{tagged[:whitelisted].log_tags}") unless tagged[:whitelisted].empty?
|
1983
1743
|
logger.debug('Autotag:', "synonyms: #{tagged[:synonyms].log_tags}") unless tagged[:synonyms].empty?
|
1984
1744
|
logger.debug('Autotag:', "transforms: #{tagged[:transformed].log_tags}") unless tagged[:transformed].empty?
|
@@ -1991,10 +1751,10 @@ module Doing
|
|
1991
1751
|
text.add_tags!(tail_tags) unless tail_tags.empty?
|
1992
1752
|
|
1993
1753
|
if text == original
|
1994
|
-
logger.debug('Autotag:', "no change to \"#{text}\"")
|
1754
|
+
logger.debug('Autotag:', "no change to \"#{text.strip}\"")
|
1995
1755
|
else
|
1996
1756
|
new_tags = tagged[:whitelisted].concat(tail_tags).concat(tagged[:replaced])
|
1997
|
-
logger.debug('Autotag:', "added #{new_tags.log_tags} to \"#{text}\"")
|
1757
|
+
logger.debug('Autotag:', "added #{new_tags.log_tags} to \"#{text.strip}\"")
|
1998
1758
|
logger.count(:autotag, level: :info, count: 1, message: 'autotag updated %count %items')
|
1999
1759
|
end
|
2000
1760
|
|
@@ -2171,7 +1931,7 @@ EOS
|
|
2171
1931
|
def format_time(seconds, human: false)
|
2172
1932
|
return [0, 0, 0] if seconds.nil?
|
2173
1933
|
|
2174
|
-
if seconds.
|
1934
|
+
if seconds.instance_of?(String) && seconds =~ /(\d+):(\d+):(\d+)/
|
2175
1935
|
h = Regexp.last_match(1)
|
2176
1936
|
m = Regexp.last_match(2)
|
2177
1937
|
s = Regexp.last_match(3)
|
@@ -2200,13 +1960,13 @@ EOS
|
|
2200
1960
|
##
|
2201
1961
|
def combined_content
|
2202
1962
|
output = @other_content_top ? "#{@other_content_top.join("\n")}\n" : ''
|
2203
|
-
|
2204
|
-
|
2205
|
-
|
2206
|
-
output += list_section({ section: title, template: "\t- %date | %title%t2note", highlight: false, wrap_width: 0, tags_color: false })
|
2207
|
-
end
|
2208
|
-
|
1963
|
+
was_color = Color.coloring?
|
1964
|
+
Color.coloring = false
|
1965
|
+
output += @content.to_s
|
2209
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
|
+
|
2210
1970
|
output.uncolor
|
2211
1971
|
end
|
2212
1972
|
|
@@ -2261,99 +2021,50 @@ EOS
|
|
2261
2021
|
##
|
2262
2022
|
## Helper function, performs the actual archiving
|
2263
2023
|
##
|
2264
|
-
## @param
|
2024
|
+
## @param section [String] The source section
|
2265
2025
|
## @param destination [String] The destination
|
2266
2026
|
## section
|
2267
2027
|
## @param opt [Hash] Additional Options
|
2268
2028
|
##
|
2269
|
-
def do_archive(
|
2029
|
+
def do_archive(section, destination, opt = {})
|
2270
2030
|
count = opt[:count] || 0
|
2271
2031
|
tags = opt[:tags] || []
|
2272
2032
|
bool = opt[:bool] || :and
|
2273
2033
|
label = opt[:label] || true
|
2274
2034
|
|
2275
|
-
|
2276
|
-
|
2277
|
-
all_sections.delete(destination)
|
2278
|
-
else
|
2279
|
-
all_sections = [sect]
|
2280
|
-
end
|
2281
|
-
|
2282
|
-
counter = 0
|
2035
|
+
section = guess_section(section)
|
2036
|
+
destination = guess_section(destination)
|
2283
2037
|
|
2284
|
-
|
2285
|
-
|
2038
|
+
section_items = @content.in_section(section)
|
2039
|
+
max = section_items.count - count.to_i
|
2286
2040
|
|
2287
|
-
|
2288
|
-
if !tags.empty? || opt[:search] || opt[:before]
|
2289
|
-
if opt[:before]
|
2290
|
-
time_string = opt[:before]
|
2291
|
-
cutoff = chronify(time_string, guess: :begin)
|
2292
|
-
end
|
2041
|
+
counter = 0
|
2293
2042
|
|
2294
|
-
|
2295
|
-
|
2296
|
-
|
2297
|
-
|
2298
|
-
|
2299
|
-
|
2300
|
-
false
|
2301
|
-
end
|
2302
|
-
end
|
2303
|
-
moved_items.each do |item|
|
2304
|
-
if label
|
2305
|
-
item.title = if section == @config['current_section']
|
2306
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
|
2307
|
-
else
|
2308
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
2309
|
-
end
|
2310
|
-
logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
|
2311
|
-
end
|
2312
|
-
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
|
2313
2049
|
|
2314
|
-
|
2315
|
-
|
2316
|
-
|
2317
|
-
|
2318
|
-
level: :info,
|
2319
|
-
count: moved_items.length,
|
2320
|
-
message: "%count %items from #{section} to #{destination}")
|
2321
|
-
else
|
2322
|
-
logger.info('Skipped:', 'No items were moved')
|
2323
|
-
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
|
2324
2054
|
else
|
2325
|
-
|
2326
|
-
|
2327
|
-
items.map! do |item|
|
2328
|
-
if label
|
2329
|
-
item.title = if section == @config['current_section']
|
2330
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
|
2331
|
-
else
|
2332
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
2333
|
-
end
|
2334
|
-
logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
|
2335
|
-
end
|
2336
|
-
item
|
2337
|
-
end
|
2338
|
-
|
2339
|
-
if items.count > count
|
2340
|
-
@content[destination][:items].concat(items[count..-1])
|
2341
|
-
else
|
2342
|
-
@content[destination][:items].concat(items)
|
2343
|
-
end
|
2344
|
-
|
2345
|
-
@content[section][:items] = if count.zero?
|
2346
|
-
[]
|
2347
|
-
else
|
2348
|
-
items[0..count - 1]
|
2349
|
-
end
|
2350
|
-
|
2351
|
-
logger.count(destination == 'Archive' ? :archived : :moved,
|
2352
|
-
level: :info,
|
2353
|
-
count: items.length - count,
|
2354
|
-
message: "%count %items from #{section} to #{destination}")
|
2055
|
+
counter += 1
|
2056
|
+
item.move_to(destination, label: label, log: false)
|
2355
2057
|
end
|
2356
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
|
2357
2068
|
end
|
2358
2069
|
|
2359
2070
|
def run_after
|
@@ -2365,31 +2076,5 @@ EOS
|
|
2365
2076
|
logger.log_now(:error, 'Script error:', "Error running #{@config['run_after']}")
|
2366
2077
|
logger.log_now(:error, 'STDERR output:', stderr)
|
2367
2078
|
end
|
2368
|
-
|
2369
|
-
def log_change(tags_added: [], tags_removed: [], count: 1, item: nil, single: false)
|
2370
|
-
if tags_added.empty? && tags_removed.empty?
|
2371
|
-
logger.count(:skipped, level: :debug, message: '%count %items with no change', count: count)
|
2372
|
-
else
|
2373
|
-
if tags_added.empty?
|
2374
|
-
logger.count(:skipped, level: :debug, message: 'no tags added to %count %items')
|
2375
|
-
else
|
2376
|
-
if single && item
|
2377
|
-
logger.info('Tagged:', %(added #{tags_added.count == 1 ? 'tag' : 'tags'} #{tags_added.map {|t| "@#{t}"}.join(', ')} to #{item.title}))
|
2378
|
-
else
|
2379
|
-
logger.count(:added_tags, level: :info, tag: tags_added, message: '%tags added to %count %items')
|
2380
|
-
end
|
2381
|
-
end
|
2382
|
-
|
2383
|
-
if tags_removed.empty?
|
2384
|
-
logger.count(:skipped, level: :debug, message: 'no tags removed from %count %items')
|
2385
|
-
else
|
2386
|
-
if single && item
|
2387
|
-
logger.info('Untagged:', %(removed #{tags_removed.count == 1 ? 'tag' : 'tags'} #{tags_added.map {|t| "@#{t}"}.join(', ')} from #{item.title}))
|
2388
|
-
else
|
2389
|
-
logger.count(:removed_tags, level: :info, tag: tags_removed, message: '%tags removed from %count %items')
|
2390
|
-
end
|
2391
|
-
end
|
2392
|
-
end
|
2393
|
-
end
|
2394
2079
|
end
|
2395
2080
|
end
|