doing 2.1.37 → 2.1.40

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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/Rakefile +7 -1
  6. data/bin/commands/config.rb +43 -34
  7. data/bin/commands/done.rb +1 -18
  8. data/bin/commands/finish.rb +30 -25
  9. data/bin/commands/grep.rb +3 -14
  10. data/bin/commands/last.rb +2 -8
  11. data/bin/commands/meanwhile.rb +13 -6
  12. data/bin/commands/now.rb +2 -4
  13. data/bin/commands/on.rb +4 -15
  14. data/bin/commands/recent.rb +2 -8
  15. data/bin/commands/reset.rb +24 -1
  16. data/bin/commands/select.rb +1 -1
  17. data/bin/commands/show.rb +8 -16
  18. data/bin/commands/since.rb +1 -12
  19. data/bin/commands/today.rb +2 -13
  20. data/bin/commands/view.rb +1 -1
  21. data/bin/commands/yesterday.rb +2 -13
  22. data/bin/doing +41 -36
  23. data/docs/doc/Array.html +1 -1
  24. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  25. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  26. data/docs/doc/BooleanTermParser/Query.html +1 -1
  27. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  28. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  29. data/docs/doc/BooleanTermParser.html +1 -1
  30. data/docs/doc/Doing/Color.html +166 -20
  31. data/docs/doc/Doing/Completion.html +1 -1
  32. data/docs/doc/Doing/Configuration.html +1 -1
  33. data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
  34. data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
  35. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  36. data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
  37. data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
  38. data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
  39. data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
  40. data/docs/doc/Doing/Errors/NoResults.html +10 -2
  41. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  42. data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
  43. data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
  44. data/docs/doc/Doing/Errors.html +9 -9
  45. data/docs/doc/Doing/Hooks.html +1 -1
  46. data/docs/doc/Doing/Item.html +114 -1576
  47. data/docs/doc/Doing/Items.html +121 -5
  48. data/docs/doc/Doing/Logger.html +1 -1
  49. data/docs/doc/Doing/Note.html +1 -1
  50. data/docs/doc/Doing/Pager.html +1 -1
  51. data/docs/doc/Doing/Plugins.html +1 -1
  52. data/docs/doc/Doing/Prompt.html +2 -2
  53. data/docs/doc/Doing/Section.html +1 -1
  54. data/docs/doc/Doing/TemplateString.html +2 -2
  55. data/docs/doc/Doing/Types.html +1 -1
  56. data/docs/doc/Doing/Util/Backup.html +5 -5
  57. data/docs/doc/Doing/Util.html +1 -1
  58. data/docs/doc/Doing/WWID.html +197 -4033
  59. data/docs/doc/Doing.html +2 -2
  60. data/docs/doc/FalseClass.html +1 -1
  61. data/docs/doc/GLI/Commands/Help.html +1 -1
  62. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  63. data/docs/doc/GLI/Commands.html +1 -1
  64. data/docs/doc/GLI.html +1 -1
  65. data/docs/doc/Hash.html +1 -1
  66. data/docs/doc/Object.html +1 -1
  67. data/docs/doc/PhraseParser/Operator.html +1 -1
  68. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  69. data/docs/doc/PhraseParser/Query.html +1 -1
  70. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  71. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  72. data/docs/doc/PhraseParser/TermClause.html +1 -1
  73. data/docs/doc/PhraseParser.html +1 -1
  74. data/docs/doc/Status.html +1 -1
  75. data/docs/doc/String.html +1 -1
  76. data/docs/doc/Symbol.html +1 -1
  77. data/docs/doc/Time.html +1 -1
  78. data/docs/doc/TrueClass.html +1 -1
  79. data/docs/doc/_index.html +26 -5
  80. data/docs/doc/class_list.html +1 -1
  81. data/docs/doc/file.README.html +2 -2
  82. data/docs/doc/index.html +2 -2
  83. data/docs/doc/method_list.html +237 -709
  84. data/docs/doc/top-level-namespace.html +3 -3
  85. data/docs/index.md +1 -1
  86. data/doing.rdoc +54 -7
  87. data/lib/completion/_doing.zsh +6 -6
  88. data/lib/completion/doing.bash +10 -10
  89. data/lib/completion/doing.fish +8 -2
  90. data/lib/doing/add_options.rb +31 -1
  91. data/lib/doing/chronify/array.rb +68 -18
  92. data/lib/doing/chronify/string.rb +3 -1
  93. data/lib/doing/colors.rb +77 -30
  94. data/lib/doing/completion.rb +4 -5
  95. data/lib/doing/errors.rb +51 -35
  96. data/lib/doing/hooks.rb +3 -3
  97. data/lib/doing/item/dates.rb +112 -0
  98. data/lib/doing/item/query.rb +433 -0
  99. data/lib/doing/item/state.rb +59 -0
  100. data/lib/doing/item/tags.rb +87 -0
  101. data/lib/doing/item.rb +6 -537
  102. data/lib/doing/items.rb +39 -14
  103. data/lib/doing/plugin_manager.rb +3 -3
  104. data/lib/doing/plugins/export/template_export.rb +4 -4
  105. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  106. data/lib/doing/prompt.rb +6 -8
  107. data/lib/doing/string/tags.rb +8 -2
  108. data/lib/doing/util_backup.rb +6 -8
  109. data/lib/doing/version.rb +1 -1
  110. data/lib/doing/wwid/display.rb +399 -0
  111. data/lib/doing/wwid/editor.rb +214 -0
  112. data/lib/doing/wwid/filetools.rb +186 -0
  113. data/lib/doing/wwid/filter.rb +218 -0
  114. data/lib/doing/wwid/guess.rb +87 -0
  115. data/lib/doing/wwid/interactive.rb +385 -0
  116. data/lib/doing/wwid/modify.rb +618 -0
  117. data/lib/doing/wwid/tags.rb +54 -0
  118. data/lib/doing/wwid/timers.rb +345 -0
  119. data/lib/doing/wwid/wwidutil.rb +104 -0
  120. data/lib/doing/wwid.rb +31 -2308
  121. metadata +19 -2
