austb-tty-prompt 0.13.0

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