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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +20 -0
  3. data/.yardoc/complete +0 -0
  4. data/.yardoc/object_types +0 -0
  5. data/.yardoc/objects/root.dat +0 -0
  6. data/.yardoc/proxy_types +0 -0
  7. data/.yardopts +1 -0
  8. data/CHANGELOG.md +1 -1
  9. data/Gemfile.lock +30 -10
  10. data/README.md +1 -1
  11. data/Rakefile +8 -1
  12. data/bin/doing +76 -30
  13. data/doc/Array.html +135 -0
  14. data/doc/Doing/Color.html +506 -0
  15. data/doc/Doing/Configuration.html +680 -0
  16. data/doc/Doing/Errors/DoingNoTraceError.html +186 -0
  17. data/doc/Doing/Errors/DoingRuntimeError.html +186 -0
  18. data/doc/Doing/Errors/DoingStandardError.html +186 -0
  19. data/doc/Doing/Errors/EmptyInput.html +186 -0
  20. data/doc/Doing/Errors/NoResults.html +186 -0
  21. data/doc/Doing/Errors/PluginException.html +248 -0
  22. data/doc/Doing/Errors/UserCancelled.html +186 -0
  23. data/doc/Doing/Errors/WrongCommand.html +186 -0
  24. data/doc/Doing/Errors.html +191 -0
  25. data/doc/Doing/Hooks.html +364 -0
  26. data/doc/Doing/Item.html +1385 -0
  27. data/doc/Doing/Items.html +393 -0
  28. data/doc/Doing/LogAdapter.html +1650 -0
  29. data/doc/Doing/Note.html +535 -0
  30. data/doc/Doing/Pager.html +268 -0
  31. data/doc/Doing/Plugins.html +849 -0
  32. data/doc/Doing/Util.html +870 -0
  33. data/doc/Doing/WWID.html +4827 -0
  34. data/doc/Doing.html +145 -0
  35. data/doc/GLI/Commands/MarkdownDocumentListener.html +763 -0
  36. data/doc/GLI/Commands.html +115 -0
  37. data/doc/GLI.html +115 -0
  38. data/doc/Hash.html +332 -0
  39. data/doc/Status.html +292 -0
  40. data/doc/String.html +1714 -0
  41. data/doc/Symbol.html +250 -0
  42. data/doc/Time.html +182 -0
  43. data/doc/_index.html +411 -0
  44. data/doc/class_list.html +51 -0
  45. data/doc/css/common.css +1 -0
  46. data/doc/css/full_list.css +58 -0
  47. data/doc/css/style.css +497 -0
  48. data/doc/file.README.html +123 -0
  49. data/doc/file_list.html +56 -0
  50. data/doc/frames.html +17 -0
  51. data/doc/index.html +123 -0
  52. data/doc/js/app.js +314 -0
  53. data/doc/js/full_list.js +216 -0
  54. data/doc/js/jquery.js +4 -0
  55. data/doc/method_list.html +1867 -0
  56. data/doc/top-level-namespace.html +112 -0
  57. data/doing.gemspec +5 -1
  58. data/doing.rdoc +86 -16
  59. data/example_plugin.rb +6 -6
  60. data/lib/doing/array.rb +1 -1
  61. data/lib/doing/configuration.rb +14 -12
  62. data/lib/doing/hash.rb +1 -1
  63. data/lib/doing/item.rb +101 -17
  64. data/lib/doing/log_adapter.rb +123 -113
  65. data/lib/doing/note.rb +1 -1
  66. data/lib/doing/plugin_manager.rb +5 -5
  67. data/lib/doing/plugins/export/csv_export.rb +1 -1
  68. data/lib/doing/plugins/export/template_export.rb +5 -7
  69. data/lib/doing/plugins/import/calendar_import.rb +1 -1
  70. data/lib/doing/plugins/import/doing_import.rb +4 -4
  71. data/lib/doing/plugins/import/timing_import.rb +5 -3
  72. data/lib/doing/string.rb +75 -22
  73. data/lib/doing/symbol.rb +9 -5
  74. data/lib/doing/time.rb +1 -1
  75. data/lib/doing/util.rb +18 -11
  76. data/lib/doing/version.rb +1 -1
  77. data/lib/doing/wwid.rb +419 -326
  78. data/lib/doing/wwidfile.rb +5 -5
  79. data/lib/doing.rb +2 -1
  80. data/lib/examples/plugins/say_export.rb +6 -6
  81. data/rdocfixer.rb +1 -1
  82. data/yard_templates/default/method_details/setup.rb +3 -0
  83. metadata +117 -4