@@ -0,0 +1,345 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ class WWID
5
+ # Timer methods for WWID class
6
+ module Timers
7
+ ##
8
+ ## Get total elapsed time for all tags in
9
+ ## selection
10
+ ##
11
+ ## @param format [String] return format (html,
12
+ ## json, or text)
13
+ ## @param sort_by [Symbol] Sort by :name or :time
14
+ ## @param sort_order [Symbol] The sort order (:asc or :desc)
15
+ ##
16
+ def tag_times(format: :text, sort_by: :time, sort_order: :asc)
17
+ return '' if @timers.empty?
18
+
19
+ max = @timers.keys.sort_by(&:length).reverse[0].length + 1
20
+
21
+ total = @timers.delete('All')
22
+
23
+ tags_data = @timers.delete_if { |_k, v| v.zero? }
24
+ sorted_tags_data = if sort_by.normalize_tag_sort == :name
25
+ tags_data.sort_by { |k, _v| k }
26
+ else
27
+ tags_data.sort_by { |_k, v| v }
28
+ end
29
+
30
+ sorted_tags_data.reverse! if sort_order.normalize_order == :asc
31
+ case format
32
+ when :html
33
+
34
+ output = <<EOHEAD
35
+ <table>
36
+ <caption id="tagtotals">Tag Totals</caption>
37
+ <colgroup>
38
+ <col style="text-align:left;"/>
39
+ <col style="text-align:left;"/>
40
+ </colgroup>
41
+ <thead>
42
+ <tr>
43
+ <th style="text-align:left;">project</th>
44
+ <th style="text-align:left;">time</th>
45
+ </tr>
46
+ </thead>
47
+ <tbody>
48
+ EOHEAD
49
+ sorted_tags_data.reverse.each do |k, v|
50
+ if v.positive?
51
+ output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{v.time_string(format: :clock)}</td></tr>\n"
52
+ end
53
+ end
54
+ tail = <<EOTAIL
55
+ <tr>
56
+ <td style="text-align:left;" colspan="2"></td>
57
+ </tr>
58
+ </tbody>
59
+ <tfoot>
60
+ <tr>
61
+ <td style="text-align:left;"><strong>Total</strong></td>
62
+ <td style="text-align:left;">#{total.time_string(format: :clock)}</td>
63
+ </tr>
64
+ </tfoot>
65
+ </table>
66
+ EOTAIL
67
+ output + tail
68
+ when :markdown
69
+ pad = sorted_tags_data.map { |k, _| k }.group_by(&:size).max.last[0].length
70
+ pad = 7 if pad < 7
71
+ output = <<~EOHEADER
72
+ | #{' ' * (pad - 7)}project | time |
73
+ | #{'-' * (pad - 1)}: | :------- |
74
+ EOHEADER
75
+ sorted_tags_data.reverse.each do |k, v|
76
+ if v.positive?
77
+ output += "| #{' ' * (pad - k.length)}#{k} | #{v.time_string(format: :clock)} |\n"
78
+ end
79
+ end
80
+ tail = '[Tag Totals]'
81
+ output + tail
82
+ when :json
83
+ output = []
84
+ sorted_tags_data.reverse.each do |k, v|
85
+ output << {
86
+ 'tag' => k,
87
+ 'seconds' => v,
88
+ 'formatted' => v.time_string(format: :clock)
89
+ }
90
+ end
91
+ output
92
+ when :human
93
+ output = []
94
+ sorted_tags_data.reverse.each do |k, v|
95
+ spacer = ''
96
+ (max - k.length).times do
97
+ spacer += ' '
98
+ end
99
+ output.push("┃ #{spacer}#{k}:#{v.time_string(format: :hm)} ┃")
100
+ end
101
+
102
+ header = '┏━━ Tag Totals '
103
+ (max - 2).times { header += '━' }
104
+ header += '┓'
105
+ footer = '┗'
106
+ (max + 12).times { footer += '━' }
107
+ footer += '┛'
108
+ divider = '┣'
109
+ (max + 12).times { divider += '━' }
110
+ divider += '┫'
111
+ output = output.empty? ? '' : "\n#{header}\n#{output.join("\n")}"
112
+ output += "\n#{divider}"
113
+ spacer = ''
114
+ (max - 6).times do
115
+ spacer += ' '
116
+ end
117
+ total_time = total.time_string(format: :hm)
118
+ total = "┃ #{spacer}total: "
119
+ total += total_time
120
+ total += ' ┃'
121
+ output += "\n#{total}"
122
+ output += "\n#{footer}"
123
+ output
124
+ else
125
+ output = []
126
+ sorted_tags_data.reverse.each do |k, v|
127
+ spacer = ''
128
+ (max - k.length).times do
129
+ spacer += ' '
130
+ end
131
+ output.push("#{k}:#{spacer}#{v.time_string(format: :clock)}")
132
+ end
133
+
134
+ output = output.empty? ? '' : "\n--- Tag Totals ---\n#{output.join("\n")}"
135
+ output += "\n\nTotal tracked: #{total.time_string(format: :clock)}\n"
136
+ output
137
+ end
138
+ end
139
+
140
+ ##
141
+ ## Gets the interval between entry's start
142
+ ## date and @done date
143
+ ##
144
+ ## @param item [Item] The entry
145
+ ## @param formatted [Boolean] Return human readable
146
+ ## time (default seconds)
147
+ ## @param record [Boolean] Add the interval to the
148
+ ## total for each tag
149
+ ##
150
+ ## @return Interval in seconds, or [d, h, m] array if
151
+ ## formatted is true. False if no end date or
152
+ ## interval is 0
153
+ ##
154
+ def get_interval(item, formatted: true, record: true)
155
+ if item.interval
156
+ seconds = item.interval
157
+ record_tag_times(item, seconds) if record
158
+ return seconds.positive? ? seconds : false unless formatted
159
+
160
+ return seconds.positive? ? seconds.time_string(format: :clock) : false
161
+ end
162
+
163
+ false
164
+ end##
165
+ ## Get total elapsed time for all tags in
166
+ ## selection
167
+ ##
168
+ ## @param format [String] return format (html,
169
+ ## json, or text)
170
+ ## @param sort_by [Symbol] Sort by :name or :time
171
+ ## @param sort_order [Symbol] The sort order (:asc or :desc)
172
+ ##
173
+ def tag_times(format: :text, sort_by: :time, sort_order: :asc)
174
+ return '' if @timers.empty?
175
+
176
+ max = @timers.keys.sort_by(&:length).reverse[0].length + 1
177
+
178
+ total = @timers.delete('All')
179
+
180
+ tags_data = @timers.delete_if { |_k, v| v.zero? }
181
+ sorted_tags_data = if sort_by.normalize_tag_sort == :name
182
+ tags_data.sort_by { |k, _v| k }
183
+ else
184
+ tags_data.sort_by { |_k, v| v }
185
+ end
186
+
187
+ sorted_tags_data.reverse! if sort_order.normalize_order == :asc
188
+ case format
189
+ when :html
190
+
191
+ output = <<EOHEAD
192
+ <table>
193
+ <caption id="tagtotals">Tag Totals</caption>
194
+ <colgroup>
195
+ <col style="text-align:left;"/>
196
+ <col style="text-align:left;"/>
197
+ </colgroup>
198
+ <thead>
199
+ <tr>
200
+ <th style="text-align:left;">project</th>
201
+ <th style="text-align:left;">time</th>
202
+ </tr>
203
+ </thead>
204
+ <tbody>
205
+ EOHEAD
206
+ sorted_tags_data.reverse.each do |k, v|
207
+ if v.positive?
208
+ output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{v.time_string(format: :clock)}</td></tr>\n"
209
+ end
210
+ end
211
+ tail = <<EOTAIL
212
+ <tr>
213
+ <td style="text-align:left;" colspan="2"></td>
214
+ </tr>
215
+ </tbody>
216
+ <tfoot>
217
+ <tr>
218
+ <td style="text-align:left;"><strong>Total</strong></td>
219
+ <td style="text-align:left;">#{total.time_string(format: :clock)}</td>
220
+ </tr>
221
+ </tfoot>
222
+ </table>
223
+ EOTAIL
224
+ output + tail
225
+ when :markdown
226
+ pad = sorted_tags_data.map { |k, _| k }.group_by(&:size).max.last[0].length
227
+ pad = 7 if pad < 7
228
+ output = <<~EOHEADER
229
+ | #{' ' * (pad - 7)}project | time |
230
+ | #{'-' * (pad - 1)}: | :------- |
231
+ EOHEADER
232
+ sorted_tags_data.reverse.each do |k, v|
233
+ if v.positive?
234
+ output += "| #{' ' * (pad - k.length)}#{k} | #{v.time_string(format: :clock)} |\n"
235
+ end
236
+ end
237
+ tail = '[Tag Totals]'
238
+ output + tail
239
+ when :json
240
+ output = []
241
+ sorted_tags_data.reverse.each do |k, v|
242
+ output << {
243
+ 'tag' => k,
244
+ 'seconds' => v,
245
+ 'formatted' => v.time_string(format: :clock)
246
+ }
247
+ end
248
+ output
249
+ when :human
250
+ output = []
251
+ sorted_tags_data.reverse.each do |k, v|
252
+ spacer = ''
253
+ (max - k.length).times do
254
+ spacer += ' '
255
+ end
256
+ output.push("┃ #{spacer}#{k}:#{v.time_string(format: :hm)} ┃")
257
+ end
258
+
259
+ header = '┏━━ Tag Totals '
260
+ (max - 2).times { header += '━' }
261
+ header += '┓'
262
+ footer = '┗'
263
+ (max + 12).times { footer += '━' }
264
+ footer += '┛'
265
+ divider = '┣'
266
+ (max + 12).times { divider += '━' }
267
+ divider += '┫'
268
+ output = output.empty? ? '' : "\n#{header}\n#{output.join("\n")}"
269
+ output += "\n#{divider}"
270
+ spacer = ''
271
+ (max - 6).times do
272
+ spacer += ' '
273
+ end
274
+ total_time = total.time_string(format: :hm)
275
+ total = "┃ #{spacer}total: "
276
+ total += total_time
277
+ total += ' ┃'
278
+ output += "\n#{total}"
279
+ output += "\n#{footer}"
280
+ output
281
+ else
282
+ output = []
283
+ sorted_tags_data.reverse.each do |k, v|
284
+ spacer = ''
285
+ (max - k.length).times do
286
+ spacer += ' '
287
+ end
288
+ output.push("#{k}:#{spacer}#{v.time_string(format: :clock)}")
289
+ end
290
+
291
+ output = output.empty? ? '' : "\n--- Tag Totals ---\n#{output.join("\n")}"
292
+ output += "\n\nTotal tracked: #{total.time_string(format: :clock)}\n"
293
+ output
294
+ end
295
+ end
296
+
297
+ ##
298
+ ## Gets the interval between entry's start
299
+ ## date and @done date
300
+ ##
301
+ ## @param item [Item] The entry
302
+ ## @param formatted [Boolean] Return human readable
303
+ ## time (default seconds)
304
+ ## @param record [Boolean] Add the interval to the
305
+ ## total for each tag
306
+ ##
307
+ ## @return Interval in seconds, or [d, h, m] array if
308
+ ## formatted is true. False if no end date or
309
+ ## interval is 0
310
+ ##
311
+ def get_interval(item, formatted: true, record: true)
312
+ if item.interval
313
+ seconds = item.interval
314
+ record_tag_times(item, seconds) if record
315
+ return seconds.positive? ? seconds : false unless formatted
316
+
317
+ return seconds.positive? ? seconds.time_string(format: :clock) : false
318
+ end
319
+
320
+ false
321
+ end
322
+
323
+ private
324
+
325
+ ##
326
+ ## Record times for item tags
327
+ ##
328
+ ## @param item [Item] The item to record
329
+ ##
330
+ def record_tag_times(item, seconds)
331
+ item_hash = "#{item.date.strftime('%s')}#{item.title}#{item.section}"
332
+ return if @recorded_items.include?(item_hash)
333
+ item.title.scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
334
+ k = m[0] == 'done' ? 'All' : m[0].downcase
335
+ if @timers.key?(k)
336
+ @timers[k] += seconds
337
+ else
338
+ @timers[k] = seconds
339
+ end
340
+ @recorded_items.push(item_hash)
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ class WWID
5
+ # Util methods for WWID class
6
+ module WWIDUtil
7
+ ##
8
+ ## Remove items from an array that already exist in
9
+ ## @content based on start and end times
10
+ ##
11
+ ## @param items [Array] The items to
12
+ ## deduplicate
13
+ ## @param no_overlap [Boolean] Remove items with
14
+ ## overlapping time spans
15
+ ##
16
+ def dedup(items, no_overlap: false)
17
+ items.delete_if do |item|
18
+ duped = false
19
+ @content.each do |comp|
20
+ duped = no_overlap ? item.overlapping_time?(comp) : item.same_time?(comp)
21
+ break if duped
22
+ end
23
+ logger.count(:skipped, level: :debug, message: '%count overlapping %items') if duped
24
+ # logger.log_now(:debug, 'Skipped:', "overlapping entry: #{item.title}") if duped
25
+ duped
26
+ end
27
+ end
28
+
29
+ ##
30
+ ## Imports external entries
31
+ ##
32
+ ## @param paths [String] Path to JSON report file
33
+ ## @param opt [Hash] Additional Options
34
+ ##
35
+ def import(paths, opt)
36
+ opt ||= {}
37
+ Plugins.plugins[:import].each do |_, options|
38
+ next unless opt[:type] =~ /^(#{options[:trigger].normalize_trigger})$/i
39
+
40
+ if paths.count.positive?
41
+ paths.each do |path|
42
+ options[:class].import(self, path, options: opt)
43
+ end
44
+ else
45
+ options[:class].import(self, nil, options: opt)
46
+ end
47
+ break
48
+ end
49
+ end
50
+
51
+ ##
52
+ ## Load configuration files and updated the @settings
53
+ ## attribute with a Doing::Configuration object
54
+ ##
55
+ ## @param filename [String] (optional) path to
56
+ ## alternative config file
57
+ ##
58
+ def configure(filename = nil)
59
+ logger.benchmark(:configure, :start)
60
+
61
+ if filename
62
+ Doing.config_with(filename, { ignore_local: true })
63
+ elsif ENV['DOING_CONFIG']
64
+ Doing.config_with(ENV['DOING_CONFIG'], { ignore_local: true })
65
+ end
66
+
67
+ logger.benchmark(:configure, :finish)
68
+
69
+ Doing.set('backup_dir', ENV['DOING_BACKUP_DIR']) if ENV['DOING_BACKUP_DIR']
70
+ end
71
+
72
+ ##
73
+ ## Get difference between current content and last backup
74
+ ##
75
+ ## @param filename [String] The file path
76
+ ##
77
+ def get_diff(filename = nil)
78
+ configure if Doing.settings.nil?
79
+
80
+ filename ||= Doing.setting('doing_file')
81
+ init_doing_file(filename)
82
+ current_content = @content.clone
83
+ backup_file = Util::Backup.last_backup(filename, count: 1)
84
+ raise DoingRuntimeError, 'No undo history to diff' if backup_file.nil?
85
+
86
+ backup = WWID.new
87
+ backup.config = Doing.settings
88
+ backup.init_doing_file(backup_file)
89
+ current_content.diff(backup.content)
90
+ end
91
+
92
+ ##
93
+ ## Return a hash of changes between initial file read
94
+ ## and current Items object
95
+ ##
96
+ ## @return [Hash] Hash containing `added` and
97
+ ## `removed` keys with arrays of Item
98
+ ##
99
+ def changes
100
+ @content.diff(@initial_content)
101
+ end
102
+ end
103
+ end
104
+ end