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
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