data/lib/doing/wwid.rb CHANGED
@@ -9,7 +9,7 @@ require 'erb'
9
9
 
10
10
  module Doing
11
11
  ##
12
- ## @brief Main "What Was I Doing" methods
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
- ## @brief Initializes the object.
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
- ## @brief Logger
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
- ## @brief Initializes the doing file.
48
+ ## Initializes the doing file.
49
49
  ##
50
- ## @param path (String) Override path to a doing file, optional
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
- ## @brief Create a new doing file
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
- ## @brief Create a process for an editor and wait for the file handle to return
121
+ ## Create a process for an editor and wait for the file handle to return
122
122
  ##
123
- ## @param input (String) Text input for editor
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
- ## @brief Takes a multi-line string and formats it as an entry
167
+ ## Takes a multi-line string and formats it as an entry
168
168
  ##
169
- ## @return (Array) [(String)title, (Array)note]
169
+ ## @param input [String] The string to parse
170
170
  ##
171
- ## @param input (String) The string to parse
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
- ## @brief Converts input string into a Time object when input takes on the
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 (String) String to chronify
204
+ ## @param input [String] String to chronify
207
205
  ##
208
- ## @return (DateTime) result
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
- ## @brief Converts simple strings into seconds that can be added to a Time
230
+ ## Converts simple strings into seconds that can be added to a Time
233
231
  ## object
234
232
  ##
235
- ## @param qty (String) HH:MM or XX[dhm][[XXhm][XXm]] (1d2h30m, 45m,
233
+ ## @param qty [String] HH:MM or XX[dhm][[XXhm][XXm]] (1d2h30m, 45m,
236
234
  ## 1.5d, 1h20m, etc.)
237
235
  ##
238
- ## @return (Integer) seconds
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
- ## @brief List sections
263
+ ## List sections
266
264
  ##
267
- ## @return (Array) section titles
265
+ ## @return [Array] section titles
268
266
  ##
269
267
  def sections
270
268
  @content.keys
271
269
  end
272
270
 
273
271
  ##
274
- ## @brief Adds a section.
272
+ ## Adds a section.
275
273
  ##
276
- ## @param title (String) The new section 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
- ## @brief Attempt to match a string with an existing section
286
+ ## Attempt to match a string with an existing section
289
287
  ##
290
- ## @param frag (String) The user-provided string
291
- ## @param guessed (Boolean) already guessed and failed
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
- ## @brief Ask a yes or no question in the terminal
331
+ ## Ask a yes or no question in the terminal
334
332
  ##
335
- ## @param question (String) The question to ask
336
- ## @param default (Bool) default response if no input
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
- ## @brief Attempt to match a string with an existing view
385
+ ## Attempt to match a string with an existing view
386
386
  ##
387
- ## @param frag (String) The user-provided string
388
- ## @param guessed (Boolean) already 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
- ## @brief Adds an entry
413
+ ## Adds an entry
414
414
  ##
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}
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
- ## @brief Remove items from a list that already exist in @content
458
+ ## Remove items from a list that already exist in @content
459
459
  ##
460
- ## @param items (Array) The items to deduplicate
461
- ## @param no_overlap (Boolean) Remove items with overlapping time spans
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
- ## @brief Imports external entries
483
+ ## Imports external entries
484
484
  ##
