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