doing 2.1.37 → 2.1.40

Sign up to get free protection for your applications and to get access to all the features.
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