485
- ## @param path (String) Path to JSON report file
486
- ## @param opt (Hash) Additional Options
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
- ## @brief Return the content of the last note for a given section
504
+ ## Return the content of the last note for a given section
505
505
  ##
506
- ## @param section (String) The section to retrieve from, default
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
- ## @brief Restart the last entry
566
+ ## Restart the last entry
567
567
  ##
568
- ## @param opt (Hash) Additional Options
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
- ## @brief Get the last entry
586
+ ## Get the last entry
587
587
  ##
588
- ## @param opt (Hash) Additional Options
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
- ## @brief Generate a menu of options and allow user selection
615
+ ## Generate a menu of options and allow user selection
616
616
  ##
617
- ## @return (String) The selected option
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
- ## @brief Filter items based on search criteria
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
- ## Available filter options in opt object
687
+ ## @param items [Array] The items to filter (if empty, filters all items)
688
+ ## @param opt [Hash] The filter parameters
660
689
  ##
661
- ## - +:section+ (String)
662
- ## - +:unfinished+ (Boolean)
663
- ## - +:tag+ (Array or comma-separated string)
664
- ## - +:tag_bool+ (:and, :or, :not)
665
- ## - +:search+ (string, optional regex with //)
666
- ## - +:date_filter+ (Array[(Time)start, (Time)end])
667
- ## - +:only_timed+ (Boolean)
668
- ## - +:before+ (Date/Time string, unparsed)
669
- ## - +:after+ (Date/Time string, unparsed)
670
- ## - +:today+ (Boolean)
671
- ## - +:yesterday+ (Boolean)
672
- ## - +:count+ (Number to return)
673
- ## - +:age+ (String, 'old' or 'new')
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
- ## @brief Display an interactive menu of entries
798
+ ## Display an interactive menu of entries
799
+ ##
800
+ ## @param opt [Hash] Additional options
770
801
  ##
