inquirex-tty 0.9.6 → 0.12.0

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.
@@ -23,6 +23,11 @@ module Inquirex
23
23
  # (fallback) → prompt.ask
24
24
  #
25
25
  # Header display verb uses TTY::Font for large ASCII-art section titles.
26
+ #
27
+ # {#collect} drives one collecting step against an engine and binds the
28
+ # back key ({BackKey}): pressed in any open prompt, it steps the engine
29
+ # back and the caller's loop re-asks the previous question with its
30
+ # answer as the default.
26
31
  class Renderer
27
32
  include UIHelper
28
33
 
@@ -62,6 +67,39 @@ module Inquirex
62
67
  sep(:magenta, "─")
63
68
  end
64
69
 
70
+ # Asks the engine's current collecting step and answers it. When the
71
+ # user presses the back key instead, the engine steps back and nothing
72
+ # is answered; the caller's loop then asks the previous question again,
73
+ # pre-filled with the answer it had.
74
+ #
75
+ # The tip line naming the key appears only when there is somewhere to
76
+ # go back to, and the key is live only then: on the first question it
77
+ # does nothing.
78
+ #
79
+ # @example Drive a flow's collecting steps
80
+ # until engine.finished?
81
+ # engine.current_step.display? ? (renderer.render(engine.current_step); engine.advance) : renderer.collect(engine)
82
+ # end
83
+ #
84
+ # @param engine [Inquirex::Engine] positioned on a collecting step
85
+ # @return [Symbol] +:answered+, or +:back+ when the engine stepped back
86
+ def collect(engine)
87
+ step_id = engine.current_step_id
88
+ can_back = engine.can_back?
89
+ puts pastel.dim("#{BackKey::LABEL}: back to the previous question") if can_back
90
+
91
+ answer = with_back_key(can_back) do
92
+ render(engine.current_step,
93
+ suggestion: engine.suggestion_for(step_id),
94
+ default: engine.previous_answer(step_id))
95
+ end
96
+ engine.answer(answer)
97
+ :answered
98
+ rescue BackKey::Pressed
99
+ go_back(engine)
100
+ :back
101
+ end
102
+
65
103
  # Renders a node. Returns the collected answer, or +nil+ for display verbs.
66
104
  #
67
105
  # @example Collect an answer for the current step, pre-checking LLM suggestions
@@ -74,9 +112,12 @@ module Inquirex
74
112
  # @param suggestion [Array, nil] prefill suggestion for the step (option
75
113
  # form values, e.g. from Engine#suggestion_for) — multi-select prompts
76
114
  # render these choices pre-checked so the user confirms or extends
115
+ # @param default [Object, nil] the answer to pre-fill, e.g. the one the
116
+ # user gave before going back (Engine#previous_answer)
77
117
  # @return [Object, nil]
78
- def render(node, suggestion: nil)
118
+ def render(node, suggestion: nil, default: nil)
79
119
  @current_suggestion = suggestion
120
+ @current_default = default
80
121
  if node.display?
81
122
  render_display_verb(node)
82
123
  nil
@@ -85,10 +126,103 @@ module Inquirex
85
126
  end
86
127
  ensure
87
128
  @current_suggestion = nil
129
+ @current_default = nil
88
130
  end
89
131
 
90
132
  private
91
133
 
134
+ # Runs the block with the back key live, when +enabled+.
135
+ #
136
+ # The handler is registered on the prompt's reader once and consulted
137
+ # through @back_armed, so the key raises only inside a collecting
138
+ # prompt: a "Press any key" on a display step treats it as any key.
139
+ #
140
+ # tty-prompt subscribes each question to the reader and unsubscribes it
141
+ # only when the question returns normally. A question unwound by
142
+ # {BackKey::Pressed} would stay subscribed and go on reacting to every
143
+ # later keypress, so the listeners it added are removed here.
144
+ #
145
+ # @param enabled [Boolean]
146
+ # @return [Object] the block's value
147
+ # @raise [BackKey::Pressed] when the key is pressed
148
+ def with_back_key(enabled)
149
+ return yield unless enabled && prompt.respond_to?(:on)
150
+
151
+ bind_back_key
152
+ listeners = reader_registrations&.to_a
153
+ @back_armed = true
154
+ yield
155
+ rescue BackKey::Pressed
156
+ drop_listeners_since(listeners)
157
+ raise
158
+ ensure
159
+ @back_armed = false
160
+ end
161
+
162
+ # @return [void]
163
+ def bind_back_key
164
+ return if @back_key_bound
165
+
166
+ prompt.on(BackKey::EVENT) { |*| raise BackKey::Pressed if @back_armed }
167
+ @back_key_bound = true
168
+ end
169
+
170
+ # @param before [Array, nil] the reader's registrations before the prompt opened
171
+ # @return [void]
172
+ def drop_listeners_since(before)
173
+ live = reader_registrations
174
+ return unless before && live
175
+
176
+ (live.to_a - before).each { |stale| live.delete(stale) }
177
+ end
178
+
179
+ # tty-reader keeps its listeners in Wisper's registration set, which it
180
+ # exposes only privately (its own #unsubscribe reads it the same way).
181
+ #
182
+ # @return [Set, nil]
183
+ def reader_registrations
184
+ reader = prompt.respond_to?(:reader) ? prompt.reader : nil
185
+ return nil unless reader.respond_to?(:local_registrations, true)
186
+
187
+ reader.send(:local_registrations)
188
+ end
189
+
190
+ # Steps the engine back to the previous question. An LLM step between
191
+ # them is stepped over too: it asks the user nothing, and landing on it
192
+ # would rerun the extraction and carry the user straight forward again.
193
+ #
194
+ # @param engine [Inquirex::Engine]
195
+ # @return [void]
196
+ def go_back(engine)
197
+ engine.back
198
+ engine.back while llm_step?(engine.current_step) && engine.can_back?
199
+ puts pastel.dim("↩ back to #{engine.current_step_id}")
200
+ end
201
+
202
+ # @param node [Inquirex::Node, nil]
203
+ # @return [Boolean]
204
+ def llm_step?(node) = node.respond_to?(:llm_verb?) && node.llm_verb?
205
+
206
+ # tty-prompt's `default:` option for the value being pre-filled.
207
+ #
208
+ # @param value [Object, nil] defaults to the value passed to #render
209
+ # @return [Hash] {} or { default: value }
210
+ def default_option(value = @current_default)
211
+ value.nil? ? {} : { default: value }
212
+ end
213
+
214
+ # select and enum_select preselect by choice NAME, the label the user
215
+ # sees, while answers are option values; translate back.
216
+ #
217
+ # @param node [Inquirex::Node]
218
+ # @return [Hash] {} or { default: label }
219
+ def choice_default(node)
220
+ return {} if @current_default.nil?
221
+
222
+ labels = node.respond_to?(:option_labels) ? node.option_labels : nil
223
+ { default: labels&.fetch(@current_default.to_s, nil) || @current_default.to_s }
224
+ end
225
+
92
226
  # Dispatches display verbs to their styled renderers.
