tty2-prompt 0.23.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/LICENSE.txt +23 -0
  4. data/README.md +52 -0
  5. data/lib/tty2/prompt/answers_collector.rb +78 -0
  6. data/lib/tty2/prompt/block_paginator.rb +59 -0
  7. data/lib/tty2/prompt/choice.rb +147 -0
  8. data/lib/tty2/prompt/choices.rb +129 -0
  9. data/lib/tty2/prompt/confirm_question.rb +158 -0
  10. data/lib/tty2/prompt/const.rb +17 -0
  11. data/lib/tty2/prompt/converter_dsl.rb +21 -0
  12. data/lib/tty2/prompt/converter_registry.rb +69 -0
  13. data/lib/tty2/prompt/converters.rb +182 -0
  14. data/lib/tty2/prompt/distance.rb +49 -0
  15. data/lib/tty2/prompt/enum_list.rb +433 -0
  16. data/lib/tty2/prompt/errors.rb +31 -0
  17. data/lib/tty2/prompt/evaluator.rb +29 -0
  18. data/lib/tty2/prompt/expander.rb +321 -0
  19. data/lib/tty2/prompt/keypress.rb +98 -0
  20. data/lib/tty2/prompt/list.rb +589 -0
  21. data/lib/tty2/prompt/mask_question.rb +96 -0
  22. data/lib/tty2/prompt/multi_list.rb +224 -0
  23. data/lib/tty2/prompt/multiline.rb +72 -0
  24. data/lib/tty2/prompt/paginator.rb +111 -0
  25. data/lib/tty2/prompt/question/checks.rb +105 -0
  26. data/lib/tty2/prompt/question/modifier.rb +96 -0
  27. data/lib/tty2/prompt/question/validation.rb +72 -0
  28. data/lib/tty2/prompt/question.rb +391 -0
  29. data/lib/tty2/prompt/result.rb +42 -0
  30. data/lib/tty2/prompt/selected_choices.rb +77 -0
  31. data/lib/tty2/prompt/slider.rb +286 -0
  32. data/lib/tty2/prompt/statement.rb +55 -0
  33. data/lib/tty2/prompt/suggestion.rb +113 -0
  34. data/lib/tty2/prompt/symbols.rb +89 -0
  35. data/lib/tty2/prompt/test.rb +36 -0
  36. data/lib/tty2/prompt/timer.rb +75 -0
  37. data/lib/tty2/prompt/utils.rb +42 -0
  38. data/lib/tty2/prompt/version.rb +7 -0
  39. data/lib/tty2/prompt.rb +589 -0
  40. data/lib/tty2-prompt.rb +1 -0
  41. metadata +148 -0