771
- ## @param opt (Hash) Additional options
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
- items = filter_items([], opt: { section: section, search: opt[:search], case: opt[:case] })
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 nil unless $stdout.isatty
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
- 'add tag',
866
- 'remove tag',
867
- 'cancel',
868
- 'delete',
869
- 'finish',
870
- 'flag',
871
- 'archive',
872
- 'move',
873
- 'edit',
874
- 'output formatted'
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: ['--height=60%', '--tac', '--no-sort'])
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
- output_format = choose_from(Plugins.available_plugins(type: :export).sort,
983
+ plugins = Plugins.available_plugins(type: :export).sort
984
+ output_format = choose_from(plugins,
905
985
  prompt: 'Which output format? > ',
906
- fzf_args: ['--height=60%', '--tac', '--no-sort'])
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
- ## @brief Tag an item from the index
1159
+ ## Tag an item from the index
1082
1160
  ##
1083
- ## @param item (Item) The item to tag
1084
- ## @param tags (string) The tag to apply
1085
- ## @param remove (Boolean) remove tags
1086
- ## @param date (Boolean) Include timestamp?
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
- ## @brief Tag the last entry or X entries
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
- ## @param opt (Hash) Additional Options
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
- ## @brief Move item from current section to
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
- ## @brief Get next item in the index
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
- ## @brief Delete an item from the index
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
- ## @brief Update an item in the index with a modified item
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
- ## @brief Edit the last entry
1393
+ ## Edit the last entry
1305
1394
  ##
1306
- ## @param section (String) The section, default "All"
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
- ## @brief Accepts one tag and the raw text of a new item if the passed tag
1338
- ## is on any item, it's replaced with @done. if new_item is not
1339
- ## nil, it's tagged with the passed tag and inserted. This is for
1340
- ## use where only one instance of a given tag should exist
1341
- ## (@meanwhile)
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 tag (String) Tag to replace
1344
- ## @param opt (Hash) Additional Options
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
- ## @brief Write content to file or STDOUT
1481
+ ## Write content to file or STDOUT
1393
1482
  ##
1394
- ## @param file (String) The filepath to write to
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 = wrapped_content
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
- ## @brief Restore a backed up version of a file
1498
+ ## Restore a backed up version of a file
1421
1499
  ##
1422
- ## @param file (String) The filepath to restore
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
- ## @brief Rename doing file with date and start fresh one
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
- ## @brief Generate a menu of sections and allow user selection
1595
+ ## Generate a menu of sections and allow user selection
1518
1596
  ##
1519
- ## @return (String) The selected section name
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
- ## @brief List available views
1605
+ ## List available views
1528
1606
  ##
1529
- ## @return (Array) View names
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
- ## @brief Generate a menu of views and allow user selection
1614
+ ## Generate a menu of views and allow user selection
1537
1615
  ##
1538
- ## @return (String) The selected view name
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
- ## @brief Gets a view from configuration
1624
+ ## Gets a view from configuration
1547
1625
  ##
1548
- ## @param title (String) The title of the view to retrieve
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
- ## @brief Display contents of a section based on options
1635
+ ## Display contents of a section based on options
1558
1636
  ##
1559
- ## @param opt (Hash) Additional Options
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
- ## @brief Move entries from a section to Archive or other specified
1693
+ ## Move entries from a section to Archive or other specified
1648
1694
  ## section
1649
1695
  ##
1650
- ## @param section (String) The source section
1651
- ## @param options (Hash) 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
- ## @brief Helper function, performs the actual archiving
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 (Boolean) show times
1776
- ## @param output (String) output format
1777
- ## @param opt (Hash) Options
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
- ## @brief Display entries within a date range
1752
+ ## Display entries within a date range
1804
1753
  ##
1805
- ## @param dates (Array) [start, end]
1806
- ## @param section (String) The section
1754
+ ## @param dates [Array] [start, end]
1755
+ ## @param section [String] The section
1807
1756
  ## @param times (Bool) Show times
1808
- ## @param output (String) Output format
1809
- ## @param opt (Hash) Additional Options
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
- ## @brief Show entries from the previous day
1772
+ ## Show entries from the previous day
1824
1773
  ##
1825
- ## @param section (String) The section
1774
+ ## @param section [String] The section
1826
1775
  ## @param times (Bool) Show times
1827
- ## @param output (String) Output format
1828
- ## @param opt (Hash) Additional Options
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
- ## @brief Show recent entries
1805
+ ## Show recent entries
1857
1806
  ##
1858
- ## @param count (Integer) The number to show
1859
- ## @param section (String) The section to show from, default Currently
1860
- ## @param opt (Hash) Additional Options
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
- ## @brief Show the last entry
1827
+ ## Show the last entry
1879
1828
  ##
1880
1829
  ## @param times (Bool) Show times
1881
- ## @param section (String) Section to pull from, default Currently
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
- ## @brief Uses 'autotag' configuration to turn keywords into tags for time tracking.
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 (String) The text to tag
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
- ## @brief Get total elapsed time for all tags in
1936
+ ## Get total elapsed time for all tags in
1988
1937
  ## selection
1989
1938
  ##
1990
- ## @param format (String) return format (html,
1939
+ ## @param format [String] return format (html,
1991
1940
  ## json, or text)
1992
- ## @param sort_by_name (Boolean) Sort by name if true, otherwise by time
1993
- ## @param sort_order (String) The sort order (asc or desc)
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
- ## @brief Gets the interval between entry's start
2072
+ ## Gets the interval between entry's start
2124
2073
  ## date and @done date
2125
2074
  ##
2126
- ## @param item (Hash) The entry
2127
- ## @param formatted (Bool) Return human readable
2075
+ ## @param item [Item] The entry
2076
+ ## @param formatted [Boolean] Return human readable
2128
2077
  ## time (default seconds)
2129
- ## @param record (Bool) Add the interval to the
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
- ## @brief Record times for item tags
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 The 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