doing 2.0.9.pre → 2.0.15

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 (86) 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 +21 -1
  9. data/Gemfile.lock +20 -10
  10. data/README.md +1 -1
  11. data/Rakefile +10 -1
  12. data/bin/doing +106 -38
  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 +6 -1
  58. data/doing.rdoc +16 -16
  59. data/example_plugin.rb +6 -6
  60. data/lib/completion/_doing.zsh +24 -20
  61. data/lib/completion/doing.bash +41 -30
  62. data/lib/completion/doing.fish +50 -1
  63. data/lib/doing/array.rb +15 -2
  64. data/lib/doing/configuration.rb +14 -12
  65. data/lib/doing/hash.rb +1 -1
  66. data/lib/doing/item.rb +104 -19
  67. data/lib/doing/log_adapter.rb +132 -119
  68. data/lib/doing/note.rb +1 -1
  69. data/lib/doing/plugin_manager.rb +5 -5
  70. data/lib/doing/plugins/export/csv_export.rb +1 -1
  71. data/lib/doing/plugins/export/template_export.rb +5 -7
  72. data/lib/doing/plugins/import/calendar_import.rb +1 -1
  73. data/lib/doing/plugins/import/doing_import.rb +4 -4
  74. data/lib/doing/plugins/import/timing_import.rb +5 -3
  75. data/lib/doing/string.rb +77 -24
  76. data/lib/doing/symbol.rb +9 -5
  77. data/lib/doing/time.rb +1 -1
  78. data/lib/doing/util.rb +18 -11
  79. data/lib/doing/version.rb +1 -1
  80. data/lib/doing/wwid.rb +513 -372
  81. data/lib/doing/wwidfile.rb +5 -5
  82. data/lib/doing.rb +2 -1
  83. data/lib/examples/plugins/say_export.rb +6 -6
  84. data/rdocfixer.rb +1 -1
  85. data/yard_templates/default/method_details/setup.rb +3 -0
  86. metadata +111 -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
770
799
  ##
771
- ## @param opt (Hash) Additional options
800
+ ## @param opt [Hash] Additional options
801
+ ##
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
1160
+ ##
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?
1082
1166
  ##
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?
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,18 +1632,25 @@ 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 = {})
1640
+ opt[:config_template] ||= 'default'
1641
+ cfg = @config.dig('templates', opt[:config_template]).deep_merge({
1642
+ 'wrap_width' => @config['wrap_width'] || 0,
1643
+ 'date_format' => @config['default_date_format'],
1644
+ 'order' => @config['order'] || 'asc',
1645
+ 'tags_color' => @config['tags_color']
1646
+ })
1562
1647
  opt[:count] ||= 0
1563
1648
  opt[:age] ||= 'newest'
1564
- opt[:format] ||= @config.dig('templates', 'default', 'date_format')
1565
- opt[:order] ||= @config.dig('templates', 'default', 'order') || 'asc'
1649
+ opt[:format] ||= cfg['date_format']
1650
+ opt[:order] ||= cfg['order'] || 'asc'
1566
1651
  opt[:tag_order] ||= 'asc'
1567
- opt[:tags_color] ||= false
1568
- opt[:template] ||= @config.dig('templates', 'default', 'template')
1652
+ opt[:tags_color] ||= cfg['tags_color'] || false
1653
+ opt[:template] ||= cfg['template']
1569
1654
 
1570
1655
  # opt[:highlight] ||= true
1571
1656
  title = ''
@@ -1606,49 +1691,17 @@ module Doing
1606
1691
 
1607
1692
  opt[:output] ||= 'template'
1608
1693
 
1609
- opt[:wrap_width] ||= @config['templates']['default']['wrap_width']
1694
+ opt[:wrap_width] ||= @config['templates']['default']['wrap_width'] || 0
1610
1695
 
1611
1696
  output(items, title, is_single, opt)
1612
1697
  end
1613
1698
 
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
1699
  ##
1647
- ## @brief Move entries from a section to Archive or other specified
1700
+ ## Move entries from a section to Archive or other specified
1648
1701
  ## section
1649
1702
  ##
1650
- ## @param section (String) The source section
1651
- ## @param options (Hash) Options
1703
+ ## @param section [String] The source section
1704
+ ## @param options [Hash] Options
1652
1705
  ##
1653
1706
  def archive(section = @config['current_section'], options = {})
