doing 2.1.39 → 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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/bin/commands/config.rb +43 -34
  6. data/bin/commands/done.rb +1 -18
  7. data/bin/commands/finish.rb +30 -25
  8. data/bin/commands/grep.rb +3 -14
  9. data/bin/commands/last.rb +2 -8
  10. data/bin/commands/meanwhile.rb +13 -6
  11. data/bin/commands/on.rb +3 -16
  12. data/bin/commands/recent.rb +2 -8
  13. data/bin/commands/reset.rb +24 -1
  14. data/bin/commands/select.rb +1 -1
  15. data/bin/commands/show.rb +6 -17
  16. data/bin/commands/since.rb +1 -12
  17. data/bin/commands/today.rb +2 -13
  18. data/bin/commands/view.rb +1 -1
  19. data/bin/commands/yesterday.rb +2 -13
  20. data/bin/doing +15 -8
  21. data/docs/doc/Array.html +1 -1
  22. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  23. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  24. data/docs/doc/BooleanTermParser/Query.html +1 -1
  25. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  26. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  27. data/docs/doc/BooleanTermParser.html +1 -1
  28. data/docs/doc/Doing/Color.html +166 -20
  29. data/docs/doc/Doing/Completion.html +1 -1
  30. data/docs/doc/Doing/Configuration.html +1 -1
  31. data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
  32. data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
  33. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  34. data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
  35. data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
  36. data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
  37. data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
  38. data/docs/doc/Doing/Errors/NoResults.html +10 -2
  39. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  40. data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
  41. data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
  42. data/docs/doc/Doing/Errors.html +9 -9
  43. data/docs/doc/Doing/Hooks.html +1 -1
  44. data/docs/doc/Doing/Item.html +90 -1615
  45. data/docs/doc/Doing/Items.html +121 -5
  46. data/docs/doc/Doing/Logger.html +1 -1
  47. data/docs/doc/Doing/Note.html +1 -1
  48. data/docs/doc/Doing/Pager.html +1 -1
  49. data/docs/doc/Doing/Plugins.html +1 -1
  50. data/docs/doc/Doing/Prompt.html +2 -2
  51. data/docs/doc/Doing/Section.html +1 -1
  52. data/docs/doc/Doing/TemplateString.html +2 -2
  53. data/docs/doc/Doing/Types.html +1 -1
  54. data/docs/doc/Doing/Util/Backup.html +5 -5
  55. data/docs/doc/Doing/Util.html +1 -1
  56. data/docs/doc/Doing/WWID.html +197 -4033
  57. data/docs/doc/Doing.html +2 -2
  58. data/docs/doc/FalseClass.html +1 -1
  59. data/docs/doc/GLI/Commands/Help.html +1 -1
  60. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  61. data/docs/doc/GLI/Commands.html +1 -1
  62. data/docs/doc/GLI.html +1 -1
  63. data/docs/doc/Hash.html +1 -1
  64. data/docs/doc/Object.html +1 -1
  65. data/docs/doc/PhraseParser/Operator.html +1 -1
  66. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  67. data/docs/doc/PhraseParser/Query.html +1 -1
  68. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  69. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  70. data/docs/doc/PhraseParser/TermClause.html +1 -1
  71. data/docs/doc/PhraseParser.html +1 -1
  72. data/docs/doc/Status.html +1 -1
  73. data/docs/doc/String.html +1 -1
  74. data/docs/doc/Symbol.html +1 -1
  75. data/docs/doc/Time.html +1 -1
  76. data/docs/doc/TrueClass.html +1 -1
  77. data/docs/doc/_index.html +26 -5
  78. data/docs/doc/class_list.html +1 -1
  79. data/docs/doc/file.README.html +2 -2
  80. data/docs/doc/index.html +2 -2
  81. data/docs/doc/method_list.html +293 -773
  82. data/docs/doc/top-level-namespace.html +3 -3
  83. data/docs/index.md +1 -1
  84. data/doing.rdoc +49 -7
  85. data/lib/completion/_doing.zsh +5 -5
  86. data/lib/completion/doing.bash +8 -8
  87. data/lib/completion/doing.fish +7 -2
  88. data/lib/doing/add_options.rb +31 -1
  89. data/lib/doing/chronify/array.rb +64 -22
  90. data/lib/doing/colors.rb +77 -30
  91. data/lib/doing/completion.rb +4 -5
  92. data/lib/doing/errors.rb +51 -35
  93. data/lib/doing/hooks.rb +3 -3
  94. data/lib/doing/item/dates.rb +112 -0
  95. data/lib/doing/item/query.rb +433 -0
  96. data/lib/doing/item/state.rb +59 -0
  97. data/lib/doing/item/tags.rb +87 -0
  98. data/lib/doing/item.rb +6 -667
  99. data/lib/doing/items.rb +38 -13
  100. data/lib/doing/plugin_manager.rb +3 -3
  101. data/lib/doing/plugins/export/template_export.rb +4 -4
  102. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  103. data/lib/doing/util_backup.rb +6 -8
  104. data/lib/doing/version.rb +1 -1
  105. data/lib/doing/wwid/display.rb +399 -0
  106. data/lib/doing/wwid/editor.rb +214 -0
  107. data/lib/doing/wwid/filetools.rb +186 -0
  108. data/lib/doing/wwid/filter.rb +218 -0
  109. data/lib/doing/wwid/guess.rb +87 -0
  110. data/lib/doing/wwid/interactive.rb +385 -0
  111. data/lib/doing/wwid/modify.rb +618 -0
  112. data/lib/doing/wwid/tags.rb +54 -0
  113. data/lib/doing/wwid/timers.rb +345 -0
  114. data/lib/doing/wwid/wwidutil.rb +104 -0
  115. data/lib/doing/wwid.rb +31 -2317
  116. metadata +19 -2
