austb-tty-prompt 0.13.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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +25 -0
  5. data/CHANGELOG.md +218 -0
  6. data/CODE_OF_CONDUCT.md +49 -0
  7. data/Gemfile +19 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +1132 -0
  10. data/Rakefile +8 -0
  11. data/appveyor.yml +23 -0
  12. data/benchmarks/speed.rb +27 -0
  13. data/examples/ask.rb +15 -0
  14. data/examples/collect.rb +19 -0
  15. data/examples/echo.rb +11 -0
  16. data/examples/enum.rb +8 -0
  17. data/examples/enum_paged.rb +9 -0
  18. data/examples/enum_select.rb +7 -0
  19. data/examples/expand.rb +29 -0
  20. data/examples/in.rb +9 -0
  21. data/examples/inputs.rb +10 -0
  22. data/examples/key_events.rb +11 -0
  23. data/examples/keypress.rb +9 -0
  24. data/examples/mask.rb +13 -0
  25. data/examples/multi_select.rb +8 -0
  26. data/examples/multi_select_paged.rb +9 -0
  27. data/examples/multiline.rb +9 -0
  28. data/examples/pause.rb +7 -0
  29. data/examples/select.rb +18 -0
  30. data/examples/select_paginated.rb +9 -0
  31. data/examples/slider.rb +6 -0
  32. data/examples/validation.rb +9 -0
  33. data/examples/yes_no.rb +7 -0
  34. data/lib/tty-prompt.rb +4 -0
  35. data/lib/tty/prompt.rb +535 -0
  36. data/lib/tty/prompt/answers_collector.rb +59 -0
  37. data/lib/tty/prompt/choice.rb +90 -0
  38. data/lib/tty/prompt/choices.rb +110 -0
  39. data/lib/tty/prompt/confirm_question.rb +129 -0
  40. data/lib/tty/prompt/converter_dsl.rb +22 -0
  41. data/lib/tty/prompt/converter_registry.rb +64 -0
  42. data/lib/tty/prompt/converters.rb +77 -0
  43. data/lib/tty/prompt/distance.rb +49 -0
  44. data/lib/tty/prompt/enum_list.rb +337 -0
  45. data/lib/tty/prompt/enum_paginator.rb +56 -0
  46. data/lib/tty/prompt/evaluator.rb +29 -0
  47. data/lib/tty/prompt/expander.rb +292 -0
  48. data/lib/tty/prompt/keypress.rb +94 -0
  49. data/lib/tty/prompt/list.rb +317 -0
  50. data/lib/tty/prompt/mask_question.rb +91 -0
  51. data/lib/tty/prompt/multi_list.rb +108 -0
  52. data/lib/tty/prompt/multiline.rb +71 -0
  53. data/lib/tty/prompt/paginator.rb +88 -0
  54. data/lib/tty/prompt/question.rb +333 -0
  55. data/lib/tty/prompt/question/checks.rb +87 -0
  56. data/lib/tty/prompt/question/modifier.rb +94 -0
  57. data/lib/tty/prompt/question/validation.rb +72 -0
  58. data/lib/tty/prompt/reader.rb +352 -0
  59. data/lib/tty/prompt/reader/codes.rb +121 -0
  60. data/lib/tty/prompt/reader/console.rb +57 -0
  61. data/lib/tty/prompt/reader/history.rb +145 -0
  62. data/lib/tty/prompt/reader/key_event.rb +91 -0
  63. data/lib/tty/prompt/reader/line.rb +162 -0
  64. data/lib/tty/prompt/reader/mode.rb +44 -0
  65. data/lib/tty/prompt/reader/win_api.rb +29 -0
  66. data/lib/tty/prompt/reader/win_console.rb +53 -0
  67. data/lib/tty/prompt/result.rb +42 -0
  68. data/lib/tty/prompt/slider.rb +182 -0
  69. data/lib/tty/prompt/statement.rb +55 -0
  70. data/lib/tty/prompt/suggestion.rb +115 -0
  71. data/lib/tty/prompt/symbols.rb +61 -0
  72. data/lib/tty/prompt/timeout.rb +69 -0
  73. data/lib/tty/prompt/utils.rb +44 -0
  74. data/lib/tty/prompt/version.rb +7 -0
  75. data/lib/tty/test_prompt.rb +20 -0
  76. data/tasks/console.rake +11 -0
  77. data/tasks/coverage.rake +11 -0
  78. data/tasks/spec.rake +29 -0
  79. data/tty-prompt.gemspec +32 -0
  80. metadata +243 -0
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'paginator'
4
+
5
+ module TTY
6
+ class Prompt
7
+ class EnumPaginator < Paginator
8
+ # Paginate list of choices based on current active choice.
9
+ # Move entire pages.
10
+ #
11
+ # @api public
12
+ def paginate(list, active, per_page = nil, &block)
13
+ default_size = (list.size <= DEFAULT_PAGE_SIZE ? list.size : DEFAULT_PAGE_SIZE)
14
+ @per_page = @per_page || per_page || default_size
15
+
16
+ # Don't paginate short lists
17
+ if list.size <= @per_page
18
+ @lower_index = 0
19
+ @upper_index = list.size - 1
20
+ if block
21
+ return list.each_with_index(&block)
22
+ else
23
+ return list.each_with_index.to_enum
24
+ end
25
+ end
26
+
27
+ unless active.nil? # User may input index out of range
28
+ @last_index = active
29
+ end
30
+ page = (@last_index / @per_page.to_f).ceil
31
+ pages = (list.size / @per_page.to_f).ceil
32
+ if page == 0
33
+ @lower_index = 0
34
+ @upper_index = @lower_index + @per_page - 1
35
+ elsif page > 0 && page <= pages
36
+ @lower_index = (page - 1) * @per_page
37
+ @upper_index = @lower_index + @per_page - 1
38
+ else
39
+ @upper_index = list.size - 1
40
+ @lower_index = @upper_index - @per_page + 1
41
+ end
42
+
43
+ sliced_list = list[@lower_index..@upper_index]
44
+ indices = (@lower_index..@upper_index)
45
+
46
+ if block
47
+ sliced_list.each_with_index do |item, index|
48
+ block[item, @lower_index + index]
49
+ end
50
+ else
51
+ sliced_list.zip(indices).to_enum unless block_given?
52
+ end
53
+ end
54
+ end # EnumPaginator
55
+ end # Prompt
56
+ end # TTY
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'result'
4
+
5
+ module TTY
6
+ class Prompt
7
+ # Evaluates provided parameters and stops if any of them fails
8
+ # @api private
9
+ class Evaluator
10
+ attr_reader :results
11
+
12
+ def initialize(question, &block)
13
+ @question = question
14
+ @results = []
15
+ instance_eval(&block) if block
16
+ end
17
+
18
+ def call(initial)
19
+ seed = Result::Success.new(@question, initial)
20
+ results.reduce(seed, &:with)
21
+ end
22
+
23
+ def check(proc = nil, &block)
24
+ results << (proc || block)
25
+ end
26
+ alias_method :<<, :check
27
+ end # Evaluator
28
+ end # Prompt
29
+ end # TTY
@@ -0,0 +1,292 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'choices'
4
+
5
+ module TTY
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
+ }
17
+
18
+ # Create instance of Expander
19
+ #
20
+ # @api public
21
+ def initialize(prompt, options = {})
22
+ @prompt = prompt
23
+ @prefix = options.fetch(:prefix) { @prompt.prefix }
24
+ @default = options.fetch(:default) { 1 }
25
+ @active_color = options.fetch(:active_color) { @prompt.active_color }
26
+ @help_color = options.fetch(:help_color) { @prompt.help_color }
27
+ @choices = Choices.new
28
+ @selected = nil
29
+ @done = false
30
+ @status = :collapsed
31
+ @hint = nil
32
+ @default_key = false
33
+
34
+ @prompt.subscribe(self)
35
+ end
36
+
37
+ def expanded?
38
+ @status == :expanded
39
+ end
40
+
41
+ def collapsed?
42
+ @status == :collapsed
43
+ end
44
+
45
+ def expand
46
+ @status = :expanded
47
+ end
48
+
49
+ # Respond to submit event
50
+ #
51
+ # @api public
52
+ def keyenter(_)
53
+ if @input.nil? || @input.empty?
54
+ @input = @choices[@default - 1].key
55
+ @default_key = true
56
+ end
57
+
58
+ selected = select_choice(@input)
59
+
60
+ if selected && selected.key.to_s == 'h'
61
+ expand
62
+ @selected = nil
63
+ @input = ''
64
+ elsif selected
65
+ @done = true
66
+ @selected = selected
67
+ else
68
+ @input = ''
69
+ end
70
+ end
71
+ alias_method :keyreturn, :keyenter
72
+
73
+ # Respond to key press event
74
+ #
75
+ # @api public
76
+ def keypress(event)
77
+ if [:backspace, :delete].include?(event.key.name)
78
+ @input.chop! unless @input.empty?
79
+ elsif event.value =~ /^[^\e\n\r]/
80
+ @input += event.value
81
+ end
82
+ @selected = select_choice(@input)
83
+ if @selected && !@default_key && collapsed?
84
+ @hint = @selected.name
85
+ end
86
+ end
87
+
88
+ # Select choice by given key
89
+ #
90
+ # @return [Choice]
91
+ #
92
+ # @api private
93
+ def select_choice(key)
94
+ @choices.find_by(:key, key)
95
+ end
96
+
97
+ # Set default value.
98
+ #
99
+ # @api public
100
+ def default(value = (not_set = true))
101
+ return @default if not_set
102
+ @default = value
103
+ end
104
+
105
+ # Add a single choice
106
+ #
107
+ # @api public
108
+ def choice(value, &block)
109
+ if block
110
+ @choices << value.update(value: block)
111
+ else
112
+ @choices << value
113
+ end
114
+ end
115
+
116
+ # Add multiple choices
117
+ #
118
+ # @param [Array[Object]] values
119
+ # the values to add as choices
120
+ #
121
+ # @api public
122
+ def choices(values)
123
+ values.each { |val| choice(val) }
124
+ end
125
+
126
+ # Execute this prompt
127
+ #
128
+ # @api public
129
+ def call(message, possibilities, &block)
130
+ choices(possibilities)
131
+ @message = message
132
+ block.call(self) if block
133
+ setup_defaults
134
+ choice(HELP_CHOICE)
135
+ render
136
+ end
137
+
138
+ private
139
+
140
+ # Create possible keys with current choice highlighted
141
+ #
142
+ # @return [String]
143
+ #
144
+ # @api private
145
+ def possible_keys
146
+ keys = @choices.pluck(:key)
147
+ default_key = keys[@default - 1]
148
+ if @selected
149
+ index = keys.index(@selected.key)
150
+ keys[index] = @prompt.decorate(keys[index], @active_color)
151
+ elsif @input.to_s.empty? && default_key
152
+ keys[@default - 1] = @prompt.decorate(default_key, @active_color)
153
+ end
154
+ keys.join(',')
155
+ end
156
+
157
+ # @api private
158
+ def render
159
+ @input = ''
160
+ until @done
161
+ question = render_question
162
+ @prompt.print(question)
163
+ read_input
164
+ @prompt.print(refresh(question.lines.count))
165
+ end
166
+ @prompt.print(render_question)
167
+ answer
168
+ end
169
+
170
+ # @api private
171
+ def answer
172
+ @selected.value
173
+ end
174
+
175
+ # Render message with options
176
+ #
177
+ # @return [String]
178
+ #
179
+ # @api private
180
+ def render_header
181
+ header = "#{@prefix}#{@message} "
182
+ if @done
183
+ selected_item = "#{@selected.name}"
184
+ header << @prompt.decorate(selected_item, @active_color)
185
+ elsif collapsed?
186
+ header << %[(enter "h" for help) ]
187
+ header << "[#{possible_keys}] "
188
+ header << @input
189
+ end
190
+ header
191
+ end
192
+
193
+ # Show hint for selected option key
194
+ #
195
+ # return [String]
196
+ #
197
+ # @api private
198
+ def render_hint
199
+ hint = "\n"
200
+ hint << @prompt.decorate('>> ', @active_color)
201
+ hint << @hint
202
+ hint << @prompt.cursor.prev_line
203
+ hint << @prompt.cursor.forward(@prompt.strip(render_header).size)
204
+ end
205
+
206
+ # Render question with menu
207
+ #
208
+ # @return [String]
209
+ #
210
+ # @api private
211
+ def render_question
212
+ header = render_header
213
+ header << render_hint if @hint
214
+ header << "\n" if @done
215
+
216
+ if !@done && expanded?
217
+ header << render_menu
218
+ header << render_footer
219
+ end
220
+ header
221
+ end
222
+
223
+ def render_footer
224
+ " Choice [#{@choices[@default - 1].key}]: #{@input}"
225
+ end
226
+
227
+ def read_input
228
+ @prompt.read_keypress
229
+ end
230
+
231
+ # Refresh the current input
232
+ #
233
+ # @param [Integer] lines
234
+ #
235
+ # @return [String]
236
+ #
237
+ # @api private
238
+ def refresh(lines)
239
+ if @hint && (!@selected || @done)
240
+ @hint = nil
241
+ @prompt.clear_lines(lines, :down) +
242
+ @prompt.cursor.prev_line
243
+ elsif expanded?
244
+ @prompt.clear_lines(lines)
245
+ else
246
+ @prompt.clear_line
247
+ end
248
+ end
249
+
250
+ # Render help menu
251
+ #
252
+ # @api private
253
+ def render_menu
254
+ output = "\n"
255
+ @choices.each do |choice|
256
+ chosen = %(#{choice.key} - #{choice.name})
257
+ if @selected && @selected.key == choice.key
258
+ chosen = @prompt.decorate(chosen, @active_color)
259
+ end
260
+ output << ' ' + chosen + "\n"
261
+ end
262
+ output
263
+ end
264
+
265
+ def setup_defaults
266
+ validate_choices
267
+ end
268
+
269
+ def validate_choices
270
+ errors = []
271
+ keys = []
272
+ @choices.each do |choice|
273
+ if choice.key.nil?
274
+ errors << "Choice #{choice.name} is missing a :key attribute"
275
+ next
276
+ end
277
+ if choice.key.length != 1
278
+ errors << "Choice key `#{choice.key}` is more than one character long."
279
+ end
280
+ if choice.key.to_s == 'h'
281
+ errors << "Choice key `#{choice.key}` is reserved for help menu."
282
+ end
283
+ if keys.include?(choice.key)
284
+ errors << "Choice key `#{choice.key}` is a duplicate."
285
+ end
286
+ keys << choice.key if choice.key
287
+ end
288
+ errors.each { |err| fail ConfigurationError, err }
289
+ end
290
+ end # Expander
291
+ end # Prompt
292
+ end # TTY
@@ -0,0 +1,94 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'question'
4
+ require_relative 'symbols'
5
+ require_relative 'timeout'
6
+
7
+ module TTY
8
+ class Prompt
9
+ class Keypress < Question
10
+ # Create keypress question
11
+ #
12
+ # @param [Prompt] prompt
13
+ # @param [Hash] options
14
+ #
15
+ # @api public
16
+ def initialize(prompt, options = {})
17
+ super
18
+ @echo = options.fetch(:echo) { false }
19
+ @keys = options.fetch(:keys) { UndefinedSetting }
20
+ @timeout = options.fetch(:timeout) { UndefinedSetting }
21
+ @interval = options.fetch(:interval) {
22
+ (@timeout != UndefinedSetting && @timeout < 1) ? @timeout : 1
23
+ }
24
+ @pause = true
25
+ @countdown = @timeout
26
+ @interval_handler = proc { |time|
27
+ question = render_question
28
+ @prompt.print(refresh(question.lines.count))
29
+ countdown(time)
30
+ @prompt.print(render_question)
31
+ }
32
+
33
+ @prompt.subscribe(self)
34
+ end
35
+
36
+ def countdown(value = (not_set = true))
37
+ return @countdown if not_set
38
+ @countdown = value
39
+ end
40
+
41
+ # Check if any specific keys are set
42
+ def any_key?
43
+ @keys == UndefinedSetting
44
+ end
45
+
46
+ # Check if timeout is set
47
+ def timeout?
48
+ @timeout != UndefinedSetting
49
+ end
50
+
51
+ def keypress(event)
52
+ if any_key?
53
+ @pause = false
54
+ elsif @keys.is_a?(Array) && @keys.include?(event.key.name)
55
+ @pause = false
56
+ else
57
+ @pause = true
58
+ end
59
+ end
60
+
61
+ def render_question
62
+ header = super
63
+ header.gsub!(/:countdown/, countdown.to_s)
64
+ header
65
+ end
66
+
67
+ def process_input(question)
68
+ time do
69
+ while @pause
70
+ @input = @prompt.read_keypress
71
+ end
72
+ @pause
73
+ end
74
+ @evaluator.(@input)
75
+ end
76
+
77
+ def refresh(lines)
78
+ @prompt.clear_lines(lines)
79
+ end
80
+
81
+ def time(&block)
82
+ if timeout?
83
+ time = Float(@timeout)
84
+ interval = Float(@interval)
85
+ scheduler = Timeout.new(interval_handler: @interval_handler)
86
+ scheduler.timeout(time, interval, &block)
87
+ else
88
+ block.()
89
+ end
90
+ rescue Timeout::Error
91
+ end
92
+ end # Keypress
93
+ end # Prompt
94
+ end # TTY