inquirex-tty 0.10.0 → 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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +57 -0
- data/.envrc +2 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +27 -12
- data/README.md +50 -12
- data/docs/badges/coverage_badge.svg +2 -2
- data/examples/08_tax_preparer.rb +2 -2
- data/examples/10_answers.json +1 -1
- data/lib/inquirex/tty/answers_file.rb +48 -0
- data/lib/inquirex/tty/back_key.rb +31 -0
- data/lib/inquirex/tty/commands/run.rb +103 -50
- data/lib/inquirex/tty/flow_loader.rb +1 -2
- data/lib/inquirex/tty/renderer.rb +146 -10
- data/lib/inquirex/tty/version.rb +1 -1
- data/lib/inquirex/tty.rb +2 -0
- metadata +213 -191
|
@@ -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
|
|
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.
|
data/lib/inquirex/tty/version.rb
CHANGED
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"
|