@@ -0,0 +1,321 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "choices"
4
+
5
+ module TTY2
6
+ class Prompt
7
+ # A class responsible for rendering expanding options
8
+ # Used by {Prompt} to display key options question.
9
+ #
10
+ # @api private
11
+ class Expander
12
+ HELP_CHOICE = {
13
+ key: "h",
14
+ name: "print help",
15
+ value: :help
16
+ }.freeze
17
+
18
+ # Names for delete keys
19
+ DELETE_KEYS = %i[backspace delete].freeze
20
+
21
+ # Create instance of Expander
22
+ #
23
+ # @api public
24
+ def initialize(prompt, options = {})
25
+ @prompt = prompt
26
+ @prefix = options.fetch(:prefix) { @prompt.prefix }
27
+ @default = options.fetch(:default, 1)
28
+ @auto_hint = options.fetch(:auto_hint, false)
29
+ @active_color = options.fetch(:active_color) { @prompt.active_color }
30
+ @help_color = options.fetch(:help_color) { @prompt.help_color }
31
+ @quiet = options.fetch(:quiet) { @prompt.quiet }
32
+ @choices = Choices.new
33
+ @selected = nil
34
+ @done = false
35
+ @status = :collapsed
36
+ @hint = nil
37
+ @default_key = false
38
+ end
39
+
40
+ def expanded?
41
+ @status == :expanded
42
+ end
43
+
44
+ def collapsed?
45
+ @status == :collapsed
46
+ end
47
+
48
+ def expand
49
+ @status = :expanded
50
+ end
51
+
52
+ # Respond to submit event
53
+ #
54
+ # @api public
55
+ def keyenter(_)
56
+ if @input.nil? || @input.empty?
57
+ @input = @choices[@default - 1].key
58
+ @default_key = true
59
+ end
60
+
61
+ selected = select_choice(@input)
62
+
63
+ if selected && selected.key.to_s == "h"
64
+ expand
65
+ @selected = nil
66
+ @input = ""
67
+ elsif selected
68
+ @done = true
69
+ @selected = selected
70
+ @hint = nil
71
+ else
72
+ @input = ""
73
+ end
74
+ end
75
+ alias keyreturn keyenter
76
+
77
+ # Respond to key press event
78
+ #
79
+ # @api public
80
+ def keypress(event)
81
+ if DELETE_KEYS.include?(event.key.name)
82
+ @input.chop! unless @input.empty?
83
+ elsif event.value =~ /^[^\e\n\r]/
84
+ @input += event.value
85
+ end
86
+
87
+ @selected = select_choice(@input)
88
+ if @selected && !@default_key && collapsed?
89
+ @hint = @selected.name
90
+ end
91
+ end
92
+
93
+ # Select choice by given key
94
+ #
95
+ # @return [Choice]
96
+ #
97
+ # @api private
98
+ def select_choice(key)
99
+ @choices.find_by(:key, key)
100
+ end
101
+
102
+ # Set default value.
103
+ #
104
+ # @api public
105
+ def default(value = (not_set = true))
106
+ return @default if not_set
107
+
108
+ @default = value
109
+ end
110
+
111
+ # Set quiet mode.
112
+ #
113
+ # @api public
114
+ def quiet(value)
115
+ @quiet = value
116
+ end
117
+
118
+ # Add a single choice
119
+ #
120
+ # @api public
121
+ def choice(value, &block)
122
+ if block
123
+ @choices << value.update(value: block)
124
+ else
125
+ @choices << value
126
+ end
127
+ end
128
+
129
+ # Add multiple choices
130
+ #
131
+ # @param [Array[Object]] values
132
+ # the values to add as choices
133
+ #
134
+ # @api public
135
+ def choices(values)
136
+ values.each { |val| choice(val) }
137
+ end
138
+
139
+ # Execute this prompt
140
+ #
141
+ # @api public
142
+ def call(message, possibilities, &block)
143
+ choices(possibilities)
144
+ @message = message
145
+ block.call(self) if block
146
+ setup_defaults
147
+ choice(HELP_CHOICE)
148
+ @prompt.subscribe(self) do
149
+ render
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ # Create possible keys with current choice highlighted
156
+ #
157
+ # @return [String]
158
+ #
159
+ # @api private
160
+ def possible_keys
161
+ keys = @choices.pluck(:key)
162
+ default_key = keys[@default - 1]
163
+ if @selected
164
+ index = keys.index(@selected.key)
165
+ keys[index] = @prompt.decorate(keys[index], @active_color)
166
+ elsif @input.to_s.empty? && default_key
167
+ keys[@default - 1] = @prompt.decorate(default_key, @active_color)
168
+ end
169
+ keys.join(",")
170
+ end
171
+
172
+ # @api private
173
+ def render
174
+ @input = ""
175
+ until @done
176
+ question = render_question
177
+ @prompt.print(question)
178
+ read_input
179
+ @prompt.print(refresh(question.lines.count))
180
+ end
181
+ @prompt.print(render_question) unless @quiet
182
+ answer
183
+ end
184
+
185
+ # @api private
186
+ def answer
187
+ @selected.value
188
+ end
189
+
190
+ # Render message with options
191
+ #
192
+ # @return [String]
193
+ #
194
+ # @api private
195
+ def render_header
196
+ header = ["#{@prefix}#{@message} "]
197
+ if @done
198
+ selected_item = @selected.name.to_s
199
+ header << @prompt.decorate(selected_item, @active_color)
200
+ elsif collapsed?
201
+ header << %[(enter "h" for help) ]
202
+ header << "[#{possible_keys}] "
203
+ header << @input
204
+ end
205
+ header.join
206
+ end
207
+
208
+ # Show hint for selected option key
209
+ #
210
+ # return [String]
211
+ #
212
+ # @api private
213
+ def render_hint
214
+ "\n" + @prompt.decorate(">> ", @active_color) +
215
+ @hint +
216
+ @prompt.cursor.prev_line +
217
+ @prompt.cursor.forward(@prompt.strip(render_header).size)
218
+ end
219
+
220
+ # Render question with menu
221
+ #
222
+ # @return [String]
223
+ #
224
+ # @api private
225
+ def render_question
226
+ load_auto_hint if @auto_hint
227
+ header = render_header
228
+ header << render_hint if @hint
229
+ header << "\n" if @done
230
+
231
+ if !@done && expanded?
232
+ header << render_menu
233
+ header << render_footer
234
+ end
235
+ header
236
+ end
237
+
238
+ def load_auto_hint
239
+ if @hint.nil? && collapsed?
240
+ if @selected
241
+ @hint = @selected.name
242
+ else
243
+ if @input.empty?
244
+ @hint = @choices[@default - 1].name
245
+ else
246
+ @hint = "invalid option"
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ def render_footer
253
+ " Choice [#{@choices[@default - 1].key}]: #{@input}"
254
+ end
255
+
256
+ def read_input
257
+ @prompt.read_keypress
258
+ end
259
+
260
+ # Refresh the current input
261
+ #
262
+ # @param [Integer] lines
263
+ #
264
+ # @return [String]
265
+ #
266
+ # @api private
267
+ def refresh(lines)
268
+ if (@hint && (!@selected || @done)) || (@auto_hint && collapsed?)
269
+ @hint = nil
270
+ @prompt.clear_lines(lines, :down) +
271
+ @prompt.cursor.prev_line
272
+ elsif expanded?
273
+ @prompt.clear_lines(lines)
274
+ else
275
+ @prompt.clear_line
276
+ end
277
+ end
278
+
279
+ # Render help menu
280
+ #
281
+ # @api private
282
+ def render_menu
283
+ output = ["\n"]
284
+ @choices.each do |choice|
285
+ chosen = %(#{choice.key} - #{choice.name})
286
+ if @selected && @selected.key == choice.key
287
+ chosen = @prompt.decorate(chosen, @active_color)
288
+ end
289
+ output << " " + chosen + "\n"
290
+ end
291
+ output.join
292
+ end
293
+
294
+ def setup_defaults
295
+ validate_choices
296
+ end
297
+
298
+ def validate_choices
299
+ errors = []
300
+ keys = []
301
+ @choices.each do |choice|
302
+ if choice.key.nil?
303
+ errors << "Choice #{choice.name} is missing a :key attribute"
304
+ next
305
+ end
306
+ if choice.key.length != 1
307
+ errors << "Choice key `#{choice.key}` is more than one character long."
308
+ end
309
+ if choice.key.to_s == "h"
310
+ errors << "Choice key `#{choice.key}` is reserved for help menu."
311
+ end
312
+ if keys.include?(choice.key)
313
+ errors << "Choice key `#{choice.key}` is a duplicate."
314
+ end
315
+ keys << choice.key if choice.key
316
+ end
317
+ errors.each { |err| raise ConfigurationError, err }
318
+ end
319
+ end # Expander
320
+ end # Prompt
321
+ end # TTY2
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "question"
4
+ require_relative "timer"
5
+
6
+ module TTY2
7
+ class Prompt
8
+ class Keypress < Question
9
+ # Create keypress question
10
+ #
11
+ # @param [Prompt] prompt
12
+ # @param [Hash] options
13
+ #
14
+ # @api public
15
+ def initialize(prompt, **options)
16
+ super
17
+ @echo = options.fetch(:echo) { false }
18
+ @keys = options.fetch(:keys) { UndefinedSetting }
19
+ @timeout = options.fetch(:timeout) { UndefinedSetting }
20
+ @interval = options.fetch(:interval) {
21
+ (@timeout != UndefinedSetting && @timeout < 1) ? @timeout : 1
22
+ }
23
+ @decimals = (@interval.to_s.split(".")[1] || []).size
24
+ @countdown = @timeout
25
+ time = timeout? ? Float(@timeout) : nil
26
+ @timer = Timer.new(time, Float(@interval))
27
+
28
+ @prompt.subscribe(self)
29
+ end
30
+
31
+ def countdown(value = (not_set = true))
32
+ return @countdown if not_set
33
+
34
+ @countdown = value
35
+ end
36
+
37
+ # Check if any specific keys are set
38
+ def any_key?
39
+ @keys == UndefinedSetting
40
+ end
41
+
42
+ # Check if timeout is set
43
+ def timeout?
44
+ @timeout != UndefinedSetting
45
+ end
46
+
47
+ def keypress(event)
48
+ if any_key?
49
+ @done = true
50
+ elsif @keys.is_a?(Array) && @keys.include?(event.key.name)
51
+ @done = true
52
+ else
53
+ @done = false
54
+ end
55
+ end
56
+
57
+ def render_question
58
+ header = super
59
+ if timeout?
60
+ header.gsub!(/:countdown/, format("%.#{@decimals}f", countdown))
61
+ end
62
+ header
63
+ end
64
+
65
+ def interval_handler(time)
66
+ return if @done
67
+
68
+ question = render_question
69
+ line_size = question.size
70
+ total_lines = @prompt.count_screen_lines(line_size)
71
+ @prompt.print(refresh(question.lines.count, total_lines))
72
+ countdown(time)
73
+ @prompt.print(render_question)
74
+ end
75
+
76
+ def process_input(question)
77
+ @prompt.print(render_question)
78
+
79
+ @timer.on_tick do |time|
80
+ interval_handler(time)
81
+ end
82
+
83
+ @timer.while_remaining do |remaining|
84
+ break if @done
85
+
86
+ @input = @prompt.read_keypress(nonblock: true)
87
+ end
88
+ countdown(0) unless @done
89
+
90
+ @evaluator.(@input)
91
+ end
92
+
93
+ def refresh(lines, lines_to_clear)
94
+ @prompt.clear_lines(lines)
95
+ end
96
+ end # Keypress
97
+ end # Prompt
98
+ end # TTY2