doing 2.1.39 → 2.1.42

Sign up to get free protection for your applications and to get access to all the features.
Files changed (229) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +67 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +1 -1
  6. data/Rakefile +4 -4
  7. data/bin/commands/again.rb +1 -3
  8. data/bin/commands/changes.rb +50 -34
  9. data/bin/commands/commands.rb +77 -52
  10. data/bin/commands/commands_accepting.rb +57 -53
  11. data/bin/commands/config.rb +45 -36
  12. data/bin/commands/done.rb +1 -18
  13. data/bin/commands/finish.rb +90 -59
  14. data/bin/commands/flag.rb +5 -1
  15. data/bin/commands/grep.rb +3 -14
  16. data/bin/commands/last.rb +2 -8
  17. data/bin/commands/meanwhile.rb +13 -6
  18. data/bin/commands/now.rb +151 -107
  19. data/bin/commands/on.rb +8 -18
  20. data/bin/commands/recent.rb +2 -8
  21. data/bin/commands/reset.rb +24 -1
  22. data/bin/commands/select.rb +1 -1
  23. data/bin/commands/show.rb +6 -17
  24. data/bin/commands/since.rb +1 -12
  25. data/bin/commands/tag_dir.rb +49 -15
  26. data/bin/commands/today.rb +2 -13
  27. data/bin/commands/undo.rb +4 -6
  28. data/bin/commands/view.rb +1 -1
  29. data/bin/commands/yesterday.rb +2 -13
  30. data/bin/doing +15 -8
  31. data/{Dockerfile → docker/Dockerfile} +3 -1
  32. data/{Dockerfile-2.6 → docker/Dockerfile-2.6} +2 -2
  33. data/{Dockerfile-2.7 → docker/Dockerfile-2.7} +2 -2
  34. data/{Dockerfile-3.0 → docker/Dockerfile-3.0} +2 -2
  35. data/{bash_profile → docker/bash_profile} +0 -0
  36. data/{inputrc → docker/inputrc} +0 -0
  37. data/docs/doc/Array.html +85 -2
  38. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  39. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  40. data/docs/doc/BooleanTermParser/Query.html +1 -1
  41. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  42. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  43. data/docs/doc/BooleanTermParser.html +1 -1
  44. data/docs/doc/Doing/ArrayNestedHash.html +198 -0
  45. data/docs/doc/Doing/ArrayTags.html +424 -0
  46. data/docs/doc/Doing/CSVExport.html +266 -0
  47. data/docs/doc/Doing/CalendarImport.html +232 -0
  48. data/docs/doc/Doing/Change.html +617 -0
  49. data/docs/doc/Doing/Changes.html +468 -0
  50. data/docs/doc/Doing/ChronifyArray.html +347 -0
  51. data/docs/doc/Doing/ChronifyNumeric.html +271 -0
  52. data/docs/doc/Doing/ChronifyString.html +682 -0
  53. data/docs/doc/Doing/Color.html +167 -21
  54. data/docs/doc/Doing/Completion/BashCompletions.html +445 -0
  55. data/docs/doc/Doing/Completion/FishCompletions.html +445 -0
  56. data/docs/doc/Doing/Completion/StringUtils.html +229 -0
  57. data/docs/doc/Doing/Completion/ZshCompletions.html +445 -0
  58. data/docs/doc/Doing/Completion.html +17 -3
  59. data/docs/doc/Doing/Configuration.html +3 -2
  60. data/docs/doc/Doing/DayOneRenderer.html +383 -0
  61. data/docs/doc/Doing/DayoneExport.html +290 -0
  62. data/docs/doc/Doing/DoingImport.html +391 -0
  63. data/docs/doc/Doing/Entry.html +381 -0
  64. data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
  65. data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
  66. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  67. data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
  68. data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
  69. data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
  70. data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
  71. data/docs/doc/Doing/Errors/NoResults.html +10 -2
  72. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  73. data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
  74. data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
  75. data/docs/doc/Doing/Errors.html +9 -9
  76. data/docs/doc/Doing/HTMLExport.html +256 -0
  77. data/docs/doc/Doing/Hooks.html +1 -1
  78. data/docs/doc/Doing/Item.html +179 -1660
  79. data/docs/doc/Doing/ItemDates.html +564 -0
  80. data/docs/doc/Doing/ItemQuery.html +614 -0
  81. data/docs/doc/Doing/ItemState.html +387 -0
  82. data/docs/doc/Doing/ItemTags.html +498 -0
  83. data/docs/doc/Doing/Items.html +581 -15
  84. data/docs/doc/Doing/JSONExport.html +222 -0
  85. data/docs/doc/Doing/Logger.html +1 -1
  86. data/docs/doc/Doing/MarkdownExport.html +266 -0
  87. data/docs/doc/Doing/MarkdownRenderer.html +383 -0
  88. data/docs/doc/Doing/Note.html +18 -4
  89. data/docs/doc/Doing/Pager.html +1 -1
  90. data/docs/doc/Doing/Plugins.html +181 -76
  91. data/docs/doc/Doing/Prompt.html +32 -683
  92. data/docs/doc/Doing/PromptChoose.html +484 -0
  93. data/docs/doc/Doing/PromptFZF.html +391 -0
  94. data/docs/doc/Doing/PromptInput.html +572 -0
  95. data/docs/doc/Doing/PromptSTD.html +293 -0
  96. data/docs/doc/Doing/PromptYN.html +237 -0
  97. data/docs/doc/Doing/Section.html +58 -2
  98. data/docs/doc/Doing/StringHighlight.html +533 -0
  99. data/docs/doc/Doing/StringNormalize.html +929 -0
  100. data/docs/doc/Doing/StringQuery.html +725 -0
  101. data/docs/doc/Doing/StringTags.html +884 -0
  102. data/docs/doc/Doing/StringTransform.html +599 -0
  103. data/docs/doc/Doing/StringTruncate.html +448 -0
  104. data/docs/doc/Doing/StringURL.html +409 -0
  105. data/docs/doc/Doing/SymbolNormalize.html +341 -0
  106. data/docs/doc/Doing/TaskPaperExport.html +222 -0
  107. data/docs/doc/Doing/TemplateExport.html +249 -0
  108. data/docs/doc/Doing/TemplateString.html +102 -3
  109. data/docs/doc/Doing/TimingImport.html +285 -0
  110. data/docs/doc/Doing/Types.html +1 -1
  111. data/docs/doc/Doing/Util/Backup.html +11 -163
  112. data/docs/doc/Doing/Util.html +67 -10
  113. data/docs/doc/Doing/Version.html +523 -0
  114. data/docs/doc/Doing/WWID/WWIDUtil.html +510 -0
  115. data/docs/doc/Doing/WWID.html +476 -139
  116. data/docs/doc/Doing/WWIDDisplay.html +865 -0
  117. data/docs/doc/Doing/WWIDEditor.html +466 -0
  118. data/docs/doc/Doing/WWIDFileTools.html +359 -0
  119. data/docs/doc/Doing/WWIDFilter.html +466 -0
  120. data/docs/doc/Doing/WWIDGuess.html +299 -0
  121. data/docs/doc/Doing/WWIDInteractive.html +752 -0
  122. data/docs/doc/Doing/WWIDModify.html +1078 -0
  123. data/docs/doc/Doing/WWIDTags.html +302 -0
  124. data/docs/doc/Doing/WWIDTimers.html +359 -0
  125. data/docs/doc/Doing/WWIDUtil.html +510 -0
  126. data/docs/doc/Doing.html +9 -6
  127. data/docs/doc/FalseClass.html +1 -1
  128. data/docs/doc/GLI/Commands/Help.html +1 -1
  129. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  130. data/docs/doc/GLI/Commands.html +1 -1
  131. data/docs/doc/GLI.html +1 -1
  132. data/docs/doc/Hash.html +1 -1
  133. data/docs/doc/Numeric.html +23 -78
  134. data/docs/doc/Object.html +1 -1
  135. data/docs/doc/PhraseParser/Operator.html +1 -1
  136. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  137. data/docs/doc/PhraseParser/Query.html +1 -1
  138. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  139. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  140. data/docs/doc/PhraseParser/TermClause.html +1 -1
  141. data/docs/doc/PhraseParser.html +1 -1
  142. data/docs/doc/Status.html +1 -1
  143. data/docs/doc/String.html +58 -633
  144. data/docs/doc/Symbol.html +9 -224
  145. data/docs/doc/Time.html +119 -13
  146. data/docs/doc/TrueClass.html +1 -1
  147. data/docs/doc/_index.html +348 -4
  148. data/docs/doc/class_list.html +1 -1
  149. data/docs/doc/file.README.html +2 -2
  150. data/docs/doc/index.html +2 -2
  151. data/docs/doc/method_list.html +1904 -592
  152. data/docs/doc/top-level-namespace.html +12 -4
  153. data/docs/index.md +1 -1
  154. data/doing.rdoc +67 -15
  155. data/lib/completion/_doing.zsh +6 -6
  156. data/lib/completion/doing.bash +10 -10
  157. data/lib/completion/doing.fish +10 -3
  158. data/lib/doing/add_options.rb +39 -1
  159. data/lib/doing/array/array.rb +18 -12
  160. data/lib/doing/array/cleanup.rb +31 -0
  161. data/lib/doing/array/nested_hash.rb +1 -1
  162. data/lib/doing/array/tags.rb +6 -5
  163. data/lib/doing/changelog/changelog.rb +6 -0
  164. data/lib/doing/chronify/array.rb +65 -25
  165. data/lib/doing/chronify/chronify.rb +12 -0
  166. data/lib/doing/chronify/numeric.rb +3 -2
  167. data/lib/doing/chronify/string.rb +1 -1
  168. data/lib/doing/colors.rb +77 -30
  169. data/lib/doing/completion/completion_string.rb +25 -0
  170. data/lib/doing/completion.rb +4 -5
  171. data/lib/doing/configuration.rb +7 -3
  172. data/lib/doing/errors.rb +51 -35
  173. data/lib/doing/good.rb +8 -0
  174. data/lib/doing/hooks.rb +3 -3
  175. data/lib/doing/item/dates.rb +112 -0
  176. data/lib/doing/item/item.rb +128 -0
  177. data/lib/doing/{item.rb → item/query.rb} +2 -353
  178. data/lib/doing/item/state.rb +59 -0
  179. data/lib/doing/item/tags.rb +87 -0
  180. data/lib/doing/items/filter.rb +67 -0
  181. data/lib/doing/items/items.rb +57 -0
  182. data/lib/doing/items/modify.rb +36 -0
  183. data/lib/doing/items/sections.rb +83 -0
  184. data/lib/doing/items/util.rb +74 -0
  185. data/lib/doing/normalize.rb +10 -2
  186. data/lib/doing/note.rb +1 -1
  187. data/lib/doing/pager.rb +9 -3
  188. data/lib/doing/plugin_manager.rb +33 -8
  189. data/lib/doing/plugins/export/markdown_export.rb +4 -2
  190. data/lib/doing/plugins/export/template_export.rb +4 -4
  191. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  192. data/lib/doing/plugins/import/doing_import.rb +1 -1
  193. data/lib/doing/prompt/choose.rb +118 -0
  194. data/lib/doing/prompt/fzf.rb +84 -0
  195. data/lib/doing/prompt/input.rb +129 -0
  196. data/lib/doing/prompt/prompt.rb +41 -0
  197. data/lib/doing/prompt/std.rb +32 -0
  198. data/lib/doing/prompt/yn.rb +64 -0
  199. data/lib/doing/section.rb +4 -0
  200. data/lib/doing/string/highlight.rb +1 -1
  201. data/lib/doing/string/query.rb +1 -1
  202. data/lib/doing/string/string.rb +18 -7
  203. data/lib/doing/string/tags.rb +14 -3
  204. data/lib/doing/string/transform.rb +7 -1
  205. data/lib/doing/string/truncate.rb +1 -1
  206. data/lib/doing/string/url.rb +1 -1
  207. data/lib/doing/time.rb +19 -1
  208. data/lib/doing/util.rb +12 -6
  209. data/lib/doing/util_backup.rb +62 -57
  210. data/lib/doing/version.rb +1 -1
  211. data/lib/doing/wwid/display.rb +396 -0
  212. data/lib/doing/wwid/editor.rb +214 -0
  213. data/lib/doing/wwid/filetools.rb +183 -0
  214. data/lib/doing/wwid/filter.rb +226 -0
  215. data/lib/doing/wwid/guess.rb +85 -0
  216. data/lib/doing/wwid/interactive.rb +377 -0
  217. data/lib/doing/wwid/modify.rb +617 -0
  218. data/lib/doing/wwid/tags.rb +51 -0
  219. data/lib/doing/wwid/timers.rb +342 -0
  220. data/lib/doing/wwid/wwid.rb +121 -0
  221. data/lib/doing/wwid/wwidutil.rb +101 -0
  222. data/lib/doing.rb +7 -7
  223. data/lib/helpers/threaded_tests.rb +1 -0
  224. metadata +94 -14
  225. data/lib/doing/changelog.rb +0 -6
  226. data/lib/doing/completion/string.rb +0 -17
  227. data/lib/doing/items.rb +0 -196
  228. data/lib/doing/prompt.rb +0 -330
  229. data/lib/doing/wwid.rb +0 -2398
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ # A Doing entry
5
+ module ItemTags
6
+ ##
7
+ ## Add (or remove) tags from the title of the item
8
+ ##
9
+ ## @param tags [Array] The tags to apply
10
+ ## @param options Additional options
11
+ ##
12
+ ## @option options :date [Boolean] Include timestamp?
13
+ ## @option options :single [Boolean] Log as a single change?
14
+ ## @option options :value [String] A value to include as @tag(value)
15
+ ## @option options :remove [Boolean] if true remove instead of adding
16
+ ## @option options :rename_to [String] if not nil, rename target tag to this tag name
17
+ ## @option options :regex [Boolean] treat target tag string as regex pattern
18
+ ## @option options :force [Boolean] with rename_to, add tag if it doesn't exist
19
+ ##
20
+ def tag(tags, **options)
21
+ added = []
22
+ removed = []
23
+
24
+ date = options.fetch(:date, false)
25
+ options[:value] ||= date ? Time.now.strftime('%F %R') : nil
26
+ options.delete(:date)
27
+
28
+ single = options.fetch(:single, false)
29
+ options.delete(:single)
30
+
31
+ tags = tags.to_tags if tags.is_a? ::String
32
+
33
+ remove = options.fetch(:remove, false)
34
+ tags.each do |tag|
35
+ if tag =~ /^(\S+)\((.*?)\)$/
36
+ m = Regexp.last_match
37
+ tag = m[1]
38
+ options[:value] ||= m[2]
39
+ end
40
+
41
+ bool = remove ? :and : :not
42
+ if tags?(tag, bool) || options[:value]
43
+ @title = @title.tag(tag, **options).strip
44
+ remove ? removed.push(tag) : added.push(tag)
45
+ end
46
+ end
47
+
48
+ Doing.logger.log_change(tags_added: added, tags_removed: removed, count: 1, item: self, single: single)
49
+
50
+ self
51
+ end
52
+
53
+ ##
54
+ ## Get a list of tags on the item
55
+ ##
56
+ ## @return [Array] array of tags (no values)
57
+ ##
58
+ def tags
59
+ @title.scan(/(?<= |\A)@([^\s(]+)/).map { |tag| tag[0] }.sort.uniq
60
+ end
61
+
62
+ ##
63
+ ## Return all tags including parenthetical values
64
+ ##
65
+ ## @return [Array<Array>] Array of array pairs,
66
+ ## [[tag1, value], [tag2, value]]
67
+ ##
68
+ def tags_with_values
69
+ @title.scan(/(?<= |\A)@([^\s(]+)(?:\((.*?)\))?/).map { |tag| [tag[0], tag[1]] }.sort.uniq
70
+ end
71
+
72
+ ##
73
+ ## convert tags on item to an array with @ symbols removed
74
+ ##
75
+ ## @return [Array] array of tags
76
+ ##
77
+ def tag_array
78
+ tags.tags_to_array
79
+ end
80
+
81
+ private
82
+
83
+ def split_tags(tags)
84
+ tags.to_tags.tags_to_array
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ class Items < Array
5
+ # Get a new Items object containing only items in a
6
+ # specified section
7
+ #
8
+ # @param section [String] section title
9
+ #
10
+ # @return [Items] Array of items
11
+ #
12
+ def in_section(section)
13
+ if section =~ /^all$/i
14
+ dup
15
+ else
16
+ items = Items.new.concat(select { |item| !item.nil? && item.section == section })
17
+ items.add_section(section, log: false)
18
+ items
19
+ end
20
+ end
21
+
22
+ ##
23
+ ## Search Items for a string (title and note)
24
+ ##
25
+ ## @param query [String] The query
26
+ ## @param case_type [Symbol] The case type
27
+ ## (:smart, :sensitive, :ignore)
28
+ ##
29
+ ## @return [Items] array of items matching search
30
+ ##
31
+ def search(query, case_type: :smart)
32
+ WWID.new.fuzzy_filter_items(self, query, case_type: case_type)
33
+ end
34
+
35
+ ##
36
+ ## Search items by tags
37
+ ##
38
+ ## @param tags [Array,String] The tags by which to
39
+ ## filter
40
+ ## @param bool [Symbol] The bool with which to
41
+ ## combine multiple tags
42
+ ##
43
+ ## @return [Items] array of items matching tag filter
44
+ ##
45
+ def tagged(tags, bool: :and)
46
+ WWID.new.filter_items(self, opt: { tag: tags, bool: bool })
47
+ end
48
+
49
+ ##
50
+ ## Filter Items by date. String arguments will be
51
+ ## chronified
52
+ ##
53
+ ## @param start [Time,String] Filter items after
54
+ ## this date
55
+ ## @param finish [Time,String] Filter items before
56
+ ## this date
57
+ ##
58
+ ## @return [Items] array of items with dates between
59
+ ## targets
60
+ ##
61
+ def between_dates(start, finish)
62
+ start = start.chronify(guess: :begin, future: false) if start.is_a?(String)
63
+ finish = finish.chronify(guess: :end) if finish.is_a?(String)
64
+ WWID.new.filter_items(self, opt: { date_filter: [start, finish] })
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'filter'
4
+ require_relative 'modify'
5
+ require_relative 'sections'
6
+ require_relative 'util'
7
+
8
+ module Doing
9
+ # A collection of Item objects
10
+ class Items < Array
11
+ attr_accessor :sections
12
+
13
+ def initialize
14
+ super
15
+ @sections = []
16
+ end
17
+
18
+ ##
19
+ ## Test if self includes Item
20
+ ##
21
+ ## @param item [Item] The item to search for
22
+ ## @param match_section [Boolean] Section must match
23
+ ##
24
+ ## @return [Boolean] True if Item exists
25
+ ##
26
+ def include?(item, match_section: true)
27
+ includes = false
28
+ each do |other_item|
29
+ if other_item.equal?(item, match_section: match_section)
30
+ includes = true
31
+ break
32
+ end
33
+ end
34
+
35
+ includes
36
+ end
37
+
38
+ # Output sections and items in Doing file format
39
+ def to_s
40
+ out = []
41
+ @sections.each do |section|
42
+ out.push(section.original)
43
+ items = in_section(section.title).sort_by { |i| [i.date, i.title] }
44
+ items.reverse! if Doing.setting('doing_file_sort').normalize_order == :desc
45
+ items.each { |item| out.push(item.to_s) }
46
+ end
47
+
48
+ out.join("\n")
49
+ end
50
+
51
+ # @private
52
+ def inspect
53
+ sections = @sections.map { |s| "<Section:#{s.title} #{in_section(s.title).count} items>" }.join(', ')
54
+ "#<Doing::Items #{count} items, #{@sections.count} sections: #{sections}>"
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ class Items < Array
5
+ ##
6
+ ## Delete an item from the index
7
+ ##
8
+ ## @param item The item
9
+ ##
10
+ def delete_item(item, single: false)
11
+ deleted = delete(item)
12
+ Doing.logger.count(:deleted)
13
+ Doing.logger.info('Entry deleted:', deleted.title) if single
14
+ deleted
15
+ end
16
+
17
+ ##
18
+ ## Update an item in the index with a modified item
19
+ ##
20
+ ## @param old_item The old item
21
+ ## @param new_item The new item
22
+ ##
23
+ def update_item(old_item, new_item)
24
+ s_idx = index { |item| item.equal?(old_item) }
25
+
26
+ raise ItemNotFound, 'Unable to find item in index, did it mutate?' unless s_idx
27
+
28
+ return if fetch(s_idx).equal?(new_item)
29
+
30
+ self[s_idx] = new_item
31
+ Doing.logger.count(:updated)
32
+ Doing.logger.info('Entry updated:', self[s_idx].title.trunc(60))
33
+ new_item
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ class Items < Array
5
+ # List sections, title only
6
+ #
7
+ # @return [Array] section titles
8
+ #
9
+ def section_titles
10
+ @sections.map(&:title)
11
+ end
12
+
13
+ # Test if section already exists
14
+ #
15
+ # @param section [String] section title
16
+ #
17
+ # @return [Boolean] true if section exists
18
+ #
19
+ def section?(section)
20
+ section = section.is_a?(Section) ? section.title.downcase : section.downcase
21
+ @sections.map { |i| i.title.downcase }.include?(section)
22
+ end
23
+
24
+ ##
25
+ ## Return the best section match for a search query
26
+ ##
27
+ ## @param frag The search query
28
+ ## @param distance The distance apart characters can be (fuzziness)
29
+ ##
30
+ ## @return [Section] (first) matching section object
31
+ ##
32
+ def guess_section(frag, distance: 2)
33
+ section = nil
34
+ re = frag.to_rx(distance: distance, case_type: :ignore)
35
+ @sections.each do |sect|
36
+ next unless sect.title =~ /#{re}/i
37
+
38
+ Doing.logger.debug('Match:', %(Assuming "#{sect.title}" from "#{frag}"))
39
+ section = sect
40
+ break
41
+ end
42
+
43
+ section
44
+ end
45
+
46
+ # Add a new section to the sections array. Accepts
47
+ # either a Section object, or a title string that will
48
+ # be converted into a Section.
49
+ #
50
+ # @param section [Section] The section to add. A
51
+ # String value will be converted to
52
+ # Section automatically.
53
+ # @param log [Boolean] Add a log message
54
+ # notifying the user about the
55
+ # creation of the section.
56
+ #
57
+ # @return nothing
58
+ #
59
+ def add_section(section, log: false)
60
+ section = section.is_a?(Section) ? section : Section.new(section.cap_first)
61
+
62
+ return if section?(section)
63
+
64
+ @sections.push(section)
65
+ Doing.logger.info('New section:', %("#{section}" added)) if log
66
+ end
67
+
68
+ def delete_section(section, log: false)
69
+ return unless section?(section)
70
+
71
+ raise DoingRuntimeError, 'Section not empty' if in_section(section).count.positive?
72
+
73
+ @sections.each do |sect|
74
+ next unless sect.title == section && in_section(sect).count.zero?
75
+
76
+ @sections.delete(sect)
77
+ Doing.logger.info('Removed section:', %("#{section}" removed)) if log
78
+ end
79
+
80
+ Doing.logger.error('Not found:', %("#{section}" not found))
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ class Items < Array
5
+ # # Create a deep copy of Items
6
+ # def clone
7
+ # Marshal.load(Marshal.dump(self))
8
+ # end
9
+
10
+ def delete(item)
11
+ deleted = nil
12
+ each_with_index do |i, idx|
13
+ if i.equal?(item, match_section: true)
14
+ deleted = delete_at(idx)
15
+ break
16
+ end
17
+ end
18
+ deleted
19
+ end
20
+
21
+ ##
22
+ ## Get all tags on Items in self
23
+ ##
24
+ ## @return [Array] array of tags
25
+ ##
26
+ def all_tags
27
+ each_with_object([]) do |entry, tags|
28
+ tags.concat(entry.tags).sort!.uniq!
29
+ end
30
+ end
31
+
32
+ ##
33
+ ## Return Items containing items that don't exist in
34
+ ## receiver
35
+ ##
36
+ ## @param items [Items] Receiver
37
+ ##
38
+ ## @return [Hash] Hash of added and deleted items
39
+ ##
40
+ def diff(items)
41
+ a = clone
42
+ b = items.clone
43
+
44
+ a.delete_if do |item|
45
+ if b.include?(item)
46
+ b.delete(item)
47
+ true
48
+ else
49
+ false
50
+ end
51
+ end
52
+ { added: b, deleted: a }
53
+ end
54
+
55
+ ##
56
+ ## Remove duplicated entries. Duplicate entries must have matching start date, title, note, and section
57
+ ##
58
+ ## @return [Items] Items array with duplicate entries removed
59
+ ##
60
+ def dedup(match_section: true)
61
+ unique = Items.new
62
+ each do |item|
63
+ unique.push(item) unless unique.include?(item, match_section: match_section)
64
+ end
65
+
66
+ unique
67
+ end
68
+
69
+ # @see #dedup
70
+ def dedup!(match_section: true)
71
+ replace dedup(match_section: match_section)
72
+ end
73
+ end
74
+ end
@@ -4,7 +4,7 @@ module Doing
4
4
  ##
5
5
  ## String to symbol conversion
6
6
  ##
7
- class ::String
7
+ module StringNormalize
8
8
  ##
9
9
  ## Convert tag sort string to a qualified type
10
10
  ##
@@ -160,7 +160,7 @@ module Doing
160
160
  ##
161
161
  ## Symbol helpers
162
162
  ##
163
- class ::Symbol
163
+ module SymbolNormalize
164
164
  def normalize_tag_sort(default = :name)
165
165
  to_s.normalize_tag_sort
166
166
  end
@@ -186,3 +186,11 @@ module Doing
186
186
  end
187
187
  end
188
188
  end
189
+
190
+ class ::String
191
+ include Doing::StringNormalize
192
+ end
193
+
194
+ class ::Symbol
195
+ include Doing::SymbolNormalize
196
+ end
data/lib/doing/note.rb CHANGED
@@ -20,7 +20,7 @@ module Doing
20
20
  ##
21
21
  ## Add note contents, optionally replacing existing note
22
22
  ##
23
- ## @param note [Array] The note to add, can be
23
+ ## @param note [Array|String|Note] The note to add, can be
24
24
  ## String, Array, or Note
25
25
  ## @param replace [Boolean] replace existing
26
26
  ## content
data/lib/doing/pager.rb CHANGED
@@ -70,14 +70,20 @@ module Doing
70
70
  end
71
71
 
72
72
  def pagers
73
- [ENV['GIT_PAGER'], ENV['PAGER'], git_pager,
74
- 'bat -p --pager="less -Xr"', 'less -Xr', 'more -r'].compact
73
+ [
74
+ Doing.setting('editors.pager'),
75
+ ENV['PAGER'],
76
+ 'less -Xr',
77
+ ENV['GIT_PAGER'],
78
+ git_pager,
79
+ 'more -r'
80
+ ].remove_bad
75
81
  end
76
82
 
77
83
  def find_executable(*commands)
78
84
  execs = commands.empty? ? pagers : commands
79
85
  execs
80
- .compact.map(&:strip).reject(&:empty?).uniq
86
+ .remove_bad.uniq
81
87
  .find { |cmd| TTY::Which.exist?(cmd.split.first) }
82
88
  end
83
89
 
@@ -4,10 +4,17 @@ module Doing
4
4
  # Plugin handling
5
5
  module Plugins
6
6
  class << self
7
+ # Return the user's home directory
7
8
  def user_home
8
9
  @user_home ||= Util.user_home
9
10
  end
10
11
 
12
+ # Storage for registered plugins. Hash with :import
13
+ # and :export keys containing hashes of available
14
+ # plugins.
15
+ #
16
+ # @return [Hash] registered plugins
17
+ #
11
18
  def plugins
12
19
  @plugins ||= {
13
20
  import: {},
@@ -81,14 +88,25 @@ module Doing
81
88
  Doing.logger.debug('Plugin Manager:', "Registered #{type} plugin \"#{title}\"")
82
89
  end
83
90
 
91
+ ##
92
+ ## Verifies that a plugin is properly configured with
93
+ ## necessary methods for its type. If the plugin fails
94
+ ## validation, a PluginUncallable exception will be
95
+ ## raised.
96
+ ##
97
+ ## @param title [String] The title
98
+ ## @param type [Symbol] type, :import or
99
+ ## :export
100
+ ## @param klass [Class] Plugin class
101
+ ##
84
102
  def validate_plugin(title, type, klass)
85
103
  type = valid_type(type)
86
104
  if type == :import && !klass.respond_to?(:import)
87
- raise Errors::PluginUncallable.new('Import plugins must respond to :import', type: type, plugin: title)
105
+ raise Errors::PluginUncallable.new('Import plugins must respond to :import', type, title)
88
106
  end
89
107
 
90
108
  if type == :export && !klass.respond_to?(:render)
91
- raise Errors::PluginUncallable.new('Export plugins must respond to :render', type: type, plugin: title)
109
+ raise Errors::PluginUncallable.new('Export plugins must respond to :render', type, title)
92
110
  end
93
111
 
94
112
  type
@@ -113,7 +131,7 @@ module Doing
113
131
  when /^e(x(p(o(r(t)?)?)?)?)?$/
114
132
  :export
115
133
  else
116
- raise Errors::InvalidPluginType, 'Invalid plugin type'
134
+ raise Errors::InvalidPluginType.new('Invalid plugin type', 'unrecognized')
117
135
  end
118
136
 
119
137
  type.to_sym
@@ -122,8 +140,10 @@ module Doing
122
140
  ##
123
141
  ## List available plugins to stdout
124
142
  ##
125
- ## @param options { type, separator }
143
+ ## @param options [Hash] additional options
126
144
  ##
145
+ ## @option options :column [Boolean] display results in a single column
146
+ ## @option options :type [String] Plugin type: all, import, or export
127
147
  def list_plugins(options = {})
128
148
  separator = options[:column] ? "\n" : "\t"
129
149
  type = options[:type].nil? || options[:type] =~ /all/i ? 'all' : valid_type(options[:type])
@@ -144,9 +164,9 @@ module Doing
144
164
  ##
145
165
  ## Return array of available plugin names
146
166
  ##
147
- ## @param type Plugin type (:import, :export)
167
+ ## @param type [Symbol] Plugin type (:import, :export)
148
168
  ##
149
- ## @return [Array<String>] plugin names
169
+ ## @return [Array] Array of plugin names (String)
150
170
  ##
151
171
  def available_plugins(type: :export)
152
172
  type = valid_type(type)
@@ -159,7 +179,7 @@ module Doing
159
179
  ## @param type Plugin type (:import, :export)
160
180
  ## @param separator The separator to join names with
161
181
  ##
162
- ## @return [String] Plugin names
182
+ ## @return [String] Plugin names joined with separator
163
183
  ##
164
184
  def plugin_names(type: :export, separator: '|')
165
185
  type = valid_type(type)
@@ -190,7 +210,7 @@ module Doing
190
210
  ## @param type [Symbol] Plugin type (:import,
191
211
  ## :export)
192
212
  ##
193
- ## @return [Array<String>] template names
213
+ ## @return [Array] Array of template names (String)
194
214
  ##
195
215
  def plugin_templates(type: :export)
196
216
  type = valid_type(type)
@@ -236,6 +256,9 @@ module Doing
236
256
  ## @param trigger [String] The trigger to test
237
257
  ## @param type [Symbol] the plugin type
238
258
  ## (:import, :export)
259
+ ## @param save_to [String] if a path is
260
+ ## specified, write the template
261
+ ## to that path. Nil for STDOUT
239
262
  ##
240
263
  ## @return [String] string content of template for trigger
241
264
  ##
@@ -255,6 +278,8 @@ module Doing
255
278
  raise Errors::InvalidArgument, "No template type matched \"#{trigger}\""
256
279
  end
257
280
 
281
+ private
282
+
258
283
  def save_template(tpl, dir, filename)
259
284
  dir = File.expand_path(dir)
260
285
  FileUtils.mkdir_p(dir) unless File.exist?(dir)
@@ -5,6 +5,7 @@
5
5
  # author: Brett Terpstra
6
6
  # url: https://brettterpstra.com
7
7
  module Doing
8
+ # @private
8
9
  class MarkdownRenderer
9
10
  attr_accessor :items, :page_title, :totals
10
11
 
@@ -15,12 +16,13 @@ module Doing
15
16
  end
16
17
 
17
18
  def get_binding
18
- binding()
19
+ binding
19
20
  end
20
21
  end
21
22
 
23
+ # Markdown Export Plugin
22
24
  class MarkdownExport
23
- include Doing::Util
25
+ include Util
24
26
 
25
27
  def self.settings
26
28
  {
@@ -17,7 +17,7 @@ module Doing
17
17
  end
18
18
 
19
19
  def self.render(wwid, items, variables: {})
20
- # Doing.logger.benchmark(:template_render, :start)
20
+ Doing.logger.benchmark(:template_render, :start)
21
21
  return if items.nil?
22
22
 
23
23
  opt = variables[:options]
@@ -126,18 +126,18 @@ module Doing
126
126
 
127
127
  output.gsub!(/\\%/, '%')
128
128
 
129
- output.highlight_search!(opt[:search]) if opt[:template] =~ /^temp/ && opt[:search] && !opt[:not] && opt[:hilite]
129
+ output.highlight_search!(opt[:search]) if opt[:output] =~ /^temp/ && opt[:search] && !opt[:not] && opt[:hilite]
130
130
 
131
131
  out += "#{output}\n"
132
132
  end
133
133
 
134
- # Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:template]}")
134
+ # Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:output]}")
135
135
  if opt[:totals]
136
136
  out += wwid.tag_times(format: Doing.setting('timer_format').to_sym,
137
137
  sort_by: opt[:sort_tags],
138
138
  sort_order: opt[:tag_order])
139
139
  end
140
- # Doing.logger.benchmark(:template_render, :finish)
140
+ Doing.logger.benchmark(:template_render, :finish)
141
141
  out
142
142
  end
143
143
 
@@ -125,7 +125,7 @@ module Doing
125
125
  next if line =~ /^\s*$/
126
126
 
127
127
  case line
128
- when /^(\S[\S ]+):\s*(@\S+\s*)*$/
128
+ when /^(\S[\S ]+):(\s+@[\w\-_.]+(?= |$))*\s*$/
129
129
  section = Regexp.last_match(1)
130
130
  current = 0
131
131
  when /^\s*- (\d{4}-\d\d-\d\d \d\d:\d\d) \| (.*)/