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
data/lib/doing/util.rb CHANGED
@@ -47,13 +47,13 @@ module Doing
47
47
  end
48
48
  end
49
49
 
50
- # Non-destructive version of deep_merge_hashes! See that
51
- # method.
50
+ # Non-destructive version of deep_merge_hashes!
51
+ # @see {deep_merge_hashes!}
52
52
  #
53
53
  # @return the merged hashes.
54
54
  #
55
- # @param [Hash] master_hash The master hash
56
- # @param [Hash] other_hash The other hash
55
+ # @param master_hash [Hash] The master hash
56
+ # @param other_hash [Hash] The other hash
57
57
  #
58
58
  def deep_merge_hashes(master_hash, other_hash)
59
59
  deep_merge_hashes!(master_hash.clone, other_hash)
@@ -61,13 +61,19 @@ module Doing
61
61
 
62
62
  # Merges a master hash with another hash, recursively.
63
63
  #
64
- # master_hash - the "parent" hash whose values will be overridden
65
- # other_hash - the other hash whose values will be persisted after the merge
64
+ # @param target [Hash] the "parent" hash whose
65
+ # values will be overridden
66
+ # @param overwrite [Hash] the other hash whose
67
+ # values will be persisted after
68
+ # the merge
66
69
  #
67
70
  # This code was lovingly stolen from some random gem:
68
71
  # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
69
72
  #
70
73
  # Thanks to whoever made it.
74
+ #
75
+ # @return [Hash] merged hashes
76
+ #
71
77
  def deep_merge_hashes!(target, overwrite)
72
78
  merge_values(target, overwrite)
73
79
  merge_default_proc(target, overwrite)
@@ -24,19 +24,6 @@ module Doing
24
24
  clear_redo(filename)
25
25
  end
26
26
 
27
- ##
28
- ## Delete all redo files
29
- ##
30
- ## @param limit Maximum number of backups to retain
31
- ##
32
- def clear_redo(filename)
33
- filename ||= Doing.setting('doing_file')
34
- backups = Dir.glob("undone*___#{File.basename(filename)}", base: backup_dir).sort.reverse
35
- backups.each do |file|
36
- FileUtils.rm(File.join(backup_dir, file))
37
- end
38
- end
39
-
40
27
  ##
41
28
  ## Retrieve the most recent backup
42
29
  ##
@@ -62,10 +49,11 @@ module Doing
62
49
  filename ||= Doing.setting('doing_file')
63
50
 
64
51
  backup_file = last_backup(filename, count: count)
65
- raise DoingRuntimeError, 'End of undo history' if backup_file.nil?
52
+ raise HistoryLimitError, 'End of undo history' if backup_file.nil?
66
53
 
67
54
  save_undone(filename)
68
- FileUtils.mv(backup_file, filename)
55
+ move_backup(backup_file, filename)
56
+
69
57
  prune_backups_after(File.basename(backup_file))
70
58
  Doing.logger.warn('File update:', "restored from #{backup_file}")
71
59
  Doing.logger.benchmark(:restore_backup, :finish)
@@ -78,7 +66,7 @@ module Doing
78
66
  ##
79
67
  def redo_backup(filename = nil, count: 1)
80
68
  filename ||= Doing.setting('doing_file')
81
- # redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
69
+
82
70
  undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort.reverse
83
71
  total = undones.count
84
72
  count = total if count > total
@@ -86,11 +74,11 @@ module Doing
86
74
  skipped = undones.slice!(0, count)
87
75
  undone = skipped.pop
88
76
 
89
- raise DoingRuntimeError, 'End of redo history' if undone.nil?
77
+ raise HistoryLimitError, 'End of redo history' if undone.nil?
90
78
 
91
79
  redo_file = File.join(backup_dir, undone)
92
80
 
93
- FileUtils.move(redo_file, filename)
81
+ move_backup(redo_file, filename)
94
82
 
95
83
  skipped.each do |f|
96
84
  FileUtils.mv(File.join(backup_dir, f), File.join(backup_dir, f.sub(/^undone/, '')))
@@ -100,14 +88,6 @@ module Doing
100
88
  Doing.logger.debug('Backup:', "#{total - skipped.count - 1} redos remaining")
101
89
  end
102
90
 
