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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/bin/commands/config.rb +43 -34
- data/bin/commands/done.rb +1 -18
- data/bin/commands/finish.rb +30 -25
- data/bin/commands/grep.rb +3 -14
- data/bin/commands/last.rb +2 -8
- data/bin/commands/meanwhile.rb +13 -6
- data/bin/commands/on.rb +3 -16
- data/bin/commands/recent.rb +2 -8
- data/bin/commands/reset.rb +24 -1
- data/bin/commands/select.rb +1 -1
- data/bin/commands/show.rb +6 -17
- data/bin/commands/since.rb +1 -12
- data/bin/commands/today.rb +2 -13
- data/bin/commands/view.rb +1 -1
- data/bin/commands/yesterday.rb +2 -13
- data/bin/doing +15 -8
- data/docs/doc/Array.html +1 -1
- data/docs/doc/BooleanTermParser/Clause.html +1 -1
- data/docs/doc/BooleanTermParser/Operator.html +1 -1
- data/docs/doc/BooleanTermParser/Query.html +1 -1
- data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
- data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/Color.html +166 -20
- data/docs/doc/Doing/Completion.html +1 -1
- data/docs/doc/Doing/Configuration.html +1 -1
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
- data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
- data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
- data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
- data/docs/doc/Doing/Errors/NoResults.html +10 -2
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
- data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
- data/docs/doc/Doing/Errors.html +9 -9
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +90 -1615
- data/docs/doc/Doing/Items.html +121 -5
- data/docs/doc/Doing/Logger.html +1 -1
- data/docs/doc/Doing/Note.html +1 -1
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +2 -2
- data/docs/doc/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +2 -2
- data/docs/doc/Doing/Types.html +1 -1
- data/docs/doc/Doing/Util/Backup.html +5 -5
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +197 -4033
- data/docs/doc/Doing.html +2 -2
- data/docs/doc/FalseClass.html +1 -1
- data/docs/doc/GLI/Commands/Help.html +1 -1
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +1 -1
- data/docs/doc/Object.html +1 -1
- data/docs/doc/PhraseParser/Operator.html +1 -1
- data/docs/doc/PhraseParser/PhraseClause.html +1 -1
- data/docs/doc/PhraseParser/Query.html +1 -1
- data/docs/doc/PhraseParser/QueryParser.html +1 -1
- data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
- data/docs/doc/PhraseParser/TermClause.html +1 -1
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +1 -1
- data/docs/doc/String.html +1 -1
- data/docs/doc/Symbol.html +1 -1
- data/docs/doc/Time.html +1 -1
- data/docs/doc/TrueClass.html +1 -1
- data/docs/doc/_index.html +26 -5
- data/docs/doc/class_list.html +1 -1
- data/docs/doc/file.README.html +2 -2
- data/docs/doc/index.html +2 -2
- data/docs/doc/method_list.html +293 -773
- data/docs/doc/top-level-namespace.html +3 -3
- data/docs/index.md +1 -1
- data/doing.rdoc +49 -7
- data/lib/completion/_doing.zsh +5 -5
- data/lib/completion/doing.bash +8 -8
- data/lib/completion/doing.fish +7 -2
- data/lib/doing/add_options.rb +31 -1
- data/lib/doing/chronify/array.rb +64 -22
- data/lib/doing/colors.rb +77 -30
- data/lib/doing/completion.rb +4 -5
- data/lib/doing/errors.rb +51 -35
- data/lib/doing/hooks.rb +3 -3
- data/lib/doing/item/dates.rb +112 -0
- data/lib/doing/item/query.rb +433 -0
- data/lib/doing/item/state.rb +59 -0
- data/lib/doing/item/tags.rb +87 -0
- data/lib/doing/item.rb +6 -667
- data/lib/doing/items.rb +38 -13
- data/lib/doing/plugin_manager.rb +3 -3
- data/lib/doing/plugins/export/template_export.rb +4 -4
- data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
- data/lib/doing/util_backup.rb +6 -8
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid/display.rb +399 -0
- data/lib/doing/wwid/editor.rb +214 -0
- data/lib/doing/wwid/filetools.rb +186 -0
- data/lib/doing/wwid/filter.rb +218 -0
- data/lib/doing/wwid/guess.rb +87 -0
- data/lib/doing/wwid/interactive.rb +385 -0
- data/lib/doing/wwid/modify.rb +618 -0
- data/lib/doing/wwid/tags.rb +54 -0
- data/lib/doing/wwid/timers.rb +345 -0
- data/lib/doing/wwid/wwidutil.rb +104 -0
- data/lib/doing/wwid.rb +31 -2317
- 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
|