doing 2.0.9.pre → 2.0.10
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 +1 -1
- data/Gemfile.lock +30 -10
- data/README.md +1 -1
- data/Rakefile +8 -1
- data/bin/doing +76 -30
- 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 +86 -16
- data/example_plugin.rb +6 -6
- data/lib/doing/array.rb +1 -1
- data/lib/doing/configuration.rb +14 -12
- data/lib/doing/hash.rb +1 -1
- data/lib/doing/item.rb +101 -17
- data/lib/doing/log_adapter.rb +123 -113
- 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 +1 -1
- data/lib/doing/plugins/import/doing_import.rb +4 -4
- data/lib/doing/plugins/import/timing_import.rb +5 -3
- data/lib/doing/string.rb +75 -22
- 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 +419 -326
- data/lib/doing/wwidfile.rb +5 -5
- data/lib/doing.rb +2 -1
- data/lib/examples/plugins/say_export.rb +6 -6
- data/rdocfixer.rb +1 -1
- data/yard_templates/default/method_details/setup.rb +3 -0
- metadata +117 -4
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
|
|
@@ -480,10 +480,10 @@ module Doing
|
|
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,6 +713,7 @@ 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
719
|
if opt[:unfinished]
|
@@ -700,11 +730,10 @@ module Doing
|
|
700
730
|
end
|
701
731
|
|
702
732
|
if keep && opt[:search]
|
703
|
-
opt[:case] = opt[:case].normalize_case unless opt[:case].is_a?(Symbol)
|
704
733
|
search_match = if opt[:search].nil? || opt[:search].empty?
|
705
734
|
true
|
706
735
|
else
|
707
|
-
item.search(opt[:search], case_type: opt[:case])
|
736
|
+
item.search(opt[:search], case_type: opt[:case].normalize_case, fuzzy: opt[:fuzzy])
|
708
737
|
end
|
709
738
|
|
710
739
|
keep = false unless search_match
|
@@ -766,9 +795,11 @@ module Doing
|
|
766
795
|
end
|
767
796
|
|
768
797
|
##
|
769
|
-
##
|
798
|
+
## Display an interactive menu of entries
|
799
|
+
##
|
800
|
+
## @param opt [Hash] Additional options
|
770
801
|
##
|
771
|
-
##
|
802
|
+
## Options hash is shared with #filter_items and #act_on
|
772
803
|
##
|
773
804
|
def interactive(opt = {})
|
774
805
|
section = opt[:section] ? guess_section(opt[:section]) : 'All'
|
@@ -784,20 +815,38 @@ module Doing
|
|
784
815
|
opt[:query] = opt[:search] if opt[:search] && !opt[:query]
|
785
816
|
opt[:query] = "!#{opt[:query]}" if opt[:not]
|
786
817
|
opt[:multiple] = true
|
787
|
-
|
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] })
|
788
820
|
|
789
821
|
selection = choose_from_items(items, opt, include_section: section =~ /^all$/i)
|
790
822
|
|
791
|
-
raise NoResults, 'no items selected' if selection.empty?
|
823
|
+
raise NoResults, 'no items selected' if selection.nil? || selection.empty?
|
792
824
|
|
793
825
|
act_on(selection, opt)
|
794
826
|
end
|
795
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
|
+
##
|
796
844
|
def choose_from_items(items, opt = {}, include_section: false)
|
797
|
-
return
|
845
|
+
return items unless $stdout.isatty
|
798
846
|
|
799
847
|
return nil unless items.count.positive?
|
800
848
|
|
849
|
+
opt[:case] ||= :smart
|
801
850
|
opt[:header] ||= "Arrows: navigate, tab: mark for selection, ctrl-a: select all, enter: commit"
|
802
851
|
opt[:prompt] ||= "Select entries to act on > "
|
803
852
|
|
@@ -828,9 +877,18 @@ module Doing
|
|
828
877
|
opt[:multiple] ? '--multi' : '--no-multi',
|
829
878
|
'-0',
|
830
879
|
'--bind ctrl-a:select-all',
|
831
|
-
%(-q "#{opt[:query]}")
|
880
|
+
%(-q "#{opt[:query]}"),
|
881
|
+
'--info=inline'
|
832
882
|
]
|
833
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
|
+
|
834
892
|
|
835
893
|
unless opt[:menu]
|
836
894
|
raise InvalidArgument, "Can't skip menu when no query is provided" unless opt[:query] && !opt[:query].empty?
|
@@ -848,6 +906,27 @@ module Doing
|
|
848
906
|
opt[:multiple] ? selected : selected[0]
|
849
907
|
end
|
850
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
|
+
##
|
851
930
|
def act_on(items, opt = {})
|
852
931
|
actions = %i[editor delete tag flag finish cancel archive output save_to again resume]
|
853
932
|
has_action = false
|
@@ -862,17 +941,17 @@ module Doing
|
|
862
941
|
|
863
942
|
unless has_action
|
864
943
|
actions = [
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
944
|
+
'add tag',
|
945
|
+
'remove tag',
|
946
|
+
'cancel',
|
947
|
+
'delete',
|
948
|
+
'finish',
|
949
|
+
'flag',
|
950
|
+
'archive',
|
951
|
+
'move',
|
952
|
+
'edit',
|
953
|
+
'output formatted'
|
954
|
+
]
|
876
955
|
|
877
956
|
actions.concat(['resume/repeat', 'begin/reset']) if items.count == 1
|
878
957
|
|
@@ -880,7 +959,7 @@ module Doing
|
|
880
959
|
prompt: 'What do you want to do with the selected items? > ',
|
881
960
|
multiple: true,
|
882
961
|
sorted: false,
|
883
|
-
fzf_args: [
|
962
|
+
fzf_args: ["--height=#{actions.count + 3}", '--tac', '--no-sort', '--info=hidden'])
|
884
963
|
return unless choice
|
885
964
|
|
886
965
|
to_do = choice.strip.split(/\n/)
|
@@ -901,14 +980,13 @@ module Doing
|
|
901
980
|
opt[:tag] = tag.strip.sub(/^@/, '')
|
902
981
|
opt[:remove] = true if type == 'remove'
|
903
982
|
when /output formatted/
|
904
|
-
|
983
|
+
plugins = Plugins.available_plugins(type: :export).sort
|
984
|
+
output_format = choose_from(plugins,
|
905
985
|
prompt: 'Which output format? > ',
|
906
|
-
fzf_args: [
|
986
|
+
fzf_args: ["--height=#{plugins.count + 3}", '--tac', '--no-sort', '--info=hidden'])
|
907
987
|
next if tag =~ /^ *$/
|
908
988
|
|
909
|
-
unless output_format
|
910
|
-
raise UserCancelled, 'Cancelled'
|
911
|
-
end
|
989
|
+
raise UserCancelled unless output_format
|
912
990
|
|
913
991
|
opt[:output] = output_format.strip
|
914
992
|
res = opt[:force] ? false : yn('Save to file?', default_response: 'n')
|
@@ -1078,12 +1156,15 @@ module Doing
|
|
1078
1156
|
end
|
1079
1157
|
|
1080
1158
|
##
|
1081
|
-
##
|
1159
|
+
## Tag an item from the index
|
1082
1160
|
##
|
1083
|
-
## @param item
|
1084
|
-
## @param tags
|
1085
|
-
## @param remove
|
1086
|
-
## @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
|
1087
1168
|
##
|
1088
1169
|
def tag_item(item, tags, remove: false, date: false, single: false)
|
1089
1170
|
added = []
|
@@ -1107,9 +1188,13 @@ module Doing
|
|
1107
1188
|
end
|
1108
1189
|
|
1109
1190
|
##
|
1110
|
-
##
|
1191
|
+
## Tag the last entry or X entries
|
1192
|
+
##
|
1193
|
+
## @param opt [Hash] Additional Options (see
|
1194
|
+
## #filter_items for filtering
|
1195
|
+
## options)
|
1111
1196
|
##
|
1112
|
-
## @
|
1197
|
+
## @see #filter_items
|
1113
1198
|
##
|
1114
1199
|
def tag_last(opt = {})
|
1115
1200
|
opt[:count] ||= 1
|
@@ -1229,13 +1314,13 @@ module Doing
|
|
1229
1314
|
end
|
1230
1315
|
|
1231
1316
|
##
|
1232
|
-
##
|
1317
|
+
## Move item from current section to
|
1233
1318
|
## destination section
|
1234
1319
|
##
|
1235
|
-
## @param item The item
|
1236
|
-
## @param section The destination section
|
1320
|
+
## @param item [Item] The item to move
|
1321
|
+
## @param section [String] The destination section
|
1237
1322
|
##
|
1238
|
-
## @return Updated item
|
1323
|
+
## @return [Item] Updated item
|
1239
1324
|
##
|
1240
1325
|
def move_item(item, section, label: true)
|
1241
1326
|
from = item.section
|
@@ -1252,9 +1337,13 @@ module Doing
|
|
1252
1337
|
end
|
1253
1338
|
|
1254
1339
|
##
|
1255
|
-
##
|
1340
|
+
## Get next item in the index
|
1256
1341
|
##
|
1257
|
-
## @param item
|
1342
|
+
## @param item [Item] target item
|
1343
|
+
## @param options [Hash] additional options
|
1344
|
+
## @see #filter_items
|
1345
|
+
##
|
1346
|
+
## @return [Item] the next chronological item in the index
|
1258
1347
|
##
|
1259
1348
|
def next_item(item, options = {})
|
1260
1349
|
items = filter_items([], opt: options)
|
@@ -1265,7 +1354,7 @@ module Doing
|
|
1265
1354
|
end
|
1266
1355
|
|
1267
1356
|
##
|
1268
|
-
##
|
1357
|
+
## Delete an item from the index
|
1269
1358
|
##
|
1270
1359
|
## @param item The item
|
1271
1360
|
##
|
@@ -1279,7 +1368,7 @@ module Doing
|
|
1279
1368
|
end
|
1280
1369
|
|
1281
1370
|
##
|
1282
|
-
##
|
1371
|
+
## Update an item in the index with a modified item
|
1283
1372
|
##
|
1284
1373
|
## @param old_item The old item
|
1285
1374
|
## @param new_item The new item
|
@@ -1301,9 +1390,9 @@ module Doing
|
|
1301
1390
|
end
|
1302
1391
|
|
1303
1392
|
##
|
1304
|
-
##
|
1393
|
+
## Edit the last entry
|
1305
1394
|
##
|
1306
|
-
## @param section
|
1395
|
+
## @param section [String] The section, default "All"
|
1307
1396
|
##
|
1308
1397
|
def edit_last(section: 'All', options: {})
|
1309
1398
|
options[:section] = guess_section(section)
|
@@ -1334,14 +1423,14 @@ module Doing
|
|
1334
1423
|
end
|
1335
1424
|
|
1336
1425
|
##
|
1337
|
-
##
|
1338
|
-
##
|
1339
|
-
##
|
1340
|
-
##
|
1341
|
-
##
|
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)
|
1342
1431
|
##
|
1343
|
-
## @param
|
1344
|
-
## @param opt
|
1432
|
+
## @param target_tag [String] Tag to replace
|
1433
|
+
## @param opt [Hash] Additional Options
|
1345
1434
|
##
|
1346
1435
|
def stop_start(target_tag, opt = {})
|
1347
1436
|
tag = target_tag.dup
|
@@ -1389,13 +1478,13 @@ module Doing
|
|
1389
1478
|
end
|
1390
1479
|
|
1391
1480
|
##
|
1392
|
-
##
|
1481
|
+
## Write content to file or STDOUT
|
1393
1482
|
##
|
1394
|
-
## @param file
|
1483
|
+
## @param file [String] The filepath to write to
|
1395
1484
|
##
|
1396
1485
|
def write(file = nil, backup: true)
|
1397
1486
|
Hooks.trigger :pre_write, self, file
|
1398
|
-
output =
|
1487
|
+
output = combined_content
|
1399
1488
|
|
1400
1489
|
if file.nil?
|
1401
1490
|
$stdout.puts output
|
@@ -1405,21 +1494,10 @@ module Doing
|
|
1405
1494
|
end
|
1406
1495
|
end
|
1407
1496
|
|
1408
|
-
def wrapped_content
|
1409
|
-
output = @other_content_top ? "#{@other_content_top.join("\n")}\n" : ''
|
1410
|
-
|
1411
|
-
@content.each do |title, section|
|
1412
|
-
output += "#{section[:original]}\n"
|
1413
|
-
output += list_section({ section: title, template: "\t- %date | %title%t2note", highlight: false, wrap_width: 0 })
|
1414
|
-
end
|
1415
|
-
|
1416
|
-
output + @other_content_bottom.join("\n") unless @other_content_bottom.nil?
|
1417
|
-
end
|
1418
|
-
|
1419
1497
|
##
|
1420
|
-
##
|
1498
|
+
## Restore a backed up version of a file
|
1421
1499
|
##
|
1422
|
-
## @param file
|
1500
|
+
## @param file [String] The filepath to restore
|
1423
1501
|
##
|
1424
1502
|
def restore_backup(file)
|
1425
1503
|
if File.exist?("#{file}~")
|
@@ -1431,7 +1509,7 @@ module Doing
|
|
1431
1509
|
end
|
1432
1510
|
|
1433
1511
|
##
|
1434
|
-
##
|
1512
|
+
## Rename doing file with date and start fresh one
|
1435
1513
|
##
|
1436
1514
|
def rotate(opt = {})
|
1437
1515
|
keep = opt[:keep] || 0
|
@@ -1514,9 +1592,9 @@ module Doing
|
|
1514
1592
|
end
|
1515
1593
|
|
1516
1594
|
##
|
1517
|
-
##
|
1595
|
+
## Generate a menu of sections and allow user selection
|
1518
1596
|
##
|
1519
|
-
## @return
|
1597
|
+
## @return [String] The selected section name
|
1520
1598
|
##
|
1521
1599
|
def choose_section
|
1522
1600
|
choice = choose_from(sections.sort, prompt: 'Choose a section > ', fzf_args: ['--height=60%'])
|
@@ -1524,18 +1602,18 @@ module Doing
|
|
1524
1602
|
end
|
1525
1603
|
|
1526
1604
|
##
|
1527
|
-
##
|
1605
|
+
## List available views
|
1528
1606
|
##
|
1529
|
-
## @return
|
1607
|
+
## @return [Array] View names
|
1530
1608
|
##
|
1531
1609
|
def views
|
1532
1610
|
@config.has_key?('views') ? @config['views'].keys : []
|
1533
1611
|
end
|
1534
1612
|
|
1535
1613
|
##
|
1536
|
-
##
|
1614
|
+
## Generate a menu of views and allow user selection
|
1537
1615
|
##
|
1538
|
-
## @return
|
1616
|
+
## @return [String] The selected view name
|
1539
1617
|
##
|
1540
1618
|
def choose_view
|
1541
1619
|
choice = choose_from(views.sort, prompt: 'Choose a view > ', fzf_args: ['--height=60%'])
|
@@ -1543,9 +1621,9 @@ module Doing
|
|
1543
1621
|
end
|
1544
1622
|
|
1545
1623
|
##
|
1546
|
-
##
|
1624
|
+
## Gets a view from configuration
|
1547
1625
|
##
|
1548
|
-
## @param title
|
1626
|
+
## @param title [String] The title of the view to retrieve
|
1549
1627
|
##
|
1550
1628
|
def get_view(title)
|
1551
1629
|
return @config['views'][title] if @config['views'].has_key?(title)
|
@@ -1554,9 +1632,9 @@ module Doing
|
|
1554
1632
|
end
|
1555
1633
|
|
1556
1634
|
##
|
1557
|
-
##
|
1635
|
+
## Display contents of a section based on options
|
1558
1636
|
##
|
1559
|
-
## @param opt
|
1637
|
+
## @param opt [Hash] Additional Options
|
1560
1638
|
##
|
1561
1639
|
def list_section(opt = {})
|
1562
1640
|
opt[:count] ||= 0
|
@@ -1611,44 +1689,12 @@ module Doing
|
|
1611
1689
|
output(items, title, is_single, opt)
|
1612
1690
|
end
|
1613
1691
|
|
1614
|
-
def output(items, title, is_single, opt = {})
|
1615
|
-
out = nil
|
1616
|
-
|
1617
|
-
raise InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
|
1618
|
-
|
1619
|
-
export_options = { page_title: title, is_single: is_single, options: opt }
|
1620
|
-
|
1621
|
-
Plugins.plugins[:export].each do |_, options|
|
1622
|
-
next unless opt[:output] =~ /^(#{options[:trigger].normalize_trigger})$/i
|
1623
|
-
|
1624
|
-
out = options[:class].render(self, items, variables: export_options)
|
1625
|
-
break
|
1626
|
-
end
|
1627
|
-
|
1628
|
-
out
|
1629
|
-
end
|
1630
|
-
|
1631
|
-
def load_plugins
|
1632
|
-
if @config.key?('plugins') && @config['plugins']['plugin_path']
|
1633
|
-
add_dir = @config['plugins']['plugin_path']
|
1634
|
-
else
|
1635
|
-
add_dir = File.join(@user_home, '.config', 'doing', 'plugins')
|
1636
|
-
begin
|
1637
|
-
FileUtils.mkdir_p(add_dir) if add_dir
|
1638
|
-
rescue
|
1639
|
-
nil
|
1640
|
-
end
|
1641
|
-
end
|
1642
|
-
|
1643
|
-
Plugins.load_plugins(add_dir)
|
1644
|
-
end
|
1645
|
-
|
1646
1692
|
##
|
1647
|
-
##
|
1693
|
+
## Move entries from a section to Archive or other specified
|
1648
1694
|
## section
|
1649
1695
|
##
|
1650
|
-
## @param section
|
1651
|
-
## @param options
|
1696
|
+
## @param section [String] The source section
|
1697
|
+
## @param options [Hash] Options
|
1652
1698
|
##
|
1653
1699
|
def archive(section = @config['current_section'], options = {})
|
1654
1700
|
count = options[:keep] || 0
|
@@ -1673,108 +1719,11 @@ module Doing
|
|
1673
1719
|
end
|
1674
1720
|
|
1675
1721
|
##
|
1676
|
-
##
|
1677
|
-
##
|
1678
|
-
## @param section (String) The source section
|
1679
|
-
## @param destination (String) The destination section
|
1680
|
-
## @param opt (Hash) Additional Options
|
1681
|
-
##
|
1682
|
-
def do_archive(sect, destination, opt = {})
|
1683
|
-
count = opt[:count] || 0
|
1684
|
-
tags = opt[:tags] || []
|
1685
|
-
bool = opt[:bool] || :and
|
1686
|
-
label = opt[:label] || true
|
1687
|
-
|
1688
|
-
if sect =~ /^all$/i
|
1689
|
-
all_sections = sections.dup
|
1690
|
-
all_sections.delete(destination)
|
1691
|
-
else
|
1692
|
-
all_sections = [sect]
|
1693
|
-
end
|
1694
|
-
|
1695
|
-
counter = 0
|
1696
|
-
|
1697
|
-
all_sections.each do |section|
|
1698
|
-
items = @content[section][:items].dup
|
1699
|
-
|
1700
|
-
moved_items = []
|
1701
|
-
if !tags.empty? || opt[:search] || opt[:before]
|
1702
|
-
if opt[:before]
|
1703
|
-
time_string = opt[:before]
|
1704
|
-
cutoff = chronify(time_string, guess: :begin)
|
1705
|
-
end
|
1706
|
-
|
1707
|
-
items.delete_if do |item|
|
1708
|
-
if ((!tags.empty? && item.tags?(tags, bool)) || (opt[:search] && item.search(opt[:search].to_s)) || (opt[:before] && item.date < cutoff))
|
1709
|
-
moved_items.push(item)
|
1710
|
-
counter += 1
|
1711
|
-
true
|
1712
|
-
else
|
1713
|
-
false
|
1714
|
-
end
|
1715
|
-
end
|
1716
|
-
moved_items.each do |item|
|
1717
|
-
if label
|
1718
|
-
item.title = if section == @config['current_section']
|
1719
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
|
1720
|
-
else
|
1721
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
1722
|
-
end
|
1723
|
-
logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
|
1724
|
-
end
|
1725
|
-
end
|
1726
|
-
|
1727
|
-
@content[section][:items] = items
|
1728
|
-
@content[destination][:items].concat(moved_items)
|
1729
|
-
if moved_items.length.positive?
|
1730
|
-
logger.count(destination == 'Archive' ? :archived : :moved,
|
1731
|
-
level: :info,
|
1732
|
-
count: moved_items.length,
|
1733
|
-
message: "%count %items from #{section} to #{destination}")
|
1734
|
-
else
|
1735
|
-
logger.info('Skipped:', 'No items were moved')
|
1736
|
-
end
|
1737
|
-
else
|
1738
|
-
count = items.length if items.length < count
|
1739
|
-
|
1740
|
-
items.map! do |item|
|
1741
|
-
if label
|
1742
|
-
item.title = if section == @config['current_section']
|
1743
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
|
1744
|
-
else
|
1745
|
-
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
1746
|
-
end
|
1747
|
-
logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
|
1748
|
-
end
|
1749
|
-
item
|
1750
|
-
end
|
1751
|
-
|
1752
|
-
if items.count > count
|
1753
|
-
@content[destination][:items].concat(items[count..-1])
|
1754
|
-
else
|
1755
|
-
@content[destination][:items].concat(items)
|
1756
|
-
end
|
1757
|
-
|
1758
|
-
@content[section][:items] = if count.zero?
|
1759
|
-
[]
|
1760
|
-
else
|
1761
|
-
items[0..count - 1]
|
1762
|
-
end
|
1763
|
-
|
1764
|
-
logger.count(destination == 'Archive' ? :archived : :moved,
|
1765
|
-
level: :info,
|
1766
|
-
count: items.length - count,
|
1767
|
-
message: "%count %items from #{section} to #{destination}")
|
1768
|
-
end
|
1769
|
-
end
|
1770
|
-
end
|
1771
|
-
|
1772
|
-
##
|
1773
|
-
## @brief Show all entries from the current day
|
1722
|
+
## Show all entries from the current day
|
1774
1723
|
##
|
1775
|
-
## @param times
|
1776
|
-
## @param output
|
1777
|
-
## @param opt
|
1724
|
+
## @param times [Boolean] show times
|
1725
|
+
## @param output [String] output format
|
1726
|
+
## @param opt [Hash] Options
|
1778
1727
|
##
|
1779
1728
|
def today(times = true, output = nil, opt = {})
|
1780
1729
|
opt[:totals] ||= false
|
@@ -1800,13 +1749,13 @@ module Doing
|
|
1800
1749
|
end
|
1801
1750
|
|
1802
1751
|
##
|
1803
|
-
##
|
1752
|
+
## Display entries within a date range
|
1804
1753
|
##
|
1805
|
-
## @param dates
|
1806
|
-
## @param section
|
1754
|
+
## @param dates [Array] [start, end]
|
1755
|
+
## @param section [String] The section
|
1807
1756
|
## @param times (Bool) Show times
|
1808
|
-
## @param output
|
1809
|
-
## @param opt
|
1757
|
+
## @param output [String] Output format
|
1758
|
+
## @param opt [Hash] Additional Options
|
1810
1759
|
##
|
1811
1760
|
def list_date(dates, section, times = nil, output = nil, opt = {})
|
1812
1761
|
opt[:totals] ||= false
|
@@ -1820,12 +1769,12 @@ module Doing
|
|
1820
1769
|
end
|
1821
1770
|
|
1822
1771
|
##
|
1823
|
-
##
|
1772
|
+
## Show entries from the previous day
|
1824
1773
|
##
|
1825
|
-
## @param section
|
1774
|
+
## @param section [String] The section
|
1826
1775
|
## @param times (Bool) Show times
|
1827
|
-
## @param output
|
1828
|
-
## @param opt
|
1776
|
+
## @param output [String] Output format
|
1777
|
+
## @param opt [Hash] Additional Options
|
1829
1778
|
##
|
1830
1779
|
def yesterday(section, times = nil, output = nil, opt = {})
|
1831
1780
|
opt[:totals] ||= false
|
@@ -1853,11 +1802,11 @@ module Doing
|
|
1853
1802
|
end
|
1854
1803
|
|
1855
1804
|
##
|
1856
|
-
##
|
1805
|
+
## Show recent entries
|
1857
1806
|
##
|
1858
|
-
## @param count
|
1859
|
-
## @param section
|
1860
|
-
## @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
|
1861
1810
|
##
|
1862
1811
|
def recent(count = 10, section = nil, opt = {})
|
1863
1812
|
times = opt[:t] || true
|
@@ -1875,10 +1824,10 @@ module Doing
|
|
1875
1824
|
end
|
1876
1825
|
|
1877
1826
|
##
|
1878
|
-
##
|
1827
|
+
## Show the last entry
|
1879
1828
|
##
|
1880
1829
|
## @param times (Bool) Show times
|
1881
|
-
## @param section
|
1830
|
+
## @param section [String] Section to pull from, default Currently
|
1882
1831
|
##
|
1883
1832
|
def last(times: true, section: nil, options: {})
|
1884
1833
|
section = section.nil? || section =~ /all/i ? 'All' : guess_section(section)
|
@@ -1907,11 +1856,11 @@ module Doing
|
|
1907
1856
|
end
|
1908
1857
|
|
1909
1858
|
##
|
1910
|
-
##
|
1859
|
+
## Uses 'autotag' configuration to turn keywords into tags for time tracking.
|
1911
1860
|
## Does not repeat tags in a title, and only converts the first instance of an
|
1912
1861
|
## untagged keyword
|
1913
1862
|
##
|
1914
|
-
## @param text
|
1863
|
+
## @param text [String] The text to tag
|
1915
1864
|
##
|
1916
1865
|
def autotag(text)
|
1917
1866
|
return unless text
|
@@ -1984,13 +1933,13 @@ module Doing
|
|
1984
1933
|
end
|
1985
1934
|
|
1986
1935
|
##
|
1987
|
-
##
|
1936
|
+
## Get total elapsed time for all tags in
|
1988
1937
|
## selection
|
1989
1938
|
##
|
1990
|
-
## @param format
|
1939
|
+
## @param format [String] return format (html,
|
1991
1940
|
## json, or text)
|
1992
|
-
## @param sort_by_name
|
1993
|
-
## @param sort_order
|
1941
|
+
## @param sort_by_name [Boolean] Sort by name if true, otherwise by time
|
1942
|
+
## @param sort_order [String] The sort order (asc or desc)
|
1994
1943
|
##
|
1995
1944
|
def tag_times(format: :text, sort_by_name: false, sort_order: 'asc')
|
1996
1945
|
return '' if @timers.empty?
|
@@ -2120,13 +2069,13 @@ EOS
|
|
2120
2069
|
end
|
2121
2070
|
|
2122
2071
|
##
|
2123
|
-
##
|
2072
|
+
## Gets the interval between entry's start
|
2124
2073
|
## date and @done date
|
2125
2074
|
##
|
2126
|
-
## @param item
|
2127
|
-
## @param formatted
|
2075
|
+
## @param item [Item] The entry
|
2076
|
+
## @param formatted [Boolean] Return human readable
|
2128
2077
|
## time (default seconds)
|
2129
|
-
## @param record
|
2078
|
+
## @param record [Boolean] Add the interval to the
|
2130
2079
|
## total for each tag
|
2131
2080
|
##
|
2132
2081
|
## @return Interval in seconds, or [d, h, m] array if
|
@@ -2146,28 +2095,9 @@ EOS
|
|
2146
2095
|
end
|
2147
2096
|
|
2148
2097
|
##
|
2149
|
-
##
|
2150
|
-
##
|
2151
|
-
## @param item The item
|
2152
|
-
##
|
2153
|
-
def record_tag_times(item, seconds)
|
2154
|
-
item_hash = "#{item.date.strftime('%s')}#{item.title}#{item.section}"
|
2155
|
-
return if @recorded_items.include?(item_hash)
|
2156
|
-
item.title.scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
|
2157
|
-
k = m[0] == 'done' ? 'All' : m[0].downcase
|
2158
|
-
if @timers.key?(k)
|
2159
|
-
@timers[k] += seconds
|
2160
|
-
else
|
2161
|
-
@timers[k] = seconds
|
2162
|
-
end
|
2163
|
-
@recorded_items.push(item_hash)
|
2164
|
-
end
|
2165
|
-
end
|
2166
|
-
|
2167
|
-
##
|
2168
|
-
## @brief Format human readable time from seconds
|
2098
|
+
## Format human readable time from seconds
|
2169
2099
|
##
|
2170
|
-
## @param seconds
|
2100
|
+
## @param seconds [Integer] Seconds
|
2171
2101
|
##
|
2172
2102
|
def format_time(seconds, human: false)
|
2173
2103
|
return [0, 0, 0] if seconds.nil?
|
@@ -2193,6 +2123,169 @@ EOS
|
|
2193
2123
|
|
2194
2124
|
private
|
2195
2125
|
|
2126
|
+
##
|
2127
|
+
## Wraps doing file content with additional
|
2128
|
+
## header/footer content
|
2129
|
+
##
|
2130
|
+
## @return [String] concatenated content
|
2131
|
+
##
|
2132
|
+
def combined_content
|
2133
|
+
output = @other_content_top ? "#{@other_content_top.join("\n")}\n" : ''
|
2134
|
+
|
2135
|
+
@content.each do |title, section|
|
2136
|
+
output += "#{section[:original]}\n"
|
2137
|
+
output += list_section({ section: title, template: "\t- %date | %title%t2note", highlight: false, wrap_width: 0 })
|
2138
|
+
end
|
2139
|
+
|
2140
|
+
output + @other_content_bottom.join("\n") unless @other_content_bottom.nil?
|
2141
|
+
end
|
2142
|
+
|
2143
|
+
##
|
2144
|
+
## Generate output using available export plugins
|
2145
|
+
##
|
2146
|
+
## @param items [Array] The items
|
2147
|
+
## @param title [String] Page title
|
2148
|
+
## @param is_single [Boolean] Indicates if single
|
2149
|
+
## section
|
2150
|
+
## @param opt [Hash] Additional options
|
2151
|
+
##
|
2152
|
+
## @return [String] formatted output based on opt[:output]
|
2153
|
+
## template trigger
|
2154
|
+
##
|
2155
|
+
def output(items, title, is_single, opt = {})
|
2156
|
+
out = nil
|
2157
|
+
|
2158
|
+
raise InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
|
2159
|
+
|
2160
|
+
export_options = { page_title: title, is_single: is_single, options: opt }
|
2161
|
+
|
2162
|
+
Plugins.plugins[:export].each do |_, options|
|
2163
|
+
next unless opt[:output] =~ /^(#{options[:trigger].normalize_trigger})$/i
|
2164
|
+
|
2165
|
+
out = options[:class].render(self, items, variables: export_options)
|
2166
|
+
break
|
2167
|
+
end
|
2168
|
+
|
2169
|
+
out
|
2170
|
+
end
|
2171
|
+
|
2172
|
+
##
|
2173
|
+
## Record times for item tags
|
2174
|
+
##
|
2175
|
+
## @param item [Item] The item to record
|
2176
|
+
##
|
2177
|
+
def record_tag_times(item, seconds)
|
2178
|
+
item_hash = "#{item.date.strftime('%s')}#{item.title}#{item.section}"
|
2179
|
+
return if @recorded_items.include?(item_hash)
|
2180
|
+
item.title.scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
|
2181
|
+
k = m[0] == 'done' ? 'All' : m[0].downcase
|
2182
|
+
if @timers.key?(k)
|
2183
|
+
@timers[k] += seconds
|
2184
|
+
else
|
2185
|
+
@timers[k] = seconds
|
2186
|
+
end
|
2187
|
+
@recorded_items.push(item_hash)
|
2188
|
+
end
|
2189
|
+
end
|
2190
|
+
|
2191
|
+
##
|
2192
|
+
## Helper function, performs the actual archiving
|
2193
|
+
##
|
2194
|
+
## @param sect [String] The source section
|
2195
|
+
## @param destination [String] The destination
|
2196
|
+
## section
|
2197
|
+
## @param opt [Hash] Additional Options
|
2198
|
+
##
|
2199
|
+
def do_archive(sect, destination, opt = {})
|
2200
|
+
count = opt[:count] || 0
|
2201
|
+
tags = opt[:tags] || []
|
2202
|
+
bool = opt[:bool] || :and
|
2203
|
+
label = opt[:label] || true
|
2204
|
+
|
2205
|
+
if sect =~ /^all$/i
|
2206
|
+
all_sections = sections.dup
|
2207
|
+
all_sections.delete(destination)
|
2208
|
+
else
|
2209
|
+
all_sections = [sect]
|
2210
|
+
end
|
2211
|
+
|
2212
|
+
counter = 0
|
2213
|
+
|
2214
|
+
all_sections.each do |section|
|
2215
|
+
items = @content[section][:items].dup
|
2216
|
+
|
2217
|
+
moved_items = []
|
2218
|
+
if !tags.empty? || opt[:search] || opt[:before]
|
2219
|
+
if opt[:before]
|
2220
|
+
time_string = opt[:before]
|
2221
|
+
cutoff = chronify(time_string, guess: :begin)
|
2222
|
+
end
|
2223
|
+
|
2224
|
+
items.delete_if do |item|
|
2225
|
+
if ((!tags.empty? && item.tags?(tags, bool)) || (opt[:search] && item.search(opt[:search].to_s)) || (opt[:before] && item.date < cutoff))
|
2226
|
+
moved_items.push(item)
|
2227
|
+
counter += 1
|
2228
|
+
true
|
2229
|
+
else
|
2230
|
+
false
|
2231
|
+
end
|
2232
|
+
end
|
2233
|
+
moved_items.each do |item|
|
2234
|
+
if label
|
2235
|
+
item.title = if section == @config['current_section']
|
2236
|
+
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
|
2237
|
+
else
|
2238
|
+
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
2239
|
+
end
|
2240
|
+
logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
|
2241
|
+
end
|
2242
|
+
end
|
2243
|
+
|
2244
|
+
@content[section][:items] = items
|
2245
|
+
@content[destination][:items].concat(moved_items)
|
2246
|
+
if moved_items.length.positive?
|
2247
|
+
logger.count(destination == 'Archive' ? :archived : :moved,
|
2248
|
+
level: :info,
|
2249
|
+
count: moved_items.length,
|
2250
|
+
message: "%count %items from #{section} to #{destination}")
|
2251
|
+
else
|
2252
|
+
logger.info('Skipped:', 'No items were moved')
|
2253
|
+
end
|
2254
|
+
else
|
2255
|
+
count = items.length if items.length < count
|
2256
|
+
|
2257
|
+
items.map! do |item|
|
2258
|
+
if label
|
2259
|
+
item.title = if section == @config['current_section']
|
2260
|
+
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
|
2261
|
+
else
|
2262
|
+
item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
2263
|
+
end
|
2264
|
+
logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
|
2265
|
+
end
|
2266
|
+
item
|
2267
|
+
end
|
2268
|
+
|
2269
|
+
if items.count > count
|
2270
|
+
@content[destination][:items].concat(items[count..-1])
|
2271
|
+
else
|
2272
|
+
@content[destination][:items].concat(items)
|
2273
|
+
end
|
2274
|
+
|
2275
|
+
@content[section][:items] = if count.zero?
|
2276
|
+
[]
|
2277
|
+
else
|
2278
|
+
items[0..count - 1]
|
2279
|
+
end
|
2280
|
+
|
2281
|
+
logger.count(destination == 'Archive' ? :archived : :moved,
|
2282
|
+
level: :info,
|
2283
|
+
count: items.length - count,
|
2284
|
+
message: "%count %items from #{section} to #{destination}")
|
2285
|
+
end
|
2286
|
+
end
|
2287
|
+
end
|
2288
|
+
|
2196
2289
|
def run_after
|
2197
2290
|
return unless @config.key?('run_after')
|
2198
2291
|
|