doing 2.1.39 → 2.1.42

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