doing 2.0.7.pre → 2.0.11
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 +20 -0
- data/.yardoc/complete +0 -0
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +13 -9
- data/Gemfile.lock +30 -10
- data/README.md +1 -1
- data/Rakefile +8 -1
- data/bin/doing +368 -21
- data/doc/Array.html +135 -0
- data/doc/Doing/Color.html +506 -0
- data/doc/Doing/Configuration.html +680 -0
- data/doc/Doing/Errors/DoingNoTraceError.html +186 -0
- data/doc/Doing/Errors/DoingRuntimeError.html +186 -0
- data/doc/Doing/Errors/DoingStandardError.html +186 -0
- data/doc/Doing/Errors/EmptyInput.html +186 -0
- data/doc/Doing/Errors/NoResults.html +186 -0
- data/doc/Doing/Errors/PluginException.html +248 -0
- data/doc/Doing/Errors/UserCancelled.html +186 -0
- data/doc/Doing/Errors/WrongCommand.html +186 -0
- data/doc/Doing/Errors.html +191 -0
- data/doc/Doing/Hooks.html +364 -0
- data/doc/Doing/Item.html +1385 -0
- data/doc/Doing/Items.html +393 -0
- data/doc/Doing/LogAdapter.html +1650 -0
- data/doc/Doing/Note.html +535 -0
- data/doc/Doing/Pager.html +268 -0
- data/doc/Doing/Plugins.html +849 -0
- data/doc/Doing/Util.html +870 -0
- data/doc/Doing/WWID.html +4827 -0
- data/doc/Doing.html +145 -0
- data/doc/GLI/Commands/MarkdownDocumentListener.html +763 -0
- data/doc/GLI/Commands.html +115 -0
- data/doc/GLI.html +115 -0
- data/doc/Hash.html +332 -0
- data/doc/Status.html +292 -0
- data/doc/String.html +1714 -0
- data/doc/Symbol.html +250 -0
- data/doc/Time.html +182 -0
- data/doc/_index.html +411 -0
- data/doc/class_list.html +51 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +58 -0
- data/doc/css/style.css +497 -0
- data/doc/file.README.html +123 -0
- data/doc/file_list.html +56 -0
- data/doc/frames.html +17 -0
- data/doc/index.html +123 -0
- data/doc/js/app.js +314 -0
- data/doc/js/full_list.js +216 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +1867 -0
- data/doc/top-level-namespace.html +112 -0
- data/doing.gemspec +5 -1
- data/doing.rdoc +354 -6
- data/example_plugin.rb +6 -6
- data/lib/doing/array.rb +15 -2
- data/lib/doing/configuration.rb +14 -12
- data/lib/doing/errors.rb +1 -1
- data/lib/doing/hash.rb +1 -1
- data/lib/doing/item.rb +113 -23
- data/lib/doing/log_adapter.rb +132 -119
- data/lib/doing/note.rb +1 -1
- data/lib/doing/plugin_manager.rb +5 -5
- data/lib/doing/plugins/export/csv_export.rb +1 -1
- data/lib/doing/plugins/export/template_export.rb +5 -7
- data/lib/doing/plugins/import/calendar_import.rb +8 -2
- data/lib/doing/plugins/import/doing_import.rb +10 -10
- data/lib/doing/plugins/import/timing_import.rb +12 -4
- data/lib/doing/string.rb +96 -21
- data/lib/doing/symbol.rb +9 -5
- data/lib/doing/time.rb +1 -1
- data/lib/doing/util.rb +18 -11
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +510 -368
- data/lib/doing/wwidfile.rb +5 -5
- data/lib/doing.rb +2 -1
- data/lib/examples/plugins/say_export.rb +6 -6
- data/lib/examples/plugins/{templates → wiki_export/templates}/wiki.css +0 -0
- data/lib/examples/plugins/{templates → wiki_export/templates}/wiki.haml +0 -0
- data/lib/examples/plugins/{templates → wiki_export/templates}/wiki_index.haml +0 -0
- data/lib/examples/plugins/{wiki_export.rb → wiki_export/wiki_export.rb} +0 -0
- data/rdocfixer.rb +1 -1
- data/yard_templates/default/method_details/setup.rb +3 -0
- metadata +121 -8
data/lib/doing/wwid.rb
CHANGED
|
@@ -9,7 +9,7 @@ require 'erb'
|
|
|
9
9
|
|
|
10
10
|
module Doing
|
|
11
11
|
##
|
|
12
|
-
##
|
|
12
|
+
## Main "What Was I Doing" methods
|
|
13
13
|
##
|
|
14
14
|
class WWID
|
|
15
15
|
attr_reader :additional_configs, :current_section, :doing_file, :content
|
|
@@ -19,7 +19,7 @@ module Doing
|
|
|
19
19
|
# include Util
|
|
20
20
|
|
|
21
21
|
##
|
|
22
|
-
##
|
|
22
|
+
## Initializes the object.
|
|
23
23
|
##
|
|
24
24
|
def initialize
|
|
25
25
|
@timers = {}
|
|
@@ -32,7 +32,7 @@ module Doing
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
##
|
|
35
|
-
##
|
|
35
|
+
## Logger
|
|
36
36
|
##
|
|
37
37
|
## Responds to :debug, :info, :warn, and :error
|
|
38
38
|
##
|
|
@@ -45,9 +45,9 @@ module Doing
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
##
|
|
48
|
-
##
|
|
48
|
+
## Initializes the doing file.
|
|
49
49
|
##
|
|
50
|
-
## @param path
|
|
50
|
+
## @param path [String] Override path to a doing file, optional
|
|
51
51
|
##
|
|
52
52
|
def init_doing_file(path = nil)
|
|
53
53
|
@doing_file = File.expand_path(@config['doing_file'])
|
|
@@ -106,7 +106,7 @@ module Doing
|
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
##
|
|
109
|
-
##
|
|
109
|
+
## Create a new doing file
|
|
110
110
|
##
|
|
111
111
|
def create(filename = nil)
|
|
112
112
|
filename = @doing_file if filename.nil?
|
|
@@ -118,9 +118,9 @@ module Doing
|
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
##
|
|
121
|
-
##
|
|
121
|
+
## Create a process for an editor and wait for the file handle to return
|
|
122
122
|
##
|
|
123
|
-
## @param input
|
|
123
|
+
## @param input [String] Text input for editor
|
|
124
124
|
##
|
|
125
125
|
def fork_editor(input = '')
|
|
126
126
|
# raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
|
@@ -164,13 +164,11 @@ module Doing
|
|
|
164
164
|
end
|
|
165
165
|
|
|
166
166
|
##
|
|
167
|
-
##
|
|
167
|
+
## Takes a multi-line string and formats it as an entry
|
|
168
168
|
##
|
|
169
|
-
## @
|
|
169
|
+
## @param input [String] The string to parse
|
|
170
170
|
##
|
|
171
|
-
## @
|
|
172
|
-
##
|
|
173
|
-
## @return (Array) [(String)title, (Note)note]
|
|
171
|
+
## @return [Array] [[String]title, [Note]note]
|
|
174
172
|
##
|
|
175
173
|
def format_input(input)
|
|
176
174
|
raise EmptyInput, 'No content in entry' if input.nil? || input.strip.empty?
|
|
@@ -197,15 +195,15 @@ module Doing
|
|
|
197
195
|
end
|
|
198
196
|
|
|
199
197
|
##
|
|
200
|
-
##
|
|
198
|
+
## Converts input string into a Time object when input takes on the
|
|
201
199
|
## following formats:
|
|
202
200
|
## - interval format e.g. '1d2h30m', '45m' etc.
|
|
203
201
|
## - a semantic phrase e.g. 'yesterday 5:30pm'
|
|
204
202
|
## - a strftime e.g. '2016-03-15 15:32:04 PDT'
|
|
205
203
|
##
|
|
206
|
-
## @param input
|
|
204
|
+
## @param input [String] String to chronify
|
|
207
205
|
##
|
|
208
|
-
## @return
|
|
206
|
+
## @return [DateTime] result
|
|
209
207
|
##
|
|
210
208
|
def chronify(input, future: false, guess: :begin)
|
|
211
209
|
now = Time.now
|
|
@@ -229,13 +227,13 @@ module Doing
|
|
|
229
227
|
end
|
|
230
228
|
|
|
231
229
|
##
|
|
232
|
-
##
|
|
230
|
+
## Converts simple strings into seconds that can be added to a Time
|
|
233
231
|
## object
|
|
234
232
|
##
|
|
235
|
-
## @param qty
|
|
233
|
+
## @param qty [String] HH:MM or XX[dhm][[XXhm][XXm]] (1d2h30m, 45m,
|
|
236
234
|
## 1.5d, 1h20m, etc.)
|
|
237
235
|
##
|
|
238
|
-
## @return
|
|
236
|
+
## @return [Integer] seconds
|
|
239
237
|
##
|
|
240
238
|
def chronify_qty(qty)
|
|
241
239
|
minutes = 0
|
|
@@ -262,18 +260,18 @@ module Doing
|
|
|
262
260
|
end
|
|
263
261
|
|
|
264
262
|
##
|
|
265
|
-
##
|
|
263
|
+
## List sections
|
|
266
264
|
##
|
|
267
|
-
## @return
|
|
265
|
+
## @return [Array] section titles
|
|
268
266
|
##
|
|
269
267
|
def sections
|
|
270
268
|
@content.keys
|
|
271
269
|
end
|
|
272
270
|
|
|
273
271
|
##
|
|
274
|
-
##
|
|
272
|
+
## Adds a section.
|
|
275
273
|
##
|
|
276
|
-
## @param title
|
|
274
|
+
## @param title [String] The new section title
|
|
277
275
|
##
|
|
278
276
|
def add_section(title)
|
|
279
277
|
if @content.key?(title.cap_first)
|
|
@@ -285,10 +283,10 @@ module Doing
|
|
|
285
283
|
end
|
|
286
284
|
|
|
287
285
|
##
|
|
288
|
-
##
|
|
286
|
+
## Attempt to match a string with an existing section
|
|
289
287
|
##
|
|
290
|
-
## @param frag
|
|
291
|
-
## @param guessed
|
|
288
|
+
## @param frag [String] The user-provided string
|
|
289
|
+
## @param guessed [Boolean] already guessed and failed
|
|
292
290
|
##
|
|
293
291
|
def guess_section(frag, guessed: false, suggest: false)
|
|
294
292
|
return 'All' if frag =~ /^all$/i
|
|
@@ -330,10 +328,12 @@ module Doing
|
|
|
330
328
|
end
|
|
331
329
|
|
|
332
330
|
##
|
|
333
|
-
##
|
|
331
|
+
## Ask a yes or no question in the terminal
|
|
334
332
|
##
|
|
335
|
-
## @param question
|
|
336
|
-
##
|
|
333
|
+
## @param question [String] The question
|
|
334
|
+
## to ask
|
|
335
|
+
## @param default_response (Bool) default
|
|
336
|
+
## response if no input
|
|
337
337
|
##
|
|
338
338
|
## @return (Bool) yes or no
|
|
339
339
|
##
|
|
@@ -382,10 +382,10 @@ module Doing
|
|
|
382
382
|
end
|
|
383
383
|
|
|
384
384
|
##
|
|
385
|
-
##
|
|
385
|
+
## Attempt to match a string with an existing view
|
|
386
386
|
##
|
|
387
|
-
## @param frag
|
|
388
|
-
## @param guessed
|
|
387
|
+
## @param frag [String] The user-provided string
|
|
388
|
+
## @param guessed [Boolean] already guessed
|
|
389
389
|
##
|
|
390
390
|
def guess_view(frag, guessed: false, suggest: false)
|
|
391
391
|
views.each { |view| return view if frag.downcase == view.downcase }
|
|
@@ -410,11 +410,11 @@ module Doing
|
|
|
410
410
|
end
|
|
411
411
|
|
|
412
412
|
##
|
|
413
|
-
##
|
|
413
|
+
## Adds an entry
|
|
414
414
|
##
|
|
415
|
-
## @param title
|
|
416
|
-
## @param section
|
|
417
|
-
## @param opt
|
|
415
|
+
## @param title [String] The entry title
|
|
416
|
+
## @param section [String] The section to add to
|
|
417
|
+
## @param opt [Hash] Additional Options: :date, :note, :back, :timed
|
|
418
418
|
##
|
|
419
419
|
def add_item(title, section = nil, opt = {})
|
|
420
420
|
section ||= @config['current_section']
|
|
@@ -455,10 +455,10 @@ module Doing
|
|
|
455
455
|
end
|
|
456
456
|
|
|
457
457
|
##
|
|
458
|
-
##
|
|
458
|
+
## Remove items from a list that already exist in @content
|
|
459
459
|
##
|
|
460
|
-
## @param items
|
|
461
|
-
## @param no_overlap
|
|
460
|
+
## @param items [Array] The items to deduplicate
|
|
461
|
+
## @param no_overlap [Boolean] Remove items with overlapping time spans
|
|
462
462
|
##
|
|
463
463
|
def dedup(items, no_overlap = false)
|
|
464
464
|
|
|
@@ -473,17 +473,17 @@ module Doing
|
|
|
473
473
|
duped = no_overlap ? item.overlapping_time?(comp) : item.same_time?(comp)
|
|
474
474
|
break if duped
|
|
475
475
|
end
|
|
476
|
-
logger.count(:skipped, level: :debug, message: 'overlapping %
|
|
476
|
+
logger.count(:skipped, level: :debug, message: '%count overlapping %items') if duped
|
|
477
477
|
# logger.log_now(:debug, 'Skipped:', "overlapping entry: #{item.title}") if duped
|
|
478
478
|
duped
|
|
479
479
|
end
|
|
480
480
|
end
|
|
481
481
|
|
|
482
482
|
##
|
|
483
|
-
##
|
|
483
|
+
## Imports external entries
|
|
484
484
|
##
|
|
485
|
-
## @param
|
|
486
|
-
## @param opt
|
|
485
|
+
## @param paths [String] Path to JSON report file
|
|
486
|
+
## @param opt [Hash] Additional Options
|
|
487
487
|
##
|
|
488
488
|
def import(paths, opt = {})
|
|
489
489
|
Plugins.plugins[:import].each do |_, options|
|
|
@@ -501,9 +501,9 @@ module Doing
|
|
|
501
501
|
end
|
|
502
502
|
|
|
503
503
|
##
|
|
504
|
-
##
|
|
504
|
+
## Return the content of the last note for a given section
|
|
505
505
|
##
|
|
506
|
-
## @param section
|
|
506
|
+
## @param section [String] The section to retrieve from, default
|
|
507
507
|
## All
|
|
508
508
|
##
|
|
509
509
|
def last_note(section = 'All')
|
|
@@ -563,9 +563,9 @@ module Doing
|
|
|
563
563
|
end
|
|
564
564
|
|
|
565
565
|
##
|
|
566
|
-
##
|
|
566
|
+
## Restart the last entry
|
|
567
567
|
##
|
|
568
|
-
## @param opt
|
|
568
|
+
## @param opt [Hash] Additional Options
|
|
569
569
|
##
|
|
570
570
|
def repeat_last(opt = {})
|
|
571
571
|
opt[:section] ||= 'all'
|
|
@@ -583,9 +583,9 @@ module Doing
|
|
|
583
583
|
end
|
|
584
584
|
|
|
585
585
|
##
|
|
586
|
-
##
|
|
586
|
+
## Get the last entry
|
|
587
587
|
##
|
|
588
|
-
## @param opt
|
|
588
|
+
## @param opt [Hash] Additional Options
|
|
589
589
|
##
|
|
590
590
|
def last_entry(opt = {})
|
|
591
591
|
opt[:tag_bool] ||= :and
|
|
@@ -612,9 +612,9 @@ module Doing
|
|
|
612
612
|
end
|
|
613
613
|
|
|
614
614
|
##
|
|
615
|
-
##
|
|
615
|
+
## Generate a menu of options and allow user selection
|
|
616
616
|
##
|
|
617
|
-
## @return
|
|
617
|
+
## @return [String] The selected option
|
|
618
618
|
##
|
|
619
619
|
def choose_from(options, prompt: 'Make a selection: ', multiple: false, sorted: true, fzf_args: [])
|
|
620
620
|
return nil unless $stdout.isatty
|
|
@@ -650,27 +650,56 @@ module Doing
|
|
|
650
650
|
tag_groups
|
|
651
651
|
end
|
|
652
652
|
|
|
653
|
+
def fuzzy_filter_items(items, opt: {})
|
|
654
|
+
scannable = items.map.with_index { |item, idx| "#{item.title} #{item.note.join(' ')}".gsub(/[|*?!]/, '') + "|#{idx}" }.join("\n")
|
|
655
|
+
|
|
656
|
+
fzf = File.join(File.dirname(__FILE__), '../helpers/fuzzyfilefinder')
|
|
657
|
+
|
|
658
|
+
fzf_args = [
|
|
659
|
+
'--multi',
|
|
660
|
+
%(--filter="#{opt[:search].sub(/^'?/, "'")}"),
|
|
661
|
+
'--no-sort',
|
|
662
|
+
'-d "\|"',
|
|
663
|
+
'--nth=1'
|
|
664
|
+
]
|
|
665
|
+
if opt[:case]
|
|
666
|
+
fzf_args << case opt[:case].normalize_case
|
|
667
|
+
when :sensitive
|
|
668
|
+
'+i'
|
|
669
|
+
when :ignore
|
|
670
|
+
'-i'
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
# fzf_args << '-e' if opt[:exact]
|
|
674
|
+
# puts fzf_args.join(' ')
|
|
675
|
+
res = `echo #{Shellwords.escape(scannable)}|#{fzf} #{fzf_args.join(' ')}`
|
|
676
|
+
selected = []
|
|
677
|
+
res.split(/\n/).each do |item|
|
|
678
|
+
idx = item.match(/\|(\d+)$/)[1].to_i
|
|
679
|
+
selected.push(items[idx])
|
|
680
|
+
end
|
|
681
|
+
selected
|
|
682
|
+
end
|
|
683
|
+
|
|
653
684
|
##
|
|
654
|
-
##
|
|
655
|
-
##
|
|
656
|
-
## @param items (Array) The items to filter (if empty, filters all items)
|
|
657
|
-
## @param opt (Hash) The filter parameters
|
|
685
|
+
## Filter items based on search criteria
|
|
658
686
|
##
|
|
659
|
-
##
|
|
687
|
+
## @param items [Array] The items to filter (if empty, filters all items)
|
|
688
|
+
## @param opt [Hash] The filter parameters
|
|
660
689
|
##
|
|
661
|
-
##
|
|
662
|
-
##
|
|
663
|
-
##
|
|
664
|
-
##
|
|
665
|
-
##
|
|
666
|
-
##
|
|
667
|
-
##
|
|
668
|
-
##
|
|
669
|
-
##
|
|
670
|
-
##
|
|
671
|
-
##
|
|
672
|
-
##
|
|
673
|
-
##
|
|
690
|
+
## @option opt [String] :section
|
|
691
|
+
## @option opt [Boolean] :unfinished
|
|
692
|
+
## @option opt [Array or String] :tag (Array or comma-separated string)
|
|
693
|
+
## @option opt [Symbol] :tag_bool (:and, :or, :not)
|
|
694
|
+
## @option opt [String] :search (string, optional regex with //)
|
|
695
|
+
## @option opt [Array] :date_filter [[Time]start, [Time]end]
|
|
696
|
+
## @option opt [Boolean] :only_timed
|
|
697
|
+
## @option opt [String] :before (Date/Time string, unparsed)
|
|
698
|
+
## @option opt [String] :after (Date/Time string, unparsed)
|
|
699
|
+
## @option opt [Boolean] :today
|
|
700
|
+
## @option opt [Boolean] :yesterday
|
|
701
|
+
## @option opt [Number] :count (Number to return)
|
|
702
|
+
## @option opt [String] :age ('old' or 'new')
|
|
674
703
|
##
|
|
675
704
|
def filter_items(items = [], opt: {})
|
|
676
705
|
if items.nil? || items.empty?
|
|
@@ -684,20 +713,31 @@ module Doing
|
|
|
684
713
|
end
|
|
685
714
|
|
|
686
715
|
items.sort_by! { |item| [item.date, item.title.downcase] }.reverse
|
|
716
|
+
|
|
687
717
|
filtered_items = items.select do |item|
|
|
688
718
|
keep = true
|
|
689
|
-
|
|
690
|
-
|
|
719
|
+
if opt[:unfinished]
|
|
720
|
+
finished = item.tags?('done', :and)
|
|
721
|
+
finished = opt[:not] ? !finished : finished
|
|
722
|
+
keep = false if finished
|
|
723
|
+
end
|
|
691
724
|
|
|
692
725
|
if keep && opt[:tag]
|
|
693
726
|
opt[:tag_bool] ||= :and
|
|
694
727
|
tag_match = opt[:tag].nil? || opt[:tag].empty? ? true : item.tags?(opt[:tag], opt[:tag_bool])
|
|
695
728
|
keep = false unless tag_match
|
|
729
|
+
keep = opt[:not] ? !keep : keep
|
|
696
730
|
end
|
|
697
731
|
|
|
698
732
|
if keep && opt[:search]
|
|
699
|
-
search_match = opt[:search].nil? || opt[:search].empty?
|
|
733
|
+
search_match = if opt[:search].nil? || opt[:search].empty?
|
|
734
|
+
true
|
|
735
|
+
else
|
|
736
|
+
item.search(opt[:search], case_type: opt[:case].normalize_case, fuzzy: opt[:fuzzy])
|
|
737
|
+
end
|
|
738
|
+
|
|
700
739
|
keep = false unless search_match
|
|
740
|
+
keep = opt[:not] ? !keep : keep
|
|
701
741
|
end
|
|
702
742
|
|
|
703
743
|
if keep && opt[:date_filter]&.length == 2
|
|
@@ -710,30 +750,36 @@ module Doing
|
|
|
710
750
|
item.date.strftime('%F') == start_date.strftime('%F')
|
|
711
751
|
end
|
|
712
752
|
keep = false unless in_date_range
|
|
753
|
+
keep = opt[:not] ? !keep : keep
|
|
713
754
|
end
|
|
714
755
|
|
|
715
756
|
keep = false if keep && opt[:only_timed] && !item.interval
|
|
716
757
|
|
|
717
758
|
if keep && opt[:tag_filter] && !opt[:tag_filter]['tags'].empty?
|
|
718
759
|
keep = item.tags?(opt[:tag_filter]['tags'], opt[:tag_filter]['bool'])
|
|
760
|
+
keep = opt[:not] ? !keep : keep
|
|
719
761
|
end
|
|
720
762
|
|
|
721
763
|
if keep && opt[:before]
|
|
722
764
|
time_string = opt[:before]
|
|
723
765
|
cutoff = chronify(time_string, guess: :begin)
|
|
724
766
|
keep = cutoff && item.date <= cutoff
|
|
767
|
+
keep = opt[:not] ? !keep : keep
|
|
725
768
|
end
|
|
726
769
|
|
|
727
770
|
if keep && opt[:after]
|
|
728
771
|
time_string = opt[:after]
|
|
729
772
|
cutoff = chronify(time_string, guess: :end)
|
|
730
773
|
keep = cutoff && item.date >= cutoff
|
|
774
|
+
keep = opt[:not] ? !keep : keep
|
|
731
775
|
end
|
|
732
776
|
|
|
733
777
|
if keep && opt[:today]
|
|
734
778
|
keep = item.date >= Date.today.to_time && item.date < Date.today.next_day.to_time
|
|
779
|
+
keep = opt[:not] ? !keep : keep
|
|
735
780
|
elsif keep && opt[:yesterday]
|
|
736
781
|
keep = item.date >= Date.today.prev_day.to_time && item.date < Date.today.to_time
|
|
782
|
+
keep = opt[:not] ? !keep : keep
|
|
737
783
|
end
|
|
738
784
|
|
|
739
785
|
keep
|
|
@@ -749,28 +795,58 @@ module Doing
|
|
|
749
795
|
end
|
|
750
796
|
|
|
751
797
|
##
|
|
752
|
-
##
|
|
798
|
+
## Display an interactive menu of entries
|
|
799
|
+
##
|
|
800
|
+
## @param opt [Hash] Additional options
|
|
753
801
|
##
|
|
754
|
-
##
|
|
802
|
+
## Options hash is shared with #filter_items and #act_on
|
|
755
803
|
##
|
|
756
804
|
def interactive(opt = {})
|
|
757
805
|
section = opt[:section] ? guess_section(opt[:section]) : 'All'
|
|
806
|
+
|
|
807
|
+
search = nil
|
|
808
|
+
|
|
809
|
+
if opt[:search]
|
|
810
|
+
search = opt[:search]
|
|
811
|
+
search.sub!(/^'?/, "'") if opt[:exact]
|
|
812
|
+
opt[:search] = search
|
|
813
|
+
end
|
|
814
|
+
|
|
758
815
|
opt[:query] = opt[:search] if opt[:search] && !opt[:query]
|
|
816
|
+
opt[:query] = "!#{opt[:query]}" if opt[:not]
|
|
759
817
|
opt[:multiple] = true
|
|
760
|
-
|
|
818
|
+
opt[:show_if_single] = true
|
|
819
|
+
items = filter_items([], opt: { section: section, search: opt[:search], fuzzy: opt[:fuzzy], case: opt[:case], not: opt[:not] })
|
|
761
820
|
|
|
762
821
|
selection = choose_from_items(items, opt, include_section: section =~ /^all$/i)
|
|
763
822
|
|
|
764
|
-
raise NoResults, 'no items selected' if selection.empty?
|
|
823
|
+
raise NoResults, 'no items selected' if selection.nil? || selection.empty?
|
|
765
824
|
|
|
766
825
|
act_on(selection, opt)
|
|
767
826
|
end
|
|
768
827
|
|
|
828
|
+
##
|
|
829
|
+
## Create an interactive menu to select from a set of Items
|
|
830
|
+
##
|
|
831
|
+
## @param items [Array] list of items
|
|
832
|
+
## @param opt [Hash] options
|
|
833
|
+
## @param include_section [Boolean] include section
|
|
834
|
+
##
|
|
835
|
+
## @option opt [String] :header
|
|
836
|
+
## @option opt [String] :prompt
|
|
837
|
+
## @option opt [String] :query
|
|
838
|
+
## @option opt [Boolean] :show_if_single
|
|
839
|
+
## @option opt [Boolean] :menu
|
|
840
|
+
## @option opt [Boolean] :sort
|
|
841
|
+
## @option opt [Boolean] :multiple
|
|
842
|
+
## @option opt [Symbol] :case (:sensitive, :ignore, :smart)
|
|
843
|
+
##
|
|
769
844
|
def choose_from_items(items, opt = {}, include_section: false)
|
|
770
|
-
return
|
|
845
|
+
return items unless $stdout.isatty
|
|
771
846
|
|
|
772
847
|
return nil unless items.count.positive?
|
|
773
848
|
|
|
849
|
+
opt[:case] ||= :smart
|
|
774
850
|
opt[:header] ||= "Arrows: navigate, tab: mark for selection, ctrl-a: select all, enter: commit"
|
|
775
851
|
opt[:prompt] ||= "Select entries to act on > "
|
|
776
852
|
|
|
@@ -801,9 +877,18 @@ module Doing
|
|
|
801
877
|
opt[:multiple] ? '--multi' : '--no-multi',
|
|
802
878
|
'-0',
|
|
803
879
|
'--bind ctrl-a:select-all',
|
|
804
|
-
%(-q "#{opt[:query]}")
|
|
880
|
+
%(-q "#{opt[:query]}"),
|
|
881
|
+
'--info=inline'
|
|
805
882
|
]
|
|
806
883
|
fzf_args.push('-1') unless opt[:show_if_single]
|
|
884
|
+
fzf_args << case opt[:case].normalize_case
|
|
885
|
+
when :sensitive
|
|
886
|
+
'+i'
|
|
887
|
+
when :ignore
|
|
888
|
+
'-i'
|
|
889
|
+
end
|
|
890
|
+
fzf_args << '-e' if opt[:exact]
|
|
891
|
+
|
|
807
892
|
|
|
808
893
|
unless opt[:menu]
|
|
809
894
|
raise InvalidArgument, "Can't skip menu when no query is provided" unless opt[:query] && !opt[:query].empty?
|
|
@@ -821,6 +906,27 @@ module Doing
|
|
|
821
906
|
opt[:multiple] ? selected : selected[0]
|
|
822
907
|
end
|
|
823
908
|
|
|
909
|
+
##
|
|
910
|
+
## Perform actions on a set of entries. If
|
|
911
|
+
## no valid action is included in the opt
|
|
912
|
+
## hash and the terminal is a TTY, a menu
|
|
913
|
+
## will be presented
|
|
914
|
+
##
|
|
915
|
+
## @param items [Array] Array of Items to affect
|
|
916
|
+
## @param opt [Hash] Options and actions to perform
|
|
917
|
+
##
|
|
918
|
+
## @option opt [Boolean] :editor
|
|
919
|
+
## @option opt [Boolean] :delete
|
|
920
|
+
## @option opt [String] :tag
|
|
921
|
+
## @option opt [Boolean] :flag
|
|
922
|
+
## @option opt [Boolean] :finish
|
|
923
|
+
## @option opt [Boolean] :cancel
|
|
924
|
+
## @option opt [Boolean] :archive
|
|
925
|
+
## @option opt [String] :output
|
|
926
|
+
## @option opt [String] :save_to
|
|
927
|
+
## @option opt [Boolean] :again
|
|
928
|
+
## @option opt [Boolean] :resume
|
|
929
|
+
##
|
|
824
930
|
def act_on(items, opt = {})
|
|
825
931
|
actions = %i[editor delete tag flag finish cancel archive output save_to again resume]
|
|
826
932
|
has_action = false
|
|
@@ -835,17 +941,17 @@ module Doing
|
|
|
835
941
|
|
|
836
942
|
unless has_action
|
|
837
943
|
actions = [
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
944
|
+
'add tag',
|
|
945
|
+
'remove tag',
|
|
946
|
+
'cancel',
|
|
947
|
+
'delete',
|
|
948
|
+
'finish',
|
|
949
|
+
'flag',
|
|
950
|
+
'archive',
|
|
951
|
+
'move',
|
|
952
|
+
'edit',
|
|
953
|
+
'output formatted'
|
|
954
|
+
]
|
|
849
955
|
|
|
850
956
|
actions.concat(['resume/repeat', 'begin/reset']) if items.count == 1
|
|
851
957
|
|
|
@@ -853,7 +959,7 @@ module Doing
|
|
|
853
959
|
prompt: 'What do you want to do with the selected items? > ',
|
|
854
960
|
multiple: true,
|
|
855
961
|
sorted: false,
|
|
856
|
-
fzf_args: [
|
|
962
|
+
fzf_args: ["--height=#{actions.count + 3}", '--tac', '--no-sort', '--info=hidden'])
|
|
857
963
|
return unless choice
|
|
858
964
|
|
|
859
965
|
to_do = choice.strip.split(/\n/)
|
|
@@ -874,14 +980,13 @@ module Doing
|
|
|
874
980
|
opt[:tag] = tag.strip.sub(/^@/, '')
|
|
875
981
|
opt[:remove] = true if type == 'remove'
|
|
876
982
|
when /output formatted/
|
|
877
|
-
|
|
983
|
+
plugins = Plugins.available_plugins(type: :export).sort
|
|
984
|
+
output_format = choose_from(plugins,
|
|
878
985
|
prompt: 'Which output format? > ',
|
|
879
|
-
fzf_args: [
|
|
986
|
+
fzf_args: ["--height=#{plugins.count + 3}", '--tac', '--no-sort', '--info=hidden'])
|
|
880
987
|
next if tag =~ /^ *$/
|
|
881
988
|
|
|
882
|
-
unless output_format
|
|
883
|
-
raise UserCancelled, 'Cancelled'
|
|
884
|
-
end
|
|
989
|
+
raise UserCancelled unless output_format
|
|
885
990
|
|
|
886
991
|
opt[:output] = output_format.strip
|
|
887
992
|
res = opt[:force] ? false : yn('Save to file?', default_response: 'n')
|
|
@@ -934,7 +1039,7 @@ module Doing
|
|
|
934
1039
|
if opt[:delete]
|
|
935
1040
|
res = opt[:force] ? true : yn("Delete #{items.size} items?", default_response: 'y')
|
|
936
1041
|
if res
|
|
937
|
-
items.each { |item| delete_item(item) }
|
|
1042
|
+
items.each { |item| delete_item(item, single: items.count == 1) }
|
|
938
1043
|
write(@doing_file)
|
|
939
1044
|
end
|
|
940
1045
|
return
|
|
@@ -992,7 +1097,7 @@ module Doing
|
|
|
992
1097
|
title = input_lines[0]&.strip
|
|
993
1098
|
|
|
994
1099
|
if title.nil? || title =~ /^#{divider.strip}$/ || title.strip.empty?
|
|
995
|
-
delete_item(items[i])
|
|
1100
|
+
delete_item(items[i], single: new_items.count == 1)
|
|
996
1101
|
else
|
|
997
1102
|
note = input_lines.length > 1 ? input_lines[1..-1] : []
|
|
998
1103
|
|
|
@@ -1051,12 +1156,15 @@ module Doing
|
|
|
1051
1156
|
end
|
|
1052
1157
|
|
|
1053
1158
|
##
|
|
1054
|
-
##
|
|
1159
|
+
## Tag an item from the index
|
|
1055
1160
|
##
|
|
1056
|
-
## @param item
|
|
1057
|
-
## @param tags
|
|
1058
|
-
## @param remove
|
|
1059
|
-
## @param date
|
|
1161
|
+
## @param item [Item] The item to tag
|
|
1162
|
+
## @param tags [String] The tag to apply
|
|
1163
|
+
## @param remove [Boolean] remove tags?
|
|
1164
|
+
## @param date [Boolean] Include timestamp?
|
|
1165
|
+
## @param single [Boolean] Log as a single change?
|
|
1166
|
+
##
|
|
1167
|
+
## @return [Item] updated item
|
|
1060
1168
|
##
|
|
1061
1169
|
def tag_item(item, tags, remove: false, date: false, single: false)
|
|
1062
1170
|
added = []
|
|
@@ -1080,9 +1188,13 @@ module Doing
|
|
|
1080
1188
|
end
|
|
1081
1189
|
|
|
1082
1190
|
##
|
|
1083
|
-
##
|
|
1191
|
+
## Tag the last entry or X entries
|
|
1192
|
+
##
|
|
1193
|
+
## @param opt [Hash] Additional Options (see
|
|
1194
|
+
## #filter_items for filtering
|
|
1195
|
+
## options)
|
|
1084
1196
|
##
|
|
1085
|
-
## @
|
|
1197
|
+
## @see #filter_items
|
|
1086
1198
|
##
|
|
1087
1199
|
def tag_last(opt = {})
|
|
1088
1200
|
opt[:count] ||= 1
|
|
@@ -1202,13 +1314,13 @@ module Doing
|
|
|
1202
1314
|
end
|
|
1203
1315
|
|
|
1204
1316
|
##
|
|
1205
|
-
##
|
|
1317
|
+
## Move item from current section to
|
|
1206
1318
|
## destination section
|
|
1207
1319
|
##
|
|
1208
|
-
## @param item The item
|
|
1209
|
-
## @param section The destination section
|
|
1320
|
+
## @param item [Item] The item to move
|
|
1321
|
+
## @param section [String] The destination section
|
|
1210
1322
|
##
|
|
1211
|
-
## @return Updated item
|
|
1323
|
+
## @return [Item] Updated item
|
|
1212
1324
|
##
|
|
1213
1325
|
def move_item(item, section, label: true)
|
|
1214
1326
|
from = item.section
|
|
@@ -1225,9 +1337,13 @@ module Doing
|
|
|
1225
1337
|
end
|
|
1226
1338
|
|
|
1227
1339
|
##
|
|
1228
|
-
##
|
|
1340
|
+
## Get next item in the index
|
|
1341
|
+
##
|
|
1342
|
+
## @param item [Item] target item
|
|
1343
|
+
## @param options [Hash] additional options
|
|
1344
|
+
## @see #filter_items
|
|
1229
1345
|
##
|
|
1230
|
-
## @
|
|
1346
|
+
## @return [Item] the next chronological item in the index
|
|
1231
1347
|
##
|
|
1232
1348
|
def next_item(item, options = {})
|
|
1233
1349
|
items = filter_items([], opt: options)
|
|
@@ -1238,21 +1354,21 @@ module Doing
|
|
|
1238
1354
|
end
|
|
1239
1355
|
|
|
1240
1356
|
##
|
|
1241
|
-
##
|
|
1357
|
+
## Delete an item from the index
|
|
1242
1358
|
##
|
|
1243
1359
|
## @param item The item
|
|
1244
1360
|
##
|
|
1245
|
-
def delete_item(item)
|
|
1361
|
+
def delete_item(item, single: false)
|
|
1246
1362
|
section = item.section
|
|
1247
1363
|
|
|
1248
1364
|
section_items = @content[section][:items]
|
|
1249
1365
|
deleted = section_items.delete(item)
|
|
1250
1366
|
logger.count(:deleted)
|
|
1251
|
-
logger.info('Entry deleted:', deleted.title)
|
|
1367
|
+
logger.info('Entry deleted:', deleted.title) if single
|
|
1252
1368
|
end
|
|
1253
1369
|
|
|
1254
1370
|
##
|
|
1255
|
-
##
|
|
1371
|
+
## Update an item in the index with a modified item
|
|
1256
1372
|
##
|
|
1257
1373
|
## @param old_item The old item
|
|
1258
1374
|
## @param new_item The new item
|
|
@@ -1274,9 +1390,9 @@ module Doing
|
|
|
1274
1390
|
end
|
|
1275
1391
|
|
|
1276
1392
|
##
|
|
1277
|
-
##
|
|
1393
|
+
## Edit the last entry
|
|
1278
1394
|
##
|
|
1279
|
-
## @param section
|
|
1395
|
+
## @param section [String] The section, default "All"
|
|
1280
1396
|
##
|
|
1281
1397
|
def edit_last(section: 'All', options: {})
|
|
1282
1398
|
options[:section] = guess_section(section)
|
|
@@ -1307,14 +1423,14 @@ module Doing
|
|
|
1307
1423
|
end
|
|
1308
1424
|
|
|
1309
1425
|
##
|
|
1310
|
-
##
|
|
1311
|
-
##
|
|
1312
|
-
##
|
|
1313
|
-
##
|
|
1314
|
-
##
|
|
1426
|
+
## Accepts one tag and the raw text of a new item if the
|
|
1427
|
+
## passed tag is on any item, it's replaced with @done.
|
|
1428
|
+
## if new_item is not nil, it's tagged with the passed
|
|
1429
|
+
## tag and inserted. This is for use where only one
|
|
1430
|
+
## instance of a given tag should exist (@meanwhile)
|
|
1315
1431
|
##
|
|
1316
|
-
## @param
|
|
1317
|
-
## @param opt
|
|
1432
|
+
## @param target_tag [String] Tag to replace
|
|
1433
|
+
## @param opt [Hash] Additional Options
|
|
1318
1434
|
##
|
|
1319
1435
|
def stop_start(target_tag, opt = {})
|
|
1320
1436
|
tag = target_tag.dup
|
|
@@ -1362,13 +1478,13 @@ module Doing
|
|
|
1362
1478
|
end
|
|
1363
1479
|
|
|
1364
1480
|
##
|
|
1365
|
-
##
|
|
1481
|
+
## Write content to file or STDOUT
|
|
1366
1482
|
##
|
|
1367
|
-
## @param file
|
|
1483
|
+
## @param file [String] The filepath to write to
|
|
1368
1484
|
##
|
|
1369
1485
|
def write(file = nil, backup: true)
|
|
1370
1486
|
Hooks.trigger :pre_write, self, file
|
|
1371
|
-
output =
|
|
1487
|
+
output = combined_content
|
|
1372
1488
|
|
|
1373
1489
|
if file.nil?
|
|
1374
1490
|
$stdout.puts output
|
|
@@ -1378,21 +1494,10 @@ module Doing
|
|
|
1378
1494
|
end
|
|
1379
1495
|
end
|
|
1380
1496
|
|
|
1381
|
-
def wrapped_content
|
|
1382
|
-
output = @other_content_top ? "#{@other_content_top.join("\n")}\n" : ''
|
|
1383
|
-
|
|
1384
|
-
@content.each do |title, section|
|
|
1385
|
-
output += "#{section[:original]}\n"
|
|
1386
|
-
output += list_section({ section: title, template: "\t- %date | %title%t2note", highlight: false, wrap_width: 0 })
|
|
1387
|
-
end
|
|
1388
|
-
|
|
1389
|
-
output + @other_content_bottom.join("\n") unless @other_content_bottom.nil?
|
|
1390
|
-
end
|
|
1391
|
-
|
|
1392
1497
|
##
|
|
1393
|
-
##
|
|
1498
|
+
## Restore a backed up version of a file
|
|
1394
1499
|
##
|
|
1395
|
-
## @param file
|
|
1500
|
+
## @param file [String] The filepath to restore
|
|
1396
1501
|
##
|
|
1397
1502
|
def restore_backup(file)
|
|
1398
1503
|
if File.exist?("#{file}~")
|
|
@@ -1404,7 +1509,7 @@ module Doing
|
|
|
1404
1509
|
end
|
|
1405
1510
|
|
|
1406
1511
|
##
|
|
1407
|
-
##
|
|
1512
|
+
## Rename doing file with date and start fresh one
|
|
1408
1513
|
##
|
|
1409
1514
|
def rotate(opt = {})
|
|
1410
1515
|
keep = opt[:keep] || 0
|
|
@@ -1487,9 +1592,9 @@ module Doing
|
|
|
1487
1592
|
end
|
|
1488
1593
|
|
|
1489
1594
|
##
|
|
1490
|
-
##
|
|
1595
|
+
## Generate a menu of sections and allow user selection
|
|
1491
1596
|
##
|
|
1492
|
-
## @return
|
|
1597
|
+
## @return [String] The selected section name
|
|
1493
1598
|
##
|
|
1494
1599
|
def choose_section
|
|
1495
1600
|
choice = choose_from(sections.sort, prompt: 'Choose a section > ', fzf_args: ['--height=60%'])
|
|
@@ -1497,18 +1602,18 @@ module Doing
|
|
|
1497
1602
|
end
|
|
1498
1603
|
|
|
1499
1604
|
##
|
|
1500
|
-
##
|
|
1605
|
+
## List available views
|
|
1501
1606
|
##
|
|
1502
|
-
## @return
|
|
1607
|
+
## @return [Array] View names
|
|
1503
1608
|
##
|
|
1504
1609
|
def views
|
|
1505
1610
|
@config.has_key?('views') ? @config['views'].keys : []
|
|
1506
1611
|
end
|
|
1507
1612
|
|
|
1508
1613
|
##
|
|
1509
|
-
##
|
|
1614
|
+
## Generate a menu of views and allow user selection
|
|
1510
1615
|
##
|
|
1511
|
-
## @return
|
|
1616
|
+
## @return [String] The selected view name
|
|
1512
1617
|
##
|
|
1513
1618
|
def choose_view
|
|
1514
1619
|
choice = choose_from(views.sort, prompt: 'Choose a view > ', fzf_args: ['--height=60%'])
|
|
@@ -1516,9 +1621,9 @@ module Doing
|
|
|
1516
1621
|
end
|
|
1517
1622
|
|
|
1518
1623
|
##
|
|
1519
|
-
##
|
|
1624
|
+
## Gets a view from configuration
|
|
1520
1625
|
##
|
|
1521
|
-
## @param title
|
|
1626
|
+
## @param title [String] The title of the view to retrieve
|
|
1522
1627
|
##
|
|
1523
1628
|
def get_view(title)
|
|
1524
1629
|
return @config['views'][title] if @config['views'].has_key?(title)
|
|
@@ -1527,9 +1632,9 @@ module Doing
|
|
|
1527
1632
|
end
|
|
1528
1633
|
|
|
1529
1634
|
##
|
|
1530
|
-
##
|
|
1635
|
+
## Display contents of a section based on options
|
|
1531
1636
|
##
|
|
1532
|
-
## @param opt
|
|
1637
|
+
## @param opt [Hash] Additional Options
|
|
1533
1638
|
##
|
|
1534
1639
|
def list_section(opt = {})
|
|
1535
1640
|
opt[:count] ||= 0
|
|
@@ -1564,7 +1669,6 @@ module Doing
|
|
|
1564
1669
|
|
|
1565
1670
|
items.reverse! if opt[:order] =~ /^d/i
|
|
1566
1671
|
|
|
1567
|
-
|
|
1568
1672
|
if opt[:interactive]
|
|
1569
1673
|
opt[:menu] = !opt[:force]
|
|
1570
1674
|
opt[:query] = '' # opt[:search]
|
|
@@ -1585,44 +1689,12 @@ module Doing
|
|
|
1585
1689
|
output(items, title, is_single, opt)
|
|
1586
1690
|
end
|
|
1587
1691
|
|
|
1588
|
-
def output(items, title, is_single, opt = {})
|
|
1589
|
-
out = nil
|
|
1590
|
-
|
|
1591
|
-
raise InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
|
|
1592
|
-
|
|
1593
|
-
export_options = { page_title: title, is_single: is_single, options: opt }
|
|
1594
|
-
|
|
1595
|
-
Plugins.plugins[:export].each do |_, options|
|
|
1596
|
-
next unless opt[:output] =~ /^(#{options[:trigger].normalize_trigger})$/i
|
|
1597
|
-
|
|
1598
|
-
out = options[:class].render(self, items, variables: export_options)
|
|
1599
|
-
break
|
|
1600
|
-
end
|
|
1601
|
-
|
|
1602
|
-
out
|
|
1603
|
-
end
|
|
1604
|
-
|
|
1605
|
-
def load_plugins
|
|
1606
|
-
if @config.key?('plugins') && @config['plugins']['plugin_path']
|
|
1607
|
-
add_dir = @config['plugins']['plugin_path']
|
|
1608
|
-
else
|
|
1609
|
-
add_dir = File.join(@user_home, '.config', 'doing', 'plugins')
|
|
1610
|
-
begin
|
|
1611
|
-
FileUtils.mkdir_p(add_dir) if add_dir
|
|
1612
|
-
rescue
|
|
1613
|
-
nil
|
|
1614
|
-
end
|
|
1615
|
-
end
|
|
1616
|
-
|
|
1617
|
-
Plugins.load_plugins(add_dir)
|
|
1618
|
-
end
|
|
1619
|
-
|
|
1620
1692
|
##
|
|
1621
|
-
##
|
|
1693
|
+
## Move entries from a section to Archive or other specified
|
|
1622
1694
|
## section
|
|
1623
1695
|
##
|
|
1624
|
-
## @param section
|
|
1625
|
-
## @param options
|
|
1696
|
+
## @param section [String] The source section
|
|
1697
|
+
## @param options [Hash] Options
|
|
1626
1698
|
##
|
|
1627
1699
|
def archive(section = @config['current_section'], options = {})
|
|
1628
1700
|
count = options[:keep] || 0
|
|
@@ -1647,108 +1719,11 @@ module Doing
|
|
|
1647
1719
|
end
|
|
1648
1720
|
|
|
1649
1721
|
##
|
|
1650
|
-
##
|
|
1722
|
+
## Show all entries from the current day
|
|
1651
1723
|
##
|
|
1652
|
-
## @param
|
|
1653
|
-
## @param
|
|
1654
|
-
## @param opt
|
|
1655
|
-
##
|
|
1656
|
-
def do_archive(sect, destination, opt = {})
|
|
1657
|
-
count = opt[:count] || 0
|
|
1658
|
-
tags = opt[:tags] || []
|
|
1659
|
-
bool = opt[:bool] || :and
|
|
1660
|
-
label = opt[:label] || true
|
|
1661
|
-
|
|
1662
|
-
if sect =~ /^all$/i
|
|
1663
|
-
all_sections = sections.dup
|
|
1664
|
-
all_sections.delete(destination)
|
|
1665
|
-
else
|
|
1666
|
-
all_sections = [sect]
|
|
1667
|
-
end
|
|
1668
|
-
|
|
1669
|
-
counter = 0
|
|
1670
|
-
|
|
1671
|
-
all_sections.each do |section|
|
|
1672
|
-
items = @content[section][:items].dup
|
|
1673
|
-
|
|
1674
|
-
moved_items = []
|
|
1675
|
-
if !tags.empty? || opt[:search] || opt[:before]
|
|
1676
|
-
if opt[:before]
|
|
1677
|
-
time_string = opt[:before]
|
|
1678
|
-
cutoff = chronify(time_string, guess: :begin)
|
|
1679
|
-
end
|
|
1680
|
-
|
|
1681
|
-
items.delete_if do |item|
|
|
1682
|
-
if ((!tags.empty? && item.tags?(tags, bool)) || (opt[:search] && item.search(opt[:search].to_s)) || (opt[:before] && item.date < cutoff))
|
|
1683
|
-
moved_items.push(item)
|
|
1684
|
-
counter += 1
|
|
1685
|
-
true
|
|
1686
|
-
else
|
|
1687
|
-
false
|
|
1688
|
-
end
|
|
1689
|
-
end
|
|
1690
|
-
moved_items.each do |item|
|
|
1691
|
-
if label
|
|
1692
|
-
item.title = if section == @config['current_section']
|
|
1693
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
|
|
1694
|
-
else
|
|
1695
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
|
1696
|
-
end
|
|
1697
|
-
logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
|
|
1698
|
-
end
|
|
1699
|
-
end
|
|
1700
|
-
|
|
1701
|
-
@content[section][:items] = items
|
|
1702
|
-
@content[destination][:items].concat(moved_items)
|
|
1703
|
-
if moved_items.length.positive?
|
|
1704
|
-
logger.count(destination == 'Archive' ? :archived : :moved,
|
|
1705
|
-
level: :info,
|
|
1706
|
-
count: moved_items.length,
|
|
1707
|
-
message: "%count %items from #{section} to #{destination}")
|
|
1708
|
-
else
|
|
1709
|
-
logger.info('Skipped:', 'No items were moved')
|
|
1710
|
-
end
|
|
1711
|
-
else
|
|
1712
|
-
count = items.length if items.length < count
|
|
1713
|
-
|
|
1714
|
-
items.map! do |item|
|
|
1715
|
-
if label
|
|
1716
|
-
item.title = if section == @config['current_section']
|
|
1717
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
|
|
1718
|
-
else
|
|
1719
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
|
1720
|
-
end
|
|
1721
|
-
logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
|
|
1722
|
-
end
|
|
1723
|
-
item
|
|
1724
|
-
end
|
|
1725
|
-
|
|
1726
|
-
if items.count > count
|
|
1727
|
-
@content[destination][:items].concat(items[count..-1])
|
|
1728
|
-
else
|
|
1729
|
-
@content[destination][:items].concat(items)
|
|
1730
|
-
end
|
|
1731
|
-
|
|
1732
|
-
@content[section][:items] = if count.zero?
|
|
1733
|
-
[]
|
|
1734
|
-
else
|
|
1735
|
-
items[0..count - 1]
|
|
1736
|
-
end
|
|
1737
|
-
|
|
1738
|
-
logger.count(destination == 'Archive' ? :archived : :moved,
|
|
1739
|
-
level: :info,
|
|
1740
|
-
count: items.length - count,
|
|
1741
|
-
message: "%count %items from #{section} to #{destination}")
|
|
1742
|
-
end
|
|
1743
|
-
end
|
|
1744
|
-
end
|
|
1745
|
-
|
|
1746
|
-
##
|
|
1747
|
-
## @brief Show all entries from the current day
|
|
1748
|
-
##
|
|
1749
|
-
## @param times (Boolean) show times
|
|
1750
|
-
## @param output (String) output format
|
|
1751
|
-
## @param opt (Hash) Options
|
|
1724
|
+
## @param times [Boolean] show times
|
|
1725
|
+
## @param output [String] output format
|
|
1726
|
+
## @param opt [Hash] Options
|
|
1752
1727
|
##
|
|
1753
1728
|
def today(times = true, output = nil, opt = {})
|
|
1754
1729
|
opt[:totals] ||= false
|
|
@@ -1774,13 +1749,13 @@ module Doing
|
|
|
1774
1749
|
end
|
|
1775
1750
|
|
|
1776
1751
|
##
|
|
1777
|
-
##
|
|
1752
|
+
## Display entries within a date range
|
|
1778
1753
|
##
|
|
1779
|
-
## @param dates
|
|
1780
|
-
## @param section
|
|
1754
|
+
## @param dates [Array] [start, end]
|
|
1755
|
+
## @param section [String] The section
|
|
1781
1756
|
## @param times (Bool) Show times
|
|
1782
|
-
## @param output
|
|
1783
|
-
## @param opt
|
|
1757
|
+
## @param output [String] Output format
|
|
1758
|
+
## @param opt [Hash] Additional Options
|
|
1784
1759
|
##
|
|
1785
1760
|
def list_date(dates, section, times = nil, output = nil, opt = {})
|
|
1786
1761
|
opt[:totals] ||= false
|
|
@@ -1794,12 +1769,12 @@ module Doing
|
|
|
1794
1769
|
end
|
|
1795
1770
|
|
|
1796
1771
|
##
|
|
1797
|
-
##
|
|
1772
|
+
## Show entries from the previous day
|
|
1798
1773
|
##
|
|
1799
|
-
## @param section
|
|
1774
|
+
## @param section [String] The section
|
|
1800
1775
|
## @param times (Bool) Show times
|
|
1801
|
-
## @param output
|
|
1802
|
-
## @param opt
|
|
1776
|
+
## @param output [String] Output format
|
|
1777
|
+
## @param opt [Hash] Additional Options
|
|
1803
1778
|
##
|
|
1804
1779
|
def yesterday(section, times = nil, output = nil, opt = {})
|
|
1805
1780
|
opt[:totals] ||= false
|
|
@@ -1827,11 +1802,11 @@ module Doing
|
|
|
1827
1802
|
end
|
|
1828
1803
|
|
|
1829
1804
|
##
|
|
1830
|
-
##
|
|
1805
|
+
## Show recent entries
|
|
1831
1806
|
##
|
|
1832
|
-
## @param count
|
|
1833
|
-
## @param section
|
|
1834
|
-
## @param opt
|
|
1807
|
+
## @param count [Integer] The number to show
|
|
1808
|
+
## @param section [String] The section to show from, default Currently
|
|
1809
|
+
## @param opt [Hash] Additional Options
|
|
1835
1810
|
##
|
|
1836
1811
|
def recent(count = 10, section = nil, opt = {})
|
|
1837
1812
|
times = opt[:t] || true
|
|
@@ -1849,10 +1824,10 @@ module Doing
|
|
|
1849
1824
|
end
|
|
1850
1825
|
|
|
1851
1826
|
##
|
|
1852
|
-
##
|
|
1827
|
+
## Show the last entry
|
|
1853
1828
|
##
|
|
1854
1829
|
## @param times (Bool) Show times
|
|
1855
|
-
## @param section
|
|
1830
|
+
## @param section [String] Section to pull from, default Currently
|
|
1856
1831
|
##
|
|
1857
1832
|
def last(times: true, section: nil, options: {})
|
|
1858
1833
|
section = section.nil? || section =~ /all/i ? 'All' : guess_section(section)
|
|
@@ -1875,16 +1850,17 @@ module Doing
|
|
|
1875
1850
|
end
|
|
1876
1851
|
|
|
1877
1852
|
opts[:search] = options[:search] if options[:search]
|
|
1878
|
-
|
|
1853
|
+
opts[:case] = options[:case]
|
|
1854
|
+
opts[:not] = options[:negate]
|
|
1879
1855
|
list_section(opts)
|
|
1880
1856
|
end
|
|
1881
1857
|
|
|
1882
1858
|
##
|
|
1883
|
-
##
|
|
1859
|
+
## Uses 'autotag' configuration to turn keywords into tags for time tracking.
|
|
1884
1860
|
## Does not repeat tags in a title, and only converts the first instance of an
|
|
1885
1861
|
## untagged keyword
|
|
1886
1862
|
##
|
|
1887
|
-
## @param text
|
|
1863
|
+
## @param text [String] The text to tag
|
|
1888
1864
|
##
|
|
1889
1865
|
def autotag(text)
|
|
1890
1866
|
return unless text
|
|
@@ -1892,78 +1868,100 @@ module Doing
|
|
|
1892
1868
|
|
|
1893
1869
|
original = text.dup
|
|
1894
1870
|
|
|
1895
|
-
current_tags = text.scan(/@\w+/)
|
|
1896
|
-
|
|
1871
|
+
current_tags = text.scan(/@\w+/).map { |t| t.sub(/^@/, '') }
|
|
1872
|
+
tagged = {
|
|
1873
|
+
whitelisted: [],
|
|
1874
|
+
synonyms: [],
|
|
1875
|
+
transformed: [],
|
|
1876
|
+
replaced: []
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1897
1879
|
@config['autotag']['whitelist'].each do |tag|
|
|
1898
1880
|
next if text =~ /@#{tag}\b/i
|
|
1899
1881
|
|
|
1900
|
-
text.sub!(/(
|
|
1901
|
-
m.downcase!
|
|
1902
|
-
whitelisted.push(
|
|
1882
|
+
text.sub!(/(?<= |\A)(#{tag.strip})(?= |\Z)/i) do |m|
|
|
1883
|
+
m.downcase! unless tag =~ /[A-Z]/
|
|
1884
|
+
tagged[:whitelisted].push(m)
|
|
1903
1885
|
"@#{m}"
|
|
1904
1886
|
end
|
|
1905
1887
|
end
|
|
1906
|
-
|
|
1888
|
+
|
|
1907
1889
|
@config['autotag']['synonyms'].each do |tag, v|
|
|
1908
1890
|
v.each do |word|
|
|
1909
1891
|
next unless text =~ /\b#{word}\b/i
|
|
1910
1892
|
|
|
1911
|
-
|
|
1893
|
+
unless current_tags.include?(tag) || tagged[:whitelisted].include?(tag)
|
|
1894
|
+
tagged[:synonyms].push(tag)
|
|
1895
|
+
tagged[:synonyms] = tagged[:synonyms].uniq
|
|
1896
|
+
end
|
|
1912
1897
|
end
|
|
1913
1898
|
end
|
|
1899
|
+
|
|
1914
1900
|
if @config['autotag'].key? 'transform'
|
|
1915
1901
|
@config['autotag']['transform'].each do |tag|
|
|
1916
1902
|
next unless tag =~ /\S+:\S+/
|
|
1917
1903
|
|
|
1918
1904
|
rx, r = tag.split(/:/)
|
|
1905
|
+
flag_rx = %r{/([r]+)$}
|
|
1906
|
+
if r =~ flag_rx
|
|
1907
|
+
flags = r.match(flag_rx)[1].split(//)
|
|
1908
|
+
r.sub!(flag_rx, '')
|
|
1909
|
+
end
|
|
1919
1910
|
r.gsub!(/\$/, '\\')
|
|
1920
|
-
rx.sub!(
|
|
1921
|
-
regex = Regexp.new(
|
|
1922
|
-
|
|
1923
|
-
matches = text.scan(regex)
|
|
1924
|
-
next unless matches
|
|
1911
|
+
rx.sub!(/^@?/, '@')
|
|
1912
|
+
regex = Regexp.new("(?<= |\\A)#{rx}(?= |\\Z)")
|
|
1925
1913
|
|
|
1926
|
-
|
|
1914
|
+
text.sub!(regex) do
|
|
1915
|
+
m = Regexp.last_match
|
|
1927
1916
|
new_tag = r
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1917
|
+
|
|
1918
|
+
m.to_a.slice(1, m.length - 1).each_with_index do |v, idx|
|
|
1919
|
+
new_tag.gsub!("\\#{idx + 1}", v)
|
|
1920
|
+
end
|
|
1921
|
+
# Replace original tag if /r
|
|
1922
|
+
if flags&.include?('r')
|
|
1923
|
+
tagged[:replaced].concat(new_tag.split(/ /).map { |t| t.sub(/^@/, '') })
|
|
1924
|
+
new_tag.split(/ /).map { |t| t.sub(/^@?/, '@') }.join(' ')
|
|
1925
|
+
else
|
|
1926
|
+
tagged[:transformed].concat(new_tag.split(/ /).map { |t| t.sub(/^@/, '') })
|
|
1927
|
+
tagged[:transformed] = tagged[:transformed].uniq
|
|
1928
|
+
m[0]
|
|
1934
1929
|
end
|
|
1935
|
-
tail_tags.push(new_tag)
|
|
1936
1930
|
end
|
|
1937
1931
|
end
|
|
1938
1932
|
end
|
|
1939
1933
|
|
|
1940
|
-
logger.debug('Autotag:', "whitelisted tags: #{whitelisted.join(', ')}") unless whitelisted.empty?
|
|
1941
|
-
new_tags = whitelisted
|
|
1942
|
-
unless tail_tags.empty?
|
|
1943
|
-
tags = tail_tags.uniq.map { |t| "@#{t}".cyan }.join(' ')
|
|
1944
|
-
logger.debug('Autotag:', "synonym tags: #{tags}")
|
|
1945
|
-
tags_a = tail_tags.map { |t| "@#{t}" }
|
|
1946
|
-
text.add_tags!(tags_a.join(' '))
|
|
1947
|
-
new_tags.concat(tags_a)
|
|
1948
|
-
end
|
|
1949
1934
|
|
|
1950
|
-
|
|
1951
|
-
|
|
1935
|
+
logger.debug('Autotag:', "whitelisted tags: #{tagged[:whitelisted].log_tags}") unless tagged[:whitelisted].empty?
|
|
1936
|
+
logger.debug('Autotag:', "synonyms: #{tagged[:synonyms].log_tags}") unless tagged[:synonyms].empty?
|
|
1937
|
+
logger.debug('Autotag:', "transforms: #{tagged[:transformed].log_tags}") unless tagged[:transformed].empty?
|
|
1938
|
+
logger.debug('Autotag:', "transform replaced: #{tagged[:replaced].log_tags}") unless tagged[:replaced].empty?
|
|
1939
|
+
|
|
1940
|
+
tail_tags = tagged[:synonyms].concat(tagged[:transformed])
|
|
1941
|
+
tail_tags.sort!
|
|
1942
|
+
tail_tags.uniq!
|
|
1943
|
+
|
|
1944
|
+
text.add_tags!(tail_tags) unless tail_tags.empty?
|
|
1945
|
+
|
|
1946
|
+
if text == original
|
|
1947
|
+
logger.debug('Autotag:', "no change to \"#{text}\"")
|
|
1952
1948
|
else
|
|
1953
|
-
|
|
1949
|
+
new_tags = tagged[:whitelisted].concat(tail_tags).concat(tagged[:replaced])
|
|
1950
|
+
logger.debug('Autotag:', "added #{new_tags.log_tags} to \"#{text}\"")
|
|
1951
|
+
logger.count(:autotag, level: :info, count: 1, message: 'autotag updated %count %items')
|
|
1954
1952
|
end
|
|
1955
1953
|
|
|
1956
|
-
text
|
|
1954
|
+
text.dedup_tags
|
|
1957
1955
|
end
|
|
1958
1956
|
|
|
1959
1957
|
##
|
|
1960
|
-
##
|
|
1958
|
+
## Get total elapsed time for all tags in
|
|
1961
1959
|
## selection
|
|
1962
1960
|
##
|
|
1963
|
-
## @param format
|
|
1961
|
+
## @param format [String] return format (html,
|
|
1964
1962
|
## json, or text)
|
|
1965
|
-
## @param sort_by_name
|
|
1966
|
-
## @param sort_order
|
|
1963
|
+
## @param sort_by_name [Boolean] Sort by name if true, otherwise by time
|
|
1964
|
+
## @param sort_order [String] The sort order (asc or desc)
|
|
1967
1965
|
##
|
|
1968
1966
|
def tag_times(format: :text, sort_by_name: false, sort_order: 'asc')
|
|
1969
1967
|
return '' if @timers.empty?
|
|
@@ -2048,7 +2046,7 @@ EOS
|
|
|
2048
2046
|
(max - k.length).times do
|
|
2049
2047
|
spacer += ' '
|
|
2050
2048
|
end
|
|
2051
|
-
|
|
2049
|
+
_d, h, m = format_time(v, human: true)
|
|
2052
2050
|
output.push("┃ #{spacer}#{k}:#{format('%<h> 4dh %<m>02dm', h: h, m: m)} ┃")
|
|
2053
2051
|
end
|
|
2054
2052
|
|
|
@@ -2093,13 +2091,13 @@ EOS
|
|
|
2093
2091
|
end
|
|
2094
2092
|
|
|
2095
2093
|
##
|
|
2096
|
-
##
|
|
2094
|
+
## Gets the interval between entry's start
|
|
2097
2095
|
## date and @done date
|
|
2098
2096
|
##
|
|
2099
|
-
## @param item
|
|
2100
|
-
## @param formatted
|
|
2097
|
+
## @param item [Item] The entry
|
|
2098
|
+
## @param formatted [Boolean] Return human readable
|
|
2101
2099
|
## time (default seconds)
|
|
2102
|
-
## @param record
|
|
2100
|
+
## @param record [Boolean] Add the interval to the
|
|
2103
2101
|
## total for each tag
|
|
2104
2102
|
##
|
|
2105
2103
|
## @return Interval in seconds, or [d, h, m] array if
|
|
@@ -2119,28 +2117,9 @@ EOS
|
|
|
2119
2117
|
end
|
|
2120
2118
|
|
|
2121
2119
|
##
|
|
2122
|
-
##
|
|
2120
|
+
## Format human readable time from seconds
|
|
2123
2121
|
##
|
|
2124
|
-
## @param
|
|
2125
|
-
##
|
|
2126
|
-
def record_tag_times(item, seconds)
|
|
2127
|
-
item_hash = "#{item.date.strftime('%s')}#{item.title}#{item.section}"
|
|
2128
|
-
return if @recorded_items.include?(item_hash)
|
|
2129
|
-
item.title.scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
|
|
2130
|
-
k = m[0] == 'done' ? 'All' : m[0].downcase
|
|
2131
|
-
if @timers.key?(k)
|
|
2132
|
-
@timers[k] += seconds
|
|
2133
|
-
else
|
|
2134
|
-
@timers[k] = seconds
|
|
2135
|
-
end
|
|
2136
|
-
@recorded_items.push(item_hash)
|
|
2137
|
-
end
|
|
2138
|
-
end
|
|
2139
|
-
|
|
2140
|
-
##
|
|
2141
|
-
## @brief Format human readable time from seconds
|
|
2142
|
-
##
|
|
2143
|
-
## @param seconds The seconds
|
|
2122
|
+
## @param seconds [Integer] Seconds
|
|
2144
2123
|
##
|
|
2145
2124
|
def format_time(seconds, human: false)
|
|
2146
2125
|
return [0, 0, 0] if seconds.nil?
|
|
@@ -2166,6 +2145,169 @@ EOS
|
|
|
2166
2145
|
|
|
2167
2146
|
private
|
|
2168
2147
|
|
|
2148
|
+
##
|
|
2149
|
+
## Wraps doing file content with additional
|
|
2150
|
+
## header/footer content
|
|
2151
|
+
##
|
|
2152
|
+
## @return [String] concatenated content
|
|
2153
|
+
##
|
|
2154
|
+
def combined_content
|
|
2155
|
+
output = @other_content_top ? "#{@other_content_top.join("\n")}\n" : ''
|
|
2156
|
+
|
|
2157
|
+
@content.each do |title, section|
|
|
2158
|
+
output += "#{section[:original]}\n"
|
|
2159
|
+
output += list_section({ section: title, template: "\t- %date | %title%t2note", highlight: false, wrap_width: 0 })
|
|
2160
|
+
end
|
|
2161
|
+
|
|
2162
|
+
output + @other_content_bottom.join("\n") unless @other_content_bottom.nil?
|
|
2163
|
+
end
|
|
2164
|
+
|
|
2165
|
+
##
|
|
2166
|
+
## Generate output using available export plugins
|
|
2167
|
+
##
|
|
2168
|
+
## @param items [Array] The items
|
|
2169
|
+
## @param title [String] Page title
|
|
2170
|
+
## @param is_single [Boolean] Indicates if single
|
|
2171
|
+
## section
|
|
2172
|
+
## @param opt [Hash] Additional options
|
|
2173
|
+
##
|
|
2174
|
+
## @return [String] formatted output based on opt[:output]
|
|
2175
|
+
## template trigger
|
|
2176
|
+
##
|
|
2177
|
+
def output(items, title, is_single, opt = {})
|
|
2178
|
+
out = nil
|
|
2179
|
+
|
|
2180
|
+
raise InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
|
|
2181
|
+
|
|
2182
|
+
export_options = { page_title: title, is_single: is_single, options: opt }
|
|
2183
|
+
|
|
2184
|
+
Plugins.plugins[:export].each do |_, options|
|
|
2185
|
+
next unless opt[:output] =~ /^(#{options[:trigger].normalize_trigger})$/i
|
|
2186
|
+
|
|
2187
|
+
out = options[:class].render(self, items, variables: export_options)
|
|
2188
|
+
break
|
|
2189
|
+
end
|
|
2190
|
+
|
|
2191
|
+
out
|
|
2192
|
+
end
|
|
2193
|
+
|
|
2194
|
+
##
|
|
2195
|
+
## Record times for item tags
|
|
2196
|
+
##
|
|
2197
|
+
## @param item [Item] The item to record
|
|
2198
|
+
##
|
|
2199
|
+
def record_tag_times(item, seconds)
|
|
2200
|
+
item_hash = "#{item.date.strftime('%s')}#{item.title}#{item.section}"
|
|
2201
|
+
return if @recorded_items.include?(item_hash)
|
|
2202
|
+
item.title.scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
|
|
2203
|
+
k = m[0] == 'done' ? 'All' : m[0].downcase
|
|
2204
|
+
if @timers.key?(k)
|
|
2205
|
+
@timers[k] += seconds
|
|
2206
|
+
else
|
|
2207
|
+
@timers[k] = seconds
|
|
2208
|
+
end
|
|
2209
|
+
@recorded_items.push(item_hash)
|
|
2210
|
+
end
|
|
2211
|
+
end
|
|
2212
|
+
|
|
2213
|
+
##
|
|
2214
|
+
## Helper function, performs the actual archiving
|
|
2215
|
+
##
|
|
2216
|
+
## @param sect [String] The source section
|
|
2217
|
+
## @param destination [String] The destination
|
|
2218
|
+
## section
|
|
2219
|
+
## @param opt [Hash] Additional Options
|
|
2220
|
+
##
|
|
2221
|
+
def do_archive(sect, destination, opt = {})
|
|
2222
|
+
count = opt[:count] || 0
|
|
2223
|
+
tags = opt[:tags] || []
|
|
2224
|
+
bool = opt[:bool] || :and
|
|
2225
|
+
label = opt[:label] || true
|
|
2226
|
+
|
|
2227
|
+
if sect =~ /^all$/i
|
|
2228
|
+
all_sections = sections.dup
|
|
2229
|
+
all_sections.delete(destination)
|
|
2230
|
+
else
|
|
2231
|
+
all_sections = [sect]
|
|
2232
|
+
end
|
|
2233
|
+
|
|
2234
|
+
counter = 0
|
|
2235
|
+
|
|
2236
|
+
all_sections.each do |section|
|
|
2237
|
+
items = @content[section][:items].dup
|
|
2238
|
+
|
|
2239
|
+
moved_items = []
|
|
2240
|
+
if !tags.empty? || opt[:search] || opt[:before]
|
|
2241
|
+
if opt[:before]
|
|
2242
|
+
time_string = opt[:before]
|
|
2243
|
+
cutoff = chronify(time_string, guess: :begin)
|
|
2244
|
+
end
|
|
2245
|
+
|
|
2246
|
+
items.delete_if do |item|
|
|
2247
|
+
if ((!tags.empty? && item.tags?(tags, bool)) || (opt[:search] && item.search(opt[:search].to_s)) || (opt[:before] && item.date < cutoff))
|
|
2248
|
+
moved_items.push(item)
|
|
2249
|
+
counter += 1
|
|
2250
|
+
true
|
|
2251
|
+
else
|
|
2252
|
+
false
|
|
2253
|
+
end
|
|
2254
|
+
end
|
|
2255
|
+
moved_items.each do |item|
|
|
2256
|
+
if label
|
|
2257
|
+
item.title = if section == @config['current_section']
|
|
2258
|
+
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
|
|
2259
|
+
else
|
|
2260
|
+
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
|
2261
|
+
end
|
|
2262
|
+
logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
|
|
2263
|
+
end
|
|
2264
|
+
end
|
|
2265
|
+
|
|
2266
|
+
@content[section][:items] = items
|
|
2267
|
+
@content[destination][:items].concat(moved_items)
|
|
2268
|
+
if moved_items.length.positive?
|
|
2269
|
+
logger.count(destination == 'Archive' ? :archived : :moved,
|
|
2270
|
+
level: :info,
|
|
2271
|
+
count: moved_items.length,
|
|
2272
|
+
message: "%count %items from #{section} to #{destination}")
|
|
2273
|
+
else
|
|
2274
|
+
logger.info('Skipped:', 'No items were moved')
|
|
2275
|
+
end
|
|
2276
|
+
else
|
|
2277
|
+
count = items.length if items.length < count
|
|
2278
|
+
|
|
2279
|
+
items.map! do |item|
|
|
2280
|
+
if label
|
|
2281
|
+
item.title = if section == @config['current_section']
|
|
2282
|
+
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
|
|
2283
|
+
else
|
|
2284
|
+
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
|
2285
|
+
end
|
|
2286
|
+
logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
|
|
2287
|
+
end
|
|
2288
|
+
item
|
|
2289
|
+
end
|
|
2290
|
+
|
|
2291
|
+
if items.count > count
|
|
2292
|
+
@content[destination][:items].concat(items[count..-1])
|
|
2293
|
+
else
|
|
2294
|
+
@content[destination][:items].concat(items)
|
|
2295
|
+
end
|
|
2296
|
+
|
|
2297
|
+
@content[section][:items] = if count.zero?
|
|
2298
|
+
[]
|
|
2299
|
+
else
|
|
2300
|
+
items[0..count - 1]
|
|
2301
|
+
end
|
|
2302
|
+
|
|
2303
|
+
logger.count(destination == 'Archive' ? :archived : :moved,
|
|
2304
|
+
level: :info,
|
|
2305
|
+
count: items.length - count,
|
|
2306
|
+
message: "%count %items from #{section} to #{destination}")
|
|
2307
|
+
end
|
|
2308
|
+
end
|
|
2309
|
+
end
|
|
2310
|
+
|
|
2169
2311
|
def run_after
|
|
2170
2312
|
return unless @config.key?('run_after')
|
|
2171
2313
|
|