103
- def clear_undone(filename = nil)
104
- filename ||= Doing.setting('doing_file')
105
- # redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
106
- Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).each do |f|
107
- FileUtils.rm(File.join(backup_dir, f))
108
- end
109
- end
110
-
111
91
  ##
112
92
  ## Select from recent undos. If a filename is
113
93
  ## provided, only backups of that filename will be used.
@@ -118,8 +98,7 @@ module Doing
118
98
  filename ||= Doing.setting('doing_file')
119
99
 
120
100
  undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort
121
-
122
- raise DoingRuntimeError, 'End of redo history' if undones.empty?
101
+ raise HistoryLimitError, 'End of redo history' if undones.empty?
123
102
 
124
103
  total = undones.count
125
104
  options = undones.each_with_object([]) do |file, arr|
@@ -128,8 +107,7 @@ module Doing
128
107
 
129
108
  arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
130
109
  end
131
-
132
- raise DoingRuntimeError, 'No backup files to load' if options.empty?
110
+ raise MissingBackupFile, 'No backup files to load' if options.empty?
133
111
 
134
112
  backup_file = show_menu(options, filename)
135
113
  idx = undones.index(File.basename(backup_file))
@@ -138,7 +116,7 @@ module Doing
138
116
 
139
117
  redo_file = File.join(backup_dir, undone)
140
118
 
141
- FileUtils.move(redo_file, filename)
119
+ move_backup(redo_file, filename)
142
120
 
143
121
  skipped.each do |f|
144
122
  FileUtils.mv(File.join(backup_dir, f), File.join(backup_dir, f.sub(/^undone/, '')))
@@ -163,15 +141,51 @@ module Doing
163
141
  arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
164
142
  end
165
143
 
166
- raise DoingRuntimeError, 'No backup files to load' if options.empty?
144
+ raise MissingBackupFile, 'No backup files to load' if options.empty?
167
145
 
168
146
  backup_file = show_menu(options, filename)
169
147
  Util.write_to_file(File.join(backup_dir, "undone___#{File.basename(filename)}"), IO.read(filename), backup: false)
170
- FileUtils.mv(backup_file, filename)
148
+ move_backup(backup_file, filename)
171
149
  prune_backups_after(File.basename(backup_file))
172
150
  Doing.logger.warn('File update:', "restored from #{backup_file}")
173
151
  end
174
152
 
153
+ ##
154
+ ## Writes a copy of the content to a dated backup file
155
+ ## in a hidden directory
156
+ ##
157
+ ## @param filename [String] The filename
158
+ ##
159
+ def write_backup(filename = nil)
160
+ Doing.logger.benchmark(:_write_backup, :start)
161
+ filename ||= Doing.setting('doing_file')
162
+
163
+ unless File.exist?(filename)
164
+ Doing.logger.debug('Backup:', "original file doesn't exist (#{filename})")
165
+ return
166
+ end
167
+
168
+ backup_file = File.join(backup_dir, "#{timestamp_filename}___#{File.basename(filename)}")
169
+ # compressed = Zlib::Deflate.deflate(content)
170
+ # Zlib::GzipWriter.open(backup_file + '.gz') do |gz|
171
+ # gz.write(IO.read(filename))
172
+ # end
173
+
174
+ FileUtils.cp(filename, backup_file)
175
+
176
+ prune_backups(filename, Doing.setting('history_size').to_i)
177
+ clear_undone(filename)
178
+ Doing.logger.benchmark(:_write_backup, :finish)
179
+ end
180
+
181
+ private
182
+
183
+ def move_backup(source, dest)
184
+ Hooks.trigger :pre_write, WWID.new, dest
185
+ FileUtils.mv(source, dest)
186
+ Hooks.trigger :post_write, dest
187
+ end
188
+
175
189
  def show_menu(options, filename)
176
190
  if TTY::Which.which('colordiff')
177
191
  preview = 'colordiff -U 1'
@@ -211,36 +225,27 @@ module Doing
211
225
  result.strip.split(/\t/).last
212
226
  end
213
227
 
228
+ def clear_undone(filename = nil)
229
+ filename ||= Doing.setting('doing_file')
230
+ # redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
231
+ Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).each do |f|
232
+ FileUtils.rm(File.join(backup_dir, f))
233
+ end
234
+ end
235
+
214
236
  ##
215
- ## Writes a copy of the content to a dated backup file
216
- ## in a hidden directory
237
+ ## Delete all redo files
217
238
  ##