@@ -0,0 +1,385 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ class WWID
5
+ # Interactive methods for WWID class
6
+ module Interactive
7
+ ##
8
+ ## Display an interactive menu of entries
9
+ ##
10
+ ## @param opt [Hash] Additional options
11
+ ##
12
+ ## Options hash is shared with #filter_items and #act_on
13
+ ##
14
+ def interactive(opt)
15
+ opt ||= {}
16
+ opt[:section] = opt[:section] ? guess_section(opt[:section]) : 'All'
17
+
18
+ search = nil
19
+
20
+ if opt[:search]
21
+ search = opt[:search]
22
+ search.sub!(/^'?/, "'") if opt[:exact]
23
+ opt[:search] = search
24
+ end
25
+
26
+ # opt[:query] = opt[:search] if opt[:search] && !opt[:query]
27
+ opt[:query] = "!#{opt[:query]}" if opt[:query] && opt[:not]
28
+ opt[:multiple] = true
29
+ opt[:show_if_single] = true
30
+ filter_options = %i[after before case date_filter from fuzzy not search section val].each_with_object({}) {
31
+ |k, hsh| hsh[k] = opt[k]
32
+ }
33
+ items = filter_items(Items.new, opt: filter_options)
34
+
35
+ menu_options = %i[search query exact multiple show_if_single menu sort case].each_with_object({}) {
36
+ |k, hsh| hsh[k] = opt[k]
37
+ }
38
+
39
+ selection = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i, **menu_options)
40
+
41
+ raise NoResults, 'no items selected' if selection.nil? || selection.empty?
42
+
43
+ act_on(selection, opt)
44
+ end
45
+
46
+ ##
47
+ ## Perform actions on a set of entries. If
48
+ ## no valid action is included in the opt
49
+ ## hash and the terminal is a TTY, a menu
50
+ ## will be presented
51
+ ##
52
+ ## @param items [Array] Array of Items to affect
53
+ ## @param opt [Hash] Options and actions to perform
54
+ ##
55
+ ## @option opt [Boolean] :editor
56
+ ## @option opt [Boolean] :delete
57
+ ## @option opt [String] :tag
58
+ ## @option opt [Boolean] :flag
59
+ ## @option opt [Boolean] :finish
60
+ ## @option opt [Boolean] :cancel
61
+ ## @option opt [Boolean] :archive
62
+ ## @option opt [String] :output
63
+ ## @option opt [String] :save_to
64
+ ## @option opt [Boolean] :again
65
+ ## @option opt [Boolean] :resume
66
+ ##
67
+ def act_on(items, opt)
68
+ opt ||= {}
69
+ actions = %i[editor delete tag flag finish cancel archive output save_to again resume]
70
+ has_action = false
71
+ single = items.count == 1
72
+
73
+ actions.each do |a|
74
+ if opt[a]
75
+ has_action = true
76
+ break
77
+ end
78
+ end
79
+
80
+ unless has_action
81
+ actions = [
82
+ 'add tag',
83
+ 'remove tag',
84
+ 'autotag',
85
+ 'cancel',
86
+ 'delete',
87
+ 'finish',
88
+ 'flag',
89
+ 'archive',
90
+ 'move',
91
+ 'edit',
92
+ 'output formatted'
93
+ ]
94
+
95
+ actions.concat(['resume/repeat', 'begin/reset']) if items.count == 1
96
+
97
+ choice = Prompt.choose_from(actions,
98
+ prompt: 'What do you want to do with the selected items? > ',
99
+ multiple: true,
100
+ sorted: false,
101
+ fzf_args: ["--height=#{actions.count + 3}", '--tac', '--no-sort', '--info=hidden'])
102
+ return unless choice
103
+
104
+ to_do = choice.strip.split(/\n/)
105
+ to_do.each do |action|
106
+ case action
107
+ when /resume/
108
+ opt[:resume] = true
109
+ when /reset/
110
+ opt[:reset] = true
111
+ when /autotag/
112
+ opt[:autotag] = true
113
+ when /(add|remove) tag/
114
+ type = action =~ /^add/ ? 'add' : 'remove'
115
+ raise InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
116
+
117
+ tags = type == 'add' ? all_tags(@content) : all_tags(items)
118
+
119
+ puts "#{yellow}Separate multiple tags with spaces, hit tab to complete known tags#{type == 'add' ? ', include values with tag(value)' : ''}"
120
+ puts "#{boldgreen}Available tags: #{boldwhite}#{tags.sort.map(&:add_at).join(', ')}" if type == 'remove'
121
+ tag = Prompt.read_line(prompt: "Tags to #{type}", completions: tags)
122
+
123
+ # print "#{yellow("Tag to #{type}: ")}#{reset}"
124
+ # tag = $stdin.gets
125
+ next if tag =~ /^ *$/
126
+
127
+ opt[:tag] = tag.strip.sub(/^@/, '')
128
+ opt[:remove] = true if type == 'remove'
129
+ when /output formatted/
130
+ plugins = Plugins.available_plugins(type: :export).sort
131
+ output_format = Prompt.choose_from(plugins,
132
+ prompt: 'Which output format? > ',
133
+ fzf_args: [
134
+ "--height=#{plugins.count + 3}",
135
+ '--tac',
136
+ '--no-sort',
137
+ '--info=hidden'
138
+ ])
139
+ next if output_format =~ /^ *$/
140
+
141
+ raise UserCancelled unless output_format
142
+
143
+ opt[:output] = output_format.strip
144
+ res = opt[:force] ? false : Prompt.yn('Save to file?', default_response: 'n')
145
+ if res
146
+ # print "#{yellow('File path/name: ')}#{reset}"
147
+ # filename = $stdin.gets.strip
148
+ filename = Prompt.read_line(prompt: 'File path/name')
149
+ next if filename.empty?
150
+
151
+ opt[:save_to] = filename
152
+ end
153
+ when /archive/
154
+ opt[:archive] = true
155
+ when /delete/
156
+ opt[:delete] = true
157
+ when /edit/
158
+ opt[:editor] = true
159
+ when /finish/
160
+ opt[:finish] = true
161
+ when /cancel/
162
+ opt[:cancel] = true
163
+ when /move/
164
+ section = choose_section.strip
165
+ opt[:move] = section.strip unless section =~ /^ *$/
166
+ when /flag/
167
+ opt[:flag] = true
168
+ end
169
+ end
170
+ end
171
+
172
+ if opt[:resume] || opt[:reset]
173
+ raise InvalidArgument, 'resume and restart can only be used on a single entry' if items.count > 1
174
+
175
+ item = items[0]
176
+ if opt[:resume] && !opt[:reset]
177
+ repeat_item(item, { editor: opt[:editor] }) # hooked
178
+ elsif opt[:reset]
179
+ res = Prompt.enter_text('Start date (blank for current time)', default_response: '')
180
+ if res =~ /^ *$/
181
+ date = Time.now
182
+ else
183
+ date = res.chronify(guess: :begin)
184
+ end
185
+
186
+ res = if item.tags?('done', :and) && !opt[:resume]
187
+ opt[:force] ? true : Prompt.yn('Remove @done tag?', default_response: 'y')
188
+ else
189
+ opt[:resume]
190
+ end
191
+ old_item = item.clone
192
+ new_entry = reset_item(item, date: date, resume: res)
193
+ @content.update_item(item, new_entry)
194
+ Hooks.trigger :post_entry_updated, self, new_entry, old_item
195
+ end
196
+ write(@doing_file)
197
+
198
+ return
199
+ end
200
+
201
+ if opt[:delete]
202
+ delete_items(items, force: opt[:force]) # hooked
203
+ write(@doing_file)
204
+
205
+ return
206
+ end
207
+
208
+ if opt[:flag]
209
+ tag = Doing.setting('marker_tag', 'flagged')
210
+ items.map! do |i|
211
+ old_item = i.clone
212
+ i.tag(tag, date: false, remove: opt[:remove], single: single)
213
+ Hooks.trigger :post_entry_updated, self, i, old_item
214
+ end
215
+ end
216
+
217
+ if opt[:finish] || opt[:cancel]
218
+ tag = 'done'
219
+ items.map! do |i|
220
+ if i.should_finish?
221
+ old_item = i.clone
222
+ should_date = !opt[:cancel] && i.should_time?
223
+ i.tag(tag, date: should_date, remove: opt[:remove], single: single)
224
+ Hooks.trigger :post_entry_updated, self, i, old_item
225
+ end
226
+ end
227
+ end
228
+
229
+ if opt[:autotag]
230
+ items.map! do |i|
231
+ new_title = autotag(i.title)
232
+ if new_title == i.title
233
+ logger.count(:skipped, level: :debug, message: '%count unchaged %items')
234
+ # logger.debug('Autotag:', 'No changes')
235
+ else
236
+ logger.count(:added_tags)
237
+ logger.write(items.count == 1 ? :info : :debug, 'Tagged:', new_title)
238
+ old_item = i.clone
239
+ i.title = new_title
240
+ Hooks.trigger :post_entry_updated, self, i, old_item
241
+ end
242
+ end
243
+ end
244
+
245
+ if opt[:tag]
246
+ tag = opt[:tag]
247
+ items.map! do |i|
248
+ old_item = i.clone
249
+ i.tag(tag, date: false, remove: opt[:remove], single: single)
250
+ i.expand_date_tags(Doing.setting('date_tags'))
251
+ Hooks.trigger :post_entry_updated, self, i, old_item
252
+ end
253
+ end
254
+
255
+ if opt[:archive] || opt[:move]
256
+ section = opt[:archive] ? 'Archive' : guess_section(opt[:move])
257
+ items.map! do |i|
258
+ old_item = i.clone
259
+ i.move_to(section, label: true)
260
+ Hooks.trigger :post_entry_updated, self, i, old_item
261
+ end
262
+ end
263
+
264
+ write(@doing_file)
265
+
266
+ if opt[:editor]
267
+ edit_items(items) # hooked
268
+
269
+ write(@doing_file)
270
+ end
271
+
272
+ return unless opt[:output]
273
+
274
+ items.each { |i| i.title = "#{i.title} @section(#{i.section})" }
275
+
276
+ export_items = Items.new
277
+ export_items.concat(items)
278
+ export_items.add_section(Section.new('Export'), log: false)
279
+ options = { section: 'All' }
280
+
281
+ if opt[:output] =~ /doing/
282
+ options[:output] = 'template'
283
+ options[:template] = '- %date | %title%note'
284
+ else
285
+ options[:output] = opt[:output]
286
+ options[:template] = opt[:template] || nil
287
+ end
288
+
289
+ output = list_section(options, items: export_items) # hooked
290
+
291
+ if opt[:save_to]
292
+ file = File.expand_path(opt[:save_to])
293
+ if File.exist?(file)
294
+ # Create a backup copy for the undo command
295
+ FileUtils.cp(file, "#{file}~")
296
+ end
297
+
298
+ File.open(file, 'w+') do |f|
299
+ f.puts output
300
+ end
301
+
302
+ logger.warn('File written:', file)
303
+ else
304
+ Doing::Pager.page output
305
+ end
306
+ end
307
+
308
+ ##
309
+ ## Generate a menu of sections and allow user selection
310
+ ##
311
+ ## @return [String] The selected section name
312
+ ##
313
+ def choose_section(include_all: false)
314
+ options = @content.section_titles.sort
315
+ options.unshift('All') if include_all
316
+ choice = Prompt.choose_from(options, prompt: 'Choose a section > ', fzf_args: ['--height=60%'])
317
+ choice ? choice.strip : choice
318
+ end
319
+
320
+ ##
321
+ ## Generate a menu of tags and allow user selection
322
+ ##
323
+ ## @return [String] The selected tag name
324
+ ##
325
+ def choose_tag(section = 'All', items: nil, include_all: false)
326
+ items ||= @content.in_section(section)
327
+ tags = all_tags(items, counts: true).map { |t, c| "@#{t} (#{c})" }
328
+ tags.unshift('No tag filter') if include_all
329
+ choice = Prompt.choose_from(tags, sorted: false, multiple: true, prompt: 'Choose tag(s) > ', fzf_args: ['--height=60%'])
330
+ choice ? choice.split(/\n/).map { |t| t.strip.sub(/ \(.*?\)$/, '')}.join(' ') : choice
331
+ end
332
+
333
+ ##
334
+ ## Generate a menu of sections and tags and allow user selection
335
+ ##
336
+ ## @return [String] The selected section or tag name
337
+ ##
338
+ def choose_section_tag
339
+ options = @content.section_titles.sort
340
+ options.concat(@content.all_tags.sort.map { |t| "@#{t}" })
341
+ choice = Prompt.choose_from(options, prompt: 'Choose a section or tag > ', fzf_args: ['--height=60%'])
342
+ choice ? choice.strip : choice
343
+ end
344
+
345
+ ##
346
+ ## Generate a menu of views and allow user selection
347
+ ##
348
+ ## @return [String] The selected view name
349
+ ##
350
+ def choose_view
351
+ choice = Prompt.choose_from(views.sort, prompt: 'Choose a view > ', fzf_args: ['--height=60%'])
352
+ choice ? choice.strip : choice
353
+ end
354
+
355
+ ##
356
+ ## Interactively verify an item modification if elapsed time is greater than configured threshold
357
+ ##
358
+ ## @param date [String] Item date
359
+ ## @param finish_date [String] The finish date
360
+ ## @param title [String] The Item title
361
+ ##
362
+ def verify_duration(date, finish_date, title: nil)
363
+ max_elapsed = Doing.setting('interaction.confirm_longer_than', 0)
364
+ max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
365
+ date = date.chronify(guess: :end, context: :today) if finish_date.is_a?(String)
366
+
367
+ elapsed = finish_date - date
368
+
369
+ if max_elapsed.positive? && (elapsed > max_elapsed)
370
+ puts boldwhite(title) if title
371
+ human = elapsed.time_string(format: :natural)
372
+ res = Prompt.yn(yellow("Did this entry actually take #{human}"), default_response: true)
373
+ unless res
374
+ new_elapsed = Prompt.enter_text('How long did it take?').chronify_qty
375
+ raise InvalidTimeExpression, 'Unrecognized time span entry' unless new_elapsed.positive?
376
+
377
+ finish_date = date + new_elapsed if new_elapsed
378
+ end
379
+ end
380
+
381
+ finish_date
382
+ end
383
+ end
384
+ end
385
+ end