1654
1707
  count = options[:keep] || 0
@@ -1673,120 +1726,28 @@ module Doing
1673
1726
  end
1674
1727
 
1675
1728
  ##
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
1729
+ ## Show all entries from the current day
1774
1730
  ##
1775
- ## @param times (Boolean) show times
1776
- ## @param output (String) output format
1777
- ## @param opt (Hash) Options
1731
+ ## @param times [Boolean] show times
1732
+ ## @param output [String] output format
1733
+ ## @param opt [Hash] Options
1778
1734
  ##
1779
1735
  def today(times = true, output = nil, opt = {})
1780
1736
  opt[:totals] ||= false
1781
1737
  opt[:sort_tags] ||= false
1782
1738
 
1783
- cfg = @config['templates']['today']
1739
+ cfg = @config['templates']['today'].deep_merge(@config['templates']['default']).deep_merge({
1740
+ 'wrap_width' => @config['wrap_width'] || 0,
1741
+ 'date_format' => @config['default_date_format'],
1742
+ 'order' => @config['order'] || 'asc',
1743
+ 'tags_color' => @config['tags_color']
1744
+ })
1784
1745
  options = {
1785
1746
  after: opt[:after],
1786
1747
  before: opt[:before],
1787
1748
  count: 0,
1788
1749
  format: cfg['date_format'],
1789
- order: 'asc',
1750
+ order: cfg['order'] || 'asc',
1790
1751
  output: output,
1791
1752
  section: opt[:section],
1792
1753
  sort_tags: opt[:sort_tags],
@@ -1794,19 +1755,21 @@ module Doing
1794
1755
  times: times,
1795
1756
  today: true,
1796
1757
  totals: opt[:totals],
1797
- wrap_width: cfg['wrap_width']
1758
+ wrap_width: cfg['wrap_width'],
1759
+ tags_color: cfg['tags_color'],
1760
+ config_template: 'today'
1798
1761
  }
1799
1762
  list_section(options)
1800
1763
  end
1801
1764
 
1802
1765
  ##
1803
- ## @brief Display entries within a date range
1766
+ ## Display entries within a date range
1804
1767
  ##
1805
- ## @param dates (Array) [start, end]
1806
- ## @param section (String) The section
1768
+ ## @param dates [Array] [start, end]
1769
+ ## @param section [String] The section
1807
1770
  ## @param times (Bool) Show times
1808
- ## @param output (String) Output format
1809
- ## @param opt (Hash) Additional Options
1771
+ ## @param output [String] Output format
1772
+ ## @param opt [Hash] Additional Options
1810
1773
  ##
1811
1774
  def list_date(dates, section, times = nil, output = nil, opt = {})
1812
1775
  opt[:totals] ||= false
@@ -1816,16 +1779,16 @@ module Doing
1816
1779
  dates = [dates, dates] if dates.instance_of?(String)
1817
1780
 
1818
1781
  list_section({ section: section, count: 0, order: 'asc', date_filter: dates, times: times,
1819
- output: output, totals: opt[:totals], sort_tags: opt[:sort_tags] })
1782
+ output: output, totals: opt[:totals], sort_tags: opt[:sort_tags], config_template: 'default' })
1820
1783
  end
1821
1784
 
1822
1785
  ##
1823
- ## @brief Show entries from the previous day
1786
+ ## Show entries from the previous day
1824
1787
  ##
1825
- ## @param section (String) The section
1788
+ ## @param section [String] The section
1826
1789
  ## @param times (Bool) Show times
1827
- ## @param output (String) Output format
1828
- ## @param opt (Hash) Additional Options
1790
+ ## @param output [String] Output format
1791
+ ## @param opt [Hash] Additional Options
1829
1792
  ##
1830
1793
  def yesterday(section, times = nil, output = nil, opt = {})
1831
1794
  opt[:totals] ||= false
@@ -1846,43 +1809,54 @@ module Doing
1846
1809
  tag_order: opt[:tag_order],
1847
1810
  times: times,
1848
1811
  totals: opt[:totals],
1849
- yesterday: true
1812
+ yesterday: true,
1813
+ config_template: 'today'
1850
1814
  }
1851
1815
 
1852
1816
  list_section(options)
1853
1817
  end
1854
1818
 
1855
1819
  ##