218
- ## @param content The data to back up
239
+ ## @param filename [String] The filename
219
240
  ##
220
- def write_backup(filename = nil)
221
- Doing.logger.benchmark(:_write_backup, :start)
241
+ def clear_redo(filename)
222
242
  filename ||= Doing.setting('doing_file')
223
-
224
- unless File.exist?(filename)
225
- Doing.logger.debug('Backup:', "original file doesn't exist (#{filename})")
226
- return
243
+ backups = Dir.glob("undone*___#{File.basename(filename)}", base: backup_dir).sort.reverse
244
+ backups.each do |file|
245
+ FileUtils.rm(File.join(backup_dir, file))
227
246
  end
228
-
229
- backup_file = File.join(backup_dir, "#{timestamp_filename}___#{File.basename(filename)}")
230
- # compressed = Zlib::Deflate.deflate(content)
231
- # Zlib::GzipWriter.open(backup_file + '.gz') do |gz|
232
- # gz.write(IO.read(filename))
233
- # end
234
-
235
- FileUtils.cp(filename, backup_file)
236
-
237
- prune_backups(filename, Doing.setting('history_size').to_i)
238
- clear_undone(filename)
239
- Doing.logger.benchmark(:_write_backup, :finish)
240
247
  end
241
248
 
242
- private
243
-
244
249
  def timestamp_filename
245
250
  Time.now.strftime('%Y-%m-%d_%H.%M.%S')
246
251
  end
@@ -281,7 +286,7 @@ module Doing
281
286
  def create_backup_dir
282
287
  dir = File.expand_path(Doing.setting('backup_dir')) || File.join(user_home, '.doing_backup')
283
288
  if File.exist?(dir) && !File.directory?(dir)
284
- raise DoingRuntimeError, "Backup error: #{dir} is not a directory"
289
+ raise DoingNoTraceError.new("#{dir} is not a directory", topic: 'History:', exit_code: 27)
285
290
 
286
291
  end
287
292
 
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '2.1.39'
2
+ VERSION = '2.1.42'
3
3
  end
