doing 2.1.39 → 2.1.42

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 (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) \| (.*)/