93
227
  # :header uses TTY::Font for a large ASCII-art title.
94
228
  # :btw and :warning use TTY::Box info/warn boxes.
@@ -164,13 +298,14 @@ module Inquirex
164
298
 
165
299
  # Single-line text.
166
300
  def render_text_input(node)
167
- prompt.ask(node.question)
301
+ prompt.ask(node.question, **default_option)
168
302
  end
169
303
 
170
304
  # Multi-line text (empty line to submit).
171
305
  def render_multiline(node)
172
306
  puts pastel.bold(node.question)
173
307
  puts pastel.dim("Enter your response. Press Enter on a blank line to submit.")
308
+ puts pastel.dim("Previous answer:\n#{@current_default}") unless @current_default.nil?
174
309
  sep(:cyan, "─")
175
310
  collect_multiline_text
176
311
  end
@@ -210,9 +345,9 @@ module Inquirex
210
345
  # @param convert [Symbol] :int or :float
211
346
  # @return [Numeric, nil]
212
347
  def ask_bounded(node, convert:)
213
- return prompt.ask(node.question, convert:) unless node.bounded?
348
+ return prompt.ask(node.question, convert:, **default_option) unless node.bounded?
214
349
 
215
- prompt.ask(node.question, convert:, in: bounds_range(node)) do |q|
350
+ prompt.ask(node.question, convert:, in: bounds_range(node), **default_option) do |q|
216
351
  q.messages[:range?] = "Enter a value #{describe_bounds(node)}."
217
352
  end
218
353
  end
@@ -239,16 +374,17 @@ module Inquirex
239
374
 
240
375
  # Boolean — tty-prompt yes?.
241
376
  def render_yes_no(node)
242
- prompt.yes?(node.question)
377
+ prompt.yes?(node.question, **default_option)
243
378
  end
244
379
 
245
380
  # Single-choice scrollable menu.
246
381
  def render_select(node)
247
- prompt.select(node.question, select_options(node))
382
+ prompt.select(node.question, select_options(node), **choice_default(node))
248
383
  end
249
384
 
250
385
  # Multiple-choice list (space to toggle, min 1 selection). Suggested
251
- # choices (an LLM extraction the user should confirm) render pre-checked.
386
+ # choices (an LLM extraction the user should confirm) render pre-checked,
387
+ # and after going back, the choices the user had made.
252
388
  def render_multi_select(node)
253
389
  prompt.multi_select(node.question, select_options(node), min: 1, **multi_select_defaults(node))
254
390
  end
@@ -261,7 +397,7 @@ module Inquirex
261
397
  # @param node [Inquirex::Node]
262
398
  # @return [Hash] {} or { default: Array<String> }
263
399
  def multi_select_defaults(node)
264
- suggested = Array(@current_suggestion)
400
+ suggested = Array(@current_default.nil? ? @current_suggestion : @current_default)
265
401
  return {} if suggested.empty?
266
402
 
267
403
  labels = node.respond_to?(:option_labels) ? node.option_labels : nil
@@ -271,7 +407,7 @@ module Inquirex
271
407
 
272
408
  # Numbered choice menu.
273
409
  def render_enum_select(node)
274
- prompt.enum_select(node.question, select_options(node))
410
+ prompt.enum_select(node.question, select_options(node), **choice_default(node))
275
411
  end
276
412
 
277
413
  # Hidden / masked input.
@@ -288,7 +424,7 @@ module Inquirex
288
424
  opts[:max] = hint_opts[:max] if hint_opts[:max]
289
425
  opts[:step] = hint_opts[:step] if hint_opts[:step]
290
426
  end
291
- prompt.slider(node.question, **opts)
427
+ prompt.slider(node.question, **opts, **default_option)
292
428
  end
293
429
 
294
430
  # Aliases: date, email, phone degrade to plain text_input in TTY.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Inquirex
4
4
  module TTY
5
- VERSION = "0.9.6"
5
+ VERSION = "0.12.0"
6
6
  end
7
7
  end
data/lib/inquirex/tty.rb CHANGED
@@ -40,6 +40,8 @@ require_relative "tty/version"
40
40
  require_relative "tty/ui_helper"
41
41
  require_relative "tty/flow_loader"
42
42
  require_relative "tty/output_path"
43
+ require_relative "tty/answers_file"
44
+ require_relative "tty/back_key"
43
45
  require_relative "tty/renderer"
44
46
  require_relative "tty/commands/run"
45
47
  require_relative "tty/commands/validate"