1856
- ## @brief Show recent entries
1820
+ ## Show recent entries
1857
1821
  ##
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
1822
+ ## @param count [Integer] The number to show
1823
+ ## @param section [String] The section to show from, default Currently
1824
+ ## @param opt [Hash] Additional Options
1861
1825
  ##
1862
1826
  def recent(count = 10, section = nil, opt = {})
1863
1827
  times = opt[:t] || true
1864
1828
  opt[:totals] ||= false
1865
1829
  opt[:sort_tags] ||= false
1866
1830
 
1867
- cfg = @config['templates']['recent']
1831
+ cfg = @config['templates']['recent'].deep_merge(@config['templates']['default']).deep_merge({
1832
+ 'wrap_width' => @config['wrap_width'] || 0,
1833
+ 'date_format' => @config['default_date_format'],
1834
+ 'order' => @config['order'] || 'asc',
1835
+ 'tags_color' => @config['tags_color']
1836
+ })
1868
1837
  section ||= @config['current_section']
1869
1838
  section = guess_section(section)
1870
1839
 
1871
1840
  list_section({ section: section, wrap_width: cfg['wrap_width'], count: count,
1872
1841
  format: cfg['date_format'], template: cfg['template'],
1873
1842
  order: 'asc', times: times, totals: opt[:totals],
1874
- sort_tags: opt[:sort_tags], tags_color: opt[:tags_color] })
1843
+ sort_tags: opt[:sort_tags], tags_color: opt[:tags_color], config_template: 'recent' })
1875
1844
  end
1876
1845
 
1877
1846
  ##
1878
- ## @brief Show the last entry
1847
+ ## Show the last entry
1879
1848
  ##
1880
1849
  ## @param times (Bool) Show times
1881
- ## @param section (String) Section to pull from, default Currently
1850
+ ## @param section [String] Section to pull from, default Currently
1882
1851
  ##
1883
1852
  def last(times: true, section: nil, options: {})
1884
1853
  section = section.nil? || section =~ /all/i ? 'All' : guess_section(section)
1885
- cfg = @config['templates']['last']
1854
+ cfg = @config['templates']['last'].deep_merge(@config['templates']['default']).deep_merge({
1855
+ 'wrap_width' => @config['wrap_width'] || 0,
1856
+ 'date_format' => @config['default_date_format'],
1857
+ 'order' => @config['order'] || 'asc',
1858
+ 'tags_color' => @config['tags_color']
1859
+ })
1886
1860
 