@@ -0,0 +1,396 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ class WWID
5
+ ##
6
+ ## Display contents of a section based on options
7
+ ##
8
+ ## @param opt [Hash] Additional Options
9
+ ##
10
+ def list_section(opt, items: Items.new)
11
+ logger.benchmark(:list_section, :start)
12
+ opt[:config_template] ||= 'default'
13
+
14
+ tpl_cfg = Doing.setting(['templates', opt[:config_template]])
15
+
16
+ cfg = if opt[:view_template]
17
+ Doing.setting(['views', opt[:view_template]]).deep_merge(tpl_cfg, { extend_existing_arrays: true, sort_merged_arrays: true })
18
+ else
19
+ tpl_cfg
20
+ end
21
+
22
+ cfg.deep_merge({
23
+ 'wrap_width' => Doing.setting('wrap_width') || 0,
24
+ 'date_format' => Doing.setting('default_date_format'),
25
+ 'order' => Doing.setting('order') || :asc,
26
+ 'tags_color' => Doing.setting('tags_color'),
27
+ 'duration' => Doing.setting('duration'),
28
+ 'interval_format' => Doing.setting('interval_format')
29
+ }, { extend_existing_arrays: true, sort_merged_arrays: true })
30
+
31
+ opt[:duration] ||= cfg['duration'] || false
32
+ opt[:interval_format] ||= cfg['interval_format'] || 'text'
33
+ opt[:count] ||= 0
34
+ opt[:age] ||= :newest
35
+ opt[:age] = opt[:age].normalize_age
36
+ opt[:format] ||= cfg['date_format']
37
+ opt[:order] ||= cfg['order'] || :asc
38
+ opt[:tag_order] ||= :asc
39
+ opt[:tags_color] = cfg['tags_color'] || false if opt[:tags_color].nil?
40
+ opt[:template] ||= cfg['template']
41
+ opt[:sort_tags] ||= opt[:tag_sort]
42
+
43
+ # opt[:highlight] ||= true
44
+ title = ''
45
+ is_single = true
46
+ if opt[:section].nil?
47
+ opt[:section] = choose_section
48
+ title = opt[:section]
49
+ elsif opt[:section].instance_of?(String)
50
+ title = if opt[:section] =~ /^all$/i
51
+ if opt[:page_title]
52
+ opt[:page_title]
53
+ elsif opt[:tag_filter] && opt[:tag_filter]['bool'].normalize_bool != :not
54
+ opt[:tag_filter]['tags'].map { |tag| "@#{tag}" }.join(' + ')
55
+ else
56
+ 'doing'
57
+ end
58
+ else
59
+ guess_section(opt[:section])
60
+ end
61
+ end
62
+
63
+ items = filter_items(items, opt: opt)
64
+
65
+ items.reverse! unless opt[:order].normalize_order == :desc
66
+
67
+ if opt[:delete]
68
+ delete_items(items, force: opt[:force])
69
+
70
+ write(@doing_file)
71
+ return
72
+ elsif opt[:editor]
73
+ edit_items(items)
74
+
75
+ write(@doing_file)
76
+ return
77
+ elsif opt[:interactive]
78
+ opt[:menu] = !opt[:force]
79
+ opt[:query] = '' # opt[:search]
80
+ opt[:multiple] = true
81
+ selected = Prompt.choose_from_items(items.reverse, include_section: opt[:section] =~ /^all$/i, **opt)
82
+
83
+ raise NoResults, 'no items selected' if selected.nil? || selected.empty?
84
+
85
+ act_on(selected, opt)
86
+ return
87
+ end
88
+
89
+ opt[:output] ||= 'template'
90
+ opt[:wrap_width] ||= Doing.setting('templates.default.wrap_width', 0)
91
+
92
+ logger.benchmark(:list_section, :finish)
93
+ output(items, title, is_single, opt)
94
+ end
95
+
96
+ ##
97
+ ## Display entries within a date range
98
+ ##
99
+ ## @param dates [Array] [start, end]
100
+ ## @param section [String] The section
101
+ ## @param times (Bool) Show times
102
+ ## @param output [String] Output format
103
+ ## @param opt [Hash] Additional Options
104
+ ##
105
+ def list_date(dates, section, times = nil, output = nil, opt)
106
+ opt ||= {}
107
+ opt[:totals] ||= false
108
+ opt[:sort_tags] ||= false
109
+ section = guess_section(section)
110
+ # :date_filter expects an array with start and end date
111
+ dates = dates.split_date_range if dates.instance_of?(String)
112
+
113
+ opt[:section] = section
114
+ opt[:count] = 0
115
+ opt[:order] = :asc
116
+ opt[:date_filter] = dates
117
+ opt[:times] = times
118
+ opt[:output] = output
119
+
120
+ time_rx = /^(\d{1,2}+(:\d{1,2}+)?( *(am|pm))?|midnight|noon)$/
121
+ if opt[:from] && opt[:from][0].is_a?(String) && opt[:from][0] =~ time_rx
122
+ opt[:time_filter] = opt[:from]
123
+ end
124
+
125
+ list_section(opt)
126
+ end
127
+
128
+ ##
129
+ ## Show all entries from the current day
130
+ ##
131
+ ## @param times [Boolean] show times
132
+ ## @param output [String] output format
133
+ ## @param opt [Hash] Options
134
+ ##
135
+ def today(times = true, output = nil, opt)
136
+ opt ||= {}
137
+ opt[:totals] ||= false
138
+ opt[:sort_tags] ||= false
139
+
140
+ cfg = Doing.setting('templates').deep_merge(Doing.setting('templates.default'), { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
141
+ 'wrap_width' => Doing.setting('wrap_width') || 0,
142
+ 'date_format' => Doing.setting('default_date_format'),
143
+ 'order' => Doing.setting('order') || :asc,
144
+ 'tags_color' => Doing.setting('tags_color'),
145
+ 'duration' => Doing.setting('duration'),
146
+ 'interval_format' => Doing.setting('interval_format')
147
+ }, { extend_existing_arrays: true, sort_merged_arrays: true })
148
+
149
+ template = opt[:template] || cfg['template']
150
+
151
+ opt[:duration] ||= cfg['duration'] || false
152
+ opt[:interval_format] ||= cfg['interval_format'] || 'text'
153
+
154
+ options = {
155
+ after: opt[:after],
156
+ before: opt[:before],
157
+ count: 0,
158
+ duration: opt[:duration],
159
+ from: opt[:from],
160
+ format: cfg['date_format'],
161
+ interval_format: opt[:interval_format],
162
+ only_timed: opt[:only_timed],
163
+ order: cfg['order'] || :asc,
164
+ output: output,
165
+ section: opt[:section],
166
+ sort_tags: opt[:sort_tags],
167
+ template: template,
168
+ times: times,
169
+ today: true,
170
+ totals: opt[:totals],
171
+ wrap_width: cfg['wrap_width'],
172
+ tags_color: cfg['tags_color'],
173
+ config_template: opt[:config_template]
174
+ }
175
+ list_section(options)
176
+ end
177
+
178
+ ##
179
+ ## Show entries from the previous day
180
+ ##
181
+ ## @param section [String] The section
182
+ ## @param times (Bool) Show times
183
+ ## @param output [String] Output format
184
+ ## @param opt [Hash] Additional Options
185
+ ##
186
+ def yesterday(section, times = nil, output = nil, opt)
187
+ opt ||= {}
188
+ opt[:totals] ||= false
189
+ opt[:sort_tags] ||= false
190
+ opt[:config_template] ||= 'today'
191
+ opt[:yesterday] = true
192
+
193
+ section = guess_section(section)
194
+ y = (Time.now - (60 * 60 * 24)).strftime('%Y-%m-%d')
195
+ opt[:after] = "#{y} #{opt[:after]}" if opt[:after]
196
+ opt[:before] = "#{y} #{opt[:before]}" if opt[:before]
197
+
198
+ opt[:output] = output
199
+ opt[:section] = section
200
+ opt[:times] = times
201
+ opt[:count] = 0
202
+
203
+ list_section(opt)
204
+ end
205
+
206
+ ##
207
+ ## Show recent entries
208
+ ##
209
+ ## @param count [Integer] The number to show
210
+ ## @param section [String] The section to show from, default Currently
211
+ ## @param opt [Hash] Additional Options
212
+ ##
213
+ def recent(count = 10, section = nil, opt)
214
+ opt ||= {}
215
+ times = opt[:t] || true
216
+ opt[:totals] ||= false
217
+ opt[:sort_tags] ||= false
218
+
219
+ cfg = Doing.setting('templates.recent').deep_merge(Doing.setting('templates.default'), { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
220
+ 'wrap_width' => Doing.setting('wrap_width') || 0,
221
+ 'date_format' => Doing.setting('default_date_format'),
222
+ 'order' => Doing.setting('order') || :asc,
223
+ 'tags_color' => Doing.setting('tags_color'),
224
+ 'duration' => Doing.setting('duration'),
225
+ 'interval_format' => Doing.setting('interval_format')
226
+ }, { extend_existing_arrays: true, sort_merged_arrays: true })
227
+ opt[:duration] ||= cfg['duration'] || false
228
+ opt[:interval_format] ||= cfg['interval_format'] || 'text'
229
+
230
+ section ||= Doing.setting('current_section')
231
+ section = guess_section(section)
232
+
233
+ opt[:section] = section
234
+ opt[:wrap_width] = cfg['wrap_width']
235
+ opt[:count] = count
236
+ opt[:format] = cfg['date_format']
237
+ opt[:template] = opt[:template] || cfg['template']
238
+ opt[:order] = :asc
239
+ opt[:times] = times
240
+
241
+ list_section(opt)
242
+ end
243
+
244
+ ##
245
+ ## Show the last entry
246
+ ##
247
+ ## @param times (Bool) Show times
248
+ ## @param section [String] Section to pull from, default Currently
249
+ ##
250
+ def last(times: true, section: nil, options: {})
251
+ section = section.nil? || section =~ /all/i ? 'All' : guess_section(section)
252
+ cfg = Doing.setting(['templates', options[:config_template]]).deep_merge(Doing.setting('templates.default'), { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
253
+ 'wrap_width' => Doing.setting('wrap_width', 0),
254
+ 'date_format' => Doing.setting('default_date_format'),
255
+ 'order' => Doing.setting('order', :asc),
256
+ 'tags_color' => Doing.setting('tags_color'),
257
+ 'duration' => Doing.setting('duration'),
258
+ 'interval_format' => Doing.setting('interval_format')
259
+ }, { extend_existing_arrays: true, sort_merged_arrays: true })
260
+ options[:duration] ||= cfg['duration'] || false
261
+ options[:interval_format] ||= cfg['interval_format'] || 'text'
262
+
263
+ opts = {
264
+ case: options[:case],
265
+ config_template: options[:config_template] || 'last',
266
+ count: 1,
267
+ delete: options[:delete],
268
+ duration: options[:duration],
269
+ format: cfg['date_format'],
270
+ interval_format: options[:interval_format],
271
+ not: options[:negate],
272
+ output: options[:output],
273
+ section: section,
274
+ template: options[:template] || cfg['template'],
275
+ times: times,
276
+ val: options[:val],
277
+ wrap_width: cfg['wrap_width']
278
+ }
279
+
280
+ if options[:tag]
281
+ opts[:tag_filter] = {
282
+ 'tags' => options[:tag],
283
+ 'bool' => options[:tag_bool]
284
+ }
285
+ end
286
+
287
+ opts[:search] = options[:search] if options[:search]
288
+
289
+ list_section(opts)
290
+ end
291
+
292
+ ##
293
+ ## Return the content of the last note for a given section
294
+ ##
295
+ ## @param section [String] The section to retrieve from, default
296
+ ## All
297
+ ##
298
+ def last_note(section = 'All')
299
+ section = guess_section(section)
300
+
301
+ last_item = last_entry({ section: section })
302
+
303
+ raise NoEntryError, 'No entry found' unless last_item
304
+
305
+ logger.log_now(:info, 'Edit note:', last_item.title)
306
+
307
+ note = last_item.note&.to_s || ''
308
+ "#{last_item.title}\n# EDIT BELOW THIS LINE ------------\n#{note}"
309
+ end
310
+
311
+ ##
312
+ ## Get the last entry
313
+ ##
314
+ ## @param opt [Hash] Additional Options
315
+ ##
316
+ def last_entry(opt)
317
+ opt ||= {}
318
+ opt[:tag_bool] ||= :and
319
+ opt[:section] ||= Doing.setting('current_section')
320
+
321
+ items = filter_items(Items.new, opt: opt)
322
+
323
+ logger.debug('Filtered:', "Parameters matched #{items.count} entries")
324
+
325
+ if opt[:interactive]
326
+ last_entry = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i,
327
+ menu: true,
328
+ header: '',
329
+ prompt: 'Select an entry > ',
330
+ multiple: false,
331
+ sort: false,
332
+ show_if_single: true
333
+ )
334
+ else
335
+ last_entry = items.max_by { |item| item.date }
336
+ end
337
+
338
+ last_entry
339
+ end
340
+
341
+ private
342
+
343
+ ##
344
+ ## Generate output using available export plugins
345
+ ##
346
+ ## @param items [Array] The items
347
+ ## @param title [String] Page title
348
+ ## @param is_single [Boolean] Indicates if single
349
+ ## section
350
+ ## @param opt [Hash] Additional options
351
+ ##
352
+ ## @return [String] formatted output based on opt[:output]
353
+ ## template trigger
354
+ ## @api private
355
+ def output(items, title, is_single, opt)
356
+ logger.benchmark(:output, :start)
357
+ opt ||= {}
358
+ out = nil
359
+
360
+ raise InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
361
+
362
+ export_options = { page_title: title, is_single: is_single, options: opt }
363
+
364
+ Hooks.trigger :pre_export, self, opt[:output], items
365
+
366
+ Plugins.plugins[:export].each do |_, options|
367
+ next unless opt[:output] =~ /^(#{options[:trigger].normalize_trigger})$/i
368
+
369
+ out = options[:class].render(self, items, variables: export_options)
370
+ break
371
+ end
372
+
373
+ logger.debug('Output:', "#{items.count} #{items.count == 1 ? 'item' : 'items'} shown")
374
+ logger.benchmark(:output, :finish)
375
+ out
376
+ end
377
+
378
+ ##
379
+ ## Get next item in the index
380
+ ##
381
+ ## @param item [Item] target item
382
+ ## @param options [Hash] additional options
383
+ ## @see #filter_items
384
+ ##
385
+ ## @return [Item] the next chronological item in the index
386
+ ##
387
+ def next_item(item, options = {})
388
+ options ||= {}
389
+ items = filter_items(Items.new, opt: options)
390
+
391
+ idx = items.index(item)
392
+
393
+ idx.positive? ? items[idx - 1] : nil
394
+ end
395
+ end
396
+ end