doing 2.1.39 → 2.1.42

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