1887
1861
  opts = {
1888
1862
  section: section,
@@ -1903,15 +1877,16 @@ module Doing
1903
1877
  opts[:search] = options[:search] if options[:search]
1904
1878
  opts[:case] = options[:case]
1905
1879
  opts[:not] = options[:negate]
1880
+ opts[:config_template] = 'last'
1906
1881
  list_section(opts)
1907
1882
  end
1908
1883
 
1909
1884
  ##
1910
- ## @brief Uses 'autotag' configuration to turn keywords into tags for time tracking.
1885
+ ## Uses 'autotag' configuration to turn keywords into tags for time tracking.
1911
1886
  ## Does not repeat tags in a title, and only converts the first instance of an
1912
1887
  ## untagged keyword
1913
1888
  ##
1914
- ## @param text (String) The text to tag
1889
+ ## @param text [String] The text to tag
1915
1890
  ##
1916
1891
  def autotag(text)
1917
1892
  return unless text
@@ -1919,78 +1894,100 @@ module Doing
1919
1894
 
1920
1895
  original = text.dup
1921
1896
 
1922
- current_tags = text.scan(/@\w+/)
1923
- whitelisted = []
1897
+ current_tags = text.scan(/@\w+/).map { |t| t.sub(/^@/, '') }
1898
+ tagged = {
1899
+ whitelisted: [],
1900
+ synonyms: [],
1901
+ transformed: [],
1902
+ replaced: []
1903
+ }
1904
+
1924
1905
  @config['autotag']['whitelist'].each do |tag|
1925
1906
  next if text =~ /@#{tag}\b/i
1926
1907
 
1927
- text.sub!(/(?<!@)\b(#{tag.strip})\b/i) do |m|
1928
- m.downcase! if tag =~ /[a-z]/
1929
- whitelisted.push("@#{m}")
1908
+ text.sub!(/(?<= |\A)(#{tag.strip})(?= |\Z)/i) do |m|
1909
+ m.downcase! unless tag =~ /[A-Z]/
1910
+ tagged[:whitelisted].push(m)
1930
1911
  "@#{m}"
1931
1912
  end
1932
1913
  end
1933
- tail_tags = []
1914
+
1934
1915
  @config['autotag']['synonyms'].each do |tag, v|
1935
1916
  v.each do |word|
1936
1917
  next unless text =~ /\b#{word}\b/i
1937
1918
 
1938
- tail_tags.push(tag) unless current_tags.include?("@#{tag}") || whitelisted.include?("@#{tag}")
1919
+ unless current_tags.include?(tag) || tagged[:whitelisted].include?(tag)
1920
+ tagged[:synonyms].push(tag)
1921
+ tagged[:synonyms] = tagged[:synonyms].uniq
1922
+ end
1939
1923
  end
1940
1924
  end
1925
+
1941
1926
  if @config['autotag'].key? 'transform'
1942
1927
  @config['autotag']['transform'].each do |tag|
1943
1928
  next unless tag =~ /\S+:\S+/
1944
1929
 
1945
1930
  rx, r = tag.split(/:/)
1931
+ flag_rx = %r{/([r]+)$}
1932
+ if r =~ flag_rx
1933
+ flags = r.match(flag_rx)[1].split(//)
1934
+ r.sub!(flag_rx, '')
1935
+ end
1946
1936
  r.gsub!(/\$/, '\\')
1947
- rx.sub!(/^@/, '')
1948
- regex = Regexp.new('@' + rx + '\b')
1949
-
1950
- matches = text.scan(regex)
1951
- next unless matches
1937
+ rx.sub!(/^@?/, '@')
1938
+ regex = Regexp.new("(?<= |\\A)#{rx}(?= |\\Z)")
1952
1939
 
1953
- matches.each do |m|
1940
+ text.sub!(regex) do
1941
+ m = Regexp.last_match
1954
1942
  new_tag = r
1955
- if m.is_a?(Array)
1956
- index = 1
1957
- m.each do |v|
1958
- new_tag.gsub!('\\' + index.to_s, v)
1959
- index += 1
1960
- end
1943
+
1944
+ m.to_a.slice(1, m.length - 1).each_with_index do |v, idx|
1945
+ new_tag.gsub!("\\#{idx + 1}", v)
1946
+ end
1947
+ # Replace original tag if /r
1948
+ if flags&.include?('r')
1949
+ tagged[:replaced].concat(new_tag.split(/ /).map { |t| t.sub(/^@/, '') })
1950
+ new_tag.split(/ /).map { |t| t.sub(/^@?/, '@') }.join(' ')
1951
+ else
1952
+ tagged[:transformed].concat(new_tag.split(/ /).map { |t| t.sub(/^@/, '') })
1953
+ tagged[:transformed] = tagged[:transformed].uniq
1954
+ m[0]
1961
1955
  end
1962
- tail_tags.push(new_tag)
1963
1956
  end
1964
1957
  end
1965
1958
  end
1966
1959
 
1967
- logger.debug('Autotag:', "whitelisted tags: #{whitelisted.join(', ')}") unless whitelisted.empty?
1968
- new_tags = whitelisted
1969
- unless tail_tags.empty?
1970
- tags = tail_tags.uniq.map { |t| "@#{t}".cyan }.join(' ')
1971
- logger.debug('Autotag:', "synonym tags: #{tags}")
1972
- tags_a = tail_tags.map { |t| "@#{t}" }
1973
- text.add_tags!(tags_a.join(' '))
1974
- new_tags.concat(tags_a)
1975
- end
1976
1960
 
1977
- unless text == original
1978
- logger.info('Autotag:', "added #{new_tags.join(', ')} to \"#{text}\"")
1961
+ logger.debug('Autotag:', "whitelisted tags: #{tagged[:whitelisted].log_tags}") unless tagged[:whitelisted].empty?
1962
+ logger.debug('Autotag:', "synonyms: #{tagged[:synonyms].log_tags}") unless tagged[:synonyms].empty?
1963
+ logger.debug('Autotag:', "transforms: #{tagged[:transformed].log_tags}") unless tagged[:transformed].empty?
1964
+ logger.debug('Autotag:', "transform replaced: #{tagged[:replaced].log_tags}") unless tagged[:replaced].empty?
1965
+
1966
+ tail_tags = tagged[:synonyms].concat(tagged[:transformed])
1967
+ tail_tags.sort!
1968
+ tail_tags.uniq!
1969
+
1970
+ text.add_tags!(tail_tags) unless tail_tags.empty?
1971
+
1972
+ if text == original
1973
+ logger.debug('Autotag:', "no change to \"#{text}\"")
1979
1974
  else
1980
- logger.debug('Skipped:', "no change to \"#{text}\"")
1975
+ new_tags = tagged[:whitelisted].concat(tail_tags).concat(tagged[:replaced])
1976
+ logger.debug('Autotag:', "added #{new_tags.log_tags} to \"#{text}\"")
1977
+ logger.count(:autotag, level: :info, count: 1, message: 'autotag updated %count %items')
1981
1978
  end
1982
1979
 
1983
- text
1980
+ text.dedup_tags
1984
1981
  end
1985
1982
 
1986
1983
  ##
1987
- ## @brief Get total elapsed time for all tags in
1984
+ ## Get total elapsed time for all tags in
1988
1985
  ## selection
1989
1986
  ##
1990
- ## @param format (String) return format (html,
1987
+ ## @param format [String] return format (html,
1991
1988
  ## 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)
1989
+ ## @param sort_by_name [Boolean] Sort by name if true, otherwise by time
1990
+ ## @param sort_order [String] The sort order (asc or desc)
1994
1991
  ##
1995
1992
  def tag_times(format: :text, sort_by_name: false, sort_order: 'asc')
1996
1993
  return '' if @timers.empty?
@@ -2120,13 +2117,13 @@ EOS
2120
2117
  end
2121
2118
 
2122
2119
  ##
2123
- ## @brief Gets the interval between entry's start
2120
+ ## Gets the interval between entry's start
2124
2121
  ## date and @done date
2125
2122
  ##
2126
- ## @param item (Hash) The entry
2127
- ## @param formatted (Bool) Return human readable
2123
+ ## @param item [Item] The entry
2124
+ ## @param formatted [Boolean] Return human readable
2128
2125
  ## time (default seconds)
2129
- ## @param record (Bool) Add the interval to the
2126
+ ## @param record [Boolean] Add the interval to the
2130
2127
  ## total for each tag
2131
2128
  ##
2132
2129
  ## @return Interval in seconds, or [d, h, m] array if
@@ -2146,28 +2143,9 @@ EOS
2146
2143
  end
2147
2144
 
2148
2145
  ##
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
2146
+ ## Format human readable time from seconds
2169
2147
  ##
2170
- ## @param seconds The seconds
2148
+ ## @param seconds [Integer] Seconds
2171
2149
  ##
2172
2150
  def format_time(seconds, human: false)
2173
2151
  return [0, 0, 0] if seconds.nil?
@@ -2193,6 +2171,169 @@ EOS
2193
2171
 
2194
2172
  private
2195
2173
 
2174
+ ##
2175
+ ## Wraps doing file content with additional
2176
+ ## header/footer content
2177
+ ##
2178
+ ## @return [String] concatenated content
2179
+ ##
2180
+ def combined_content
2181
+ output = @other_content_top ? "#{@other_content_top.join("\n")}\n" : ''
2182
+
2183
+ @content.each do |title, section|
2184
+ output += "#{section[:original]}\n"
2185
+ output += list_section({ section: title, template: "\t- %date | %title%t2note", highlight: false, wrap_width: 0 })
2186
+ end
2187
+
2188
+ output + @other_content_bottom.join("\n") unless @other_content_bottom.nil?
2189
+ end
2190
+
2191
+ ##
2192
+ ## Generate output using available export plugins
2193
+ ##
2194
+ ## @param items [Array] The items
2195
+ ## @param title [String] Page title
2196
+ ## @param is_single [Boolean] Indicates if single
2197
+ ## section
2198
+ ## @param opt [Hash] Additional options
2199
+ ##
2200
+ ## @return [String] formatted output based on opt[:output]
2201
+ ## template trigger
2202
+ ##
2203
+ def output(items, title, is_single, opt = {})
2204
+ out = nil
2205
+
2206
+ raise InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
2207
+
2208
+ export_options = { page_title: title, is_single: is_single, options: opt }
2209
+
2210
+ Plugins.plugins[:export].each do |_, options|
2211
+ next unless opt[:output] =~ /^(#{options[:trigger].normalize_trigger})$/i
2212
+
2213
+ out = options[:class].render(self, items, variables: export_options)
2214
+ break
2215
+ end
2216
+
2217
+ out
2218
+ end
2219
+
2220
+ ##
2221
+ ## Record times for item tags
2222
+ ##
2223
+ ## @param item [Item] The item to record
2224
+ ##
2225
+ def record_tag_times(item, seconds)
2226
+ item_hash = "#{item.date.strftime('%s')}#{item.title}#{item.section}"
2227
+ return if @recorded_items.include?(item_hash)
2228
+ item.title.scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
2229
+ k = m[0] == 'done' ? 'All' : m[0].downcase
2230
+ if @timers.key?(k)
2231
+ @timers[k] += seconds
2232
+ else
2233
+ @timers[k] = seconds
2234
+ end
2235
+ @recorded_items.push(item_hash)
2236
+ end
2237
+ end
2238
+
2239
+ ##
2240
+ ## Helper function, performs the actual archiving
2241
+ ##
2242
+ ## @param sect [String] The source section
2243
+ ## @param destination [String] The destination
2244
+ ## section
2245
+ ## @param opt [Hash] Additional Options
2246
+ ##
2247
+ def do_archive(sect, destination, opt = {})
2248
+ count = opt[:count] || 0
2249
+ tags = opt[:tags] || []
2250
+ bool = opt[:bool] || :and
2251
+ label = opt[:label] || true
2252
+
2253
+ if sect =~ /^all$/i
2254
+ all_sections = sections.dup
2255
+ all_sections.delete(destination)
2256
+ else
2257
+ all_sections = [sect]
2258
+ end
2259
+
2260
+ counter = 0
2261
+
2262
+ all_sections.each do |section|
2263
+ items = @content[section][:items].dup
2264
+
2265
+ moved_items = []
2266
+ if !tags.empty? || opt[:search] || opt[:before]
2267
+ if opt[:before]
2268
+ time_string = opt[:before]
2269
+ cutoff = chronify(time_string, guess: :begin)
2270
+ end
2271
+
2272
+ items.delete_if do |item|
2273
+ if ((!tags.empty? && item.tags?(tags, bool)) || (opt[:search] && item.search(opt[:search].to_s)) || (opt[:before] && item.date < cutoff))
2274
+ moved_items.push(item)
2275
+ counter += 1
2276
+ true
2277
+ else
2278
+ false
2279
+ end
2280
+ end
2281
+ moved_items.each do |item|
2282
+ if label
2283
+ item.title = if section == @config['current_section']
2284
+ item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
2285
+ else
2286
+ item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
2287
+ end
2288
+ logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
2289
+ end
2290
+ end
2291
+
2292
+ @content[section][:items] = items
2293
+ @content[destination][:items].concat(moved_items)
2294
+ if moved_items.length.positive?
2295
+ logger.count(destination == 'Archive' ? :archived : :moved,
2296
+ level: :info,
2297
+ count: moved_items.length,
2298
+ message: "%count %items from #{section} to #{destination}")
2299
+ else
2300
+ logger.info('Skipped:', 'No items were moved')
2301
+ end
2302
+ else
2303
+ count = items.length if items.length < count
2304
+
2305
+ items.map! do |item|
2306
+ if label
2307
+ item.title = if section == @config['current_section']
2308
+ item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
2309
+ else
2310
+ item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
2311
+ end
2312
+ logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
2313
+ end
2314
+ item
2315
+ end
2316
+
2317
+ if items.count > count
2318
+ @content[destination][:items].concat(items[count..-1])
2319
+ else
2320
+ @content[destination][:items].concat(items)
2321
+ end
2322
+
2323
+ @content[section][:items] = if count.zero?
2324
+ []
2325
+ else
2326
+ items[0..count - 1]
2327
+ end
2328
+
2329
+ logger.count(destination == 'Archive' ? :archived : :moved,
2330
+ level: :info,
2331
+ count: items.length - count,
2332
+ message: "%count %items from #{section} to #{destination}")
2333
+ end
2334
+ end
2335
+ end
2336
+
2196
2337
  def run_after
2197
2338
  return unless @config.key?('run_after')
2198
2339