tty-prompt 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,14 +11,17 @@ module TTY
11
11
  #
12
12
  # @api public
13
13
  def initialize(prompt, options = {})
14
- @prompt = prompt
15
- @done = false
16
- @failure = false
17
- @enum = options.fetch(:enum) { ')' }
18
- @default = options.fetch(:default) { 1 }
19
- @active = @default
20
- @choices = Choices.new
21
- @color = options.fetch(:color) { :green }
14
+ @prompt = prompt
15
+ @prefix = options.fetch(:prefix) { @prompt.prefix }
16
+ @enum = options.fetch(:enum) { ')' }
17
+ @default = options.fetch(:default) { 1 }
18
+ @active_color = options.fetch(:active_color) { @prompt.active_color }
19
+ @help_color = options.fetch(:help_color) { @prompt.help_color }
20
+ @error_color = options.fetch(:error_color) { @prompt.error_color }
21
+ @done = false
22
+ @failure = false
23
+ @active = @default
24
+ @choices = Choices.new
22
25
 
23
26
  @prompt.subscribe(self)
24
27
  end
@@ -161,7 +164,7 @@ module TTY
161
164
  #
162
165
  # @api private
163
166
  def render_question
164
- header = "#{@prompt.prefix}#{@question} #{render_header}"
167
+ header = "#{@prefix}#{@question} #{render_header}"
165
168
  @prompt.puts(header)
166
169
  return if @done
167
170
  @prompt.print(render_menu)
@@ -172,7 +175,7 @@ module TTY
172
175
  # @api private
173
176
  def render_error
174
177
  error = 'Please enter a valid index'
175
- @prompt.print("\n" + @prompt.decorate('>>', :red) + ' ' + error)
178
+ @prompt.print("\n" + @prompt.decorate('>>', @error_color) + ' ' + error)
176
179
  @prompt.print(@prompt.cursor.prev_line)
177
180
  @prompt.print(@prompt.cursor.forward(render_footer.size))
178
181
  end
@@ -186,7 +189,7 @@ module TTY
186
189
  return '' unless @done
187
190
  return '' unless @active
188
191
  selected_item = "#{@choices[@active - 1].name}"
189
- @prompt.decorate(selected_item, @color)
192
+ @prompt.decorate(selected_item, @active_color)
190
193
  end
191
194
 
192
195
  # Render footer for the indexed menu
@@ -209,7 +212,7 @@ module TTY
209
212
  num = (index + 1).to_s + @enum + Symbols::SPACE
210
213
  selected = Symbols::SPACE * 2 + num + choice.name
211
214
  output << if index + 1 == @active
212
- @prompt.decorate("#{selected}", @color)
215
+ @prompt.decorate("#{selected}", @active_color)
213
216
  else
214
217
  selected
215
218
  end
@@ -0,0 +1,288 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty/prompt/question'
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
+ render_question
162
+ read_input
163
+ refresh
164
+ end
165
+ render_question
166
+ render_answer
167
+ end
168
+
169
+ # @api private
170
+ def render_answer
171
+ @selected.value
172
+ end
173
+
174
+ def render_header
175
+ header = "#{@prefix}#{@message} "
176
+ if @done
177
+ selected_item = "#{@selected.name}"
178
+ header << @prompt.decorate(selected_item, @active_color)
179
+ elsif collapsed?
180
+ header << %[(enter "h" for help) ]
181
+ header << "[#{possible_keys}] "
182
+ header << @input
183
+ end
184
+ header
185
+ end
186
+
187
+ # @api private
188
+ def render_hint
189
+ hint = "\n"
190
+ hint << @prompt.decorate('>> ', @active_color)
191
+ hint << @hint
192
+ @prompt.print(hint)
193
+ @prompt.print(@prompt.cursor.prev_line)
194
+ @prompt.print(@prompt.cursor.forward(@prompt.strip(render_header).size))
195
+ end
196
+
197
+ # @api private
198
+ def render_question
199
+ header = render_header
200
+ @prompt.print(header)
201
+ render_hint if @hint
202
+ @prompt.print("\n") if @done
203
+
204
+ if !@done && expanded?
205
+ @prompt.print(render_menu)
206
+ @prompt.print(render_footer)
207
+ end
208
+ end
209
+
210
+ def render_footer
211
+ " Choice [#{@choices[@default - 1].key}]: #{@input}"
212
+ end
213
+
214
+ def read_input
215
+ @prompt.read_keypress
216
+ end
217
+
218
+ # @api private
219
+ def count_lines
220
+ lines = render_header.scan("\n").length + 1
221
+ if @hint
222
+ lines += @hint.scan("\n").length + 1
223
+ elsif expanded?
224
+ lines += @choices.length
225
+ lines += render_footer.scan("\n").length + 1
226
+ end
227
+ lines
228
+ end
229
+
230
+ # Refresh the current input
231
+ #
232
+ # @api private
233
+ def refresh
234
+ lines = count_lines
235
+ if @hint && (!@selected || @done)
236
+ @hint = nil
237
+ @prompt.print(@prompt.clear_lines(lines, :down))
238
+ @prompt.print(@prompt.cursor.prev_line)
239
+ elsif expanded?
240
+ @prompt.print(@prompt.clear_lines(lines))
241
+ else
242
+ @prompt.print(@prompt.clear_line)
243
+ end
244
+ end
245
+
246
+ # Render help menu
247
+ #
248
+ # @api private
249
+ def render_menu
250
+ output = "\n"
251
+ @choices.each do |choice|
252
+ chosen = %(#{choice.key} - #{choice.name})
253
+ if @selected && @selected.key == choice.key
254
+ chosen = @prompt.decorate(chosen, @active_color)
255
+ end
256
+ output << ' ' + chosen + "\n"
257
+ end
258
+ output
259
+ end
260
+
261
+ def setup_defaults
262
+ validate_choices
263
+ end
264
+
265
+ def validate_choices
266
+ errors = []
267
+ keys = []
268
+ @choices.each do |choice|
269
+ if choice.key.nil?
270
+ errors << "Choice #{choice.name} is missing a :key attribute"
271
+ next
272
+ end
273
+ if choice.key.length != 1
274
+ errors << "Choice key `#{choice.key}` is more than one character long."
275
+ end
276
+ if choice.key.to_s == 'h'
277
+ errors << "Choice key `#{choice.key}` is reserved for help menu."
278
+ end
279
+ if keys.include?(choice.key)
280
+ errors << "Choice key `#{choice.key}` is a duplicate."
281
+ end
282
+ keys << choice.key if choice.key
283
+ end
284
+ errors.each { |err| fail ConfigurationError, err }
285
+ end
286
+ end # Expander
287
+ end # Prompt
288
+ end # TTY
@@ -25,15 +25,17 @@ module TTY
25
25
  # @api public
26
26
  def initialize(prompt, options = {})
27
27
  @prompt = prompt
28
- @first_render = true
29
- @done = false
28
+ @prefix = options.fetch(:prefix) { @prompt.prefix }
30
29
  @enum = options.fetch(:enum) { nil }
31
30
  @default = Array[options.fetch(:default) { 1 }]
32
31
  @active = @default.first
33
32
  @choices = Choices.new
34
- @color = options.fetch(:color) { :green }
33
+ @active_color = options.fetch(:active_color) { @prompt.active_color }
34
+ @help_color = options.fetch(:help_color) { @prompt.help_color }
35
35
  @marker = options.fetch(:marker) { Symbols::ITEM_SELECTED }
36
36
  @help = options[:help]
37
+ @first_render = true
38
+ @done = false
37
39
 
38
40
  @prompt.subscribe(self)
39
41
  end
@@ -143,6 +145,10 @@ module TTY
143
145
  # @api private
144
146
  def validate_defaults
145
147
  @default.each do |d|
148
+ if d.nil? || d.to_s.empty?
149
+ fail ConfigurationError,
150
+ "default index must be an integer in range (1 - #{@choices.size})"
151
+ end
146
152
  if d < 1 || d > @choices.size
147
153
  fail ConfigurationError,
148
154
  "default index `#{d}` out of range (1 - #{@choices.size})"
@@ -193,7 +199,7 @@ module TTY
193
199
  #
194
200
  # @api private
195
201
  def render_question
196
- header = "#{@prompt.prefix}#{@question} #{render_header}"
202
+ header = "#{@prefix}#{@question} #{render_header}"
197
203
  @prompt.puts(header)
198
204
  @first_render = false
199
205
  @prompt.print(render_menu) unless @done
@@ -204,7 +210,7 @@ module TTY
204
210
  # @return [String]
205
211
  def help
206
212
  return @help unless @help.nil?
207
- self.class::HELP % [enumerate? ? ' or number (0-9)' : '']
213
+ self.class::HELP % [enumerate? ? " or number (1-#{@choices.size})" : '']
208
214
  end
209
215
 
210
216
  # Render initial help and selected choice
@@ -215,9 +221,9 @@ module TTY
215
221
  def render_header
216
222
  if @done
217
223
  selected_item = "#{@choices[@active - 1].name}"
218
- @prompt.decorate(selected_item, @color)
224
+ @prompt.decorate(selected_item, @active_color)
219
225
  elsif @first_render
220
- @prompt.decorate(help, :bright_black)
226
+ @prompt.decorate(help, @help_color)
221
227
  end
222
228
  end
223
229
 
@@ -230,7 +236,7 @@ module TTY
230
236
  num = enumerate? ? (index + 1).to_s + @enum + Symbols::SPACE : ''
231
237
  message = if index + 1 == @active
232
238
  selected = @marker + Symbols::SPACE + num + choice.name
233
- @prompt.decorate("#{selected}", @color)
239
+ @prompt.decorate("#{selected}", @active_color)
234
240
  else
235
241
  Symbols::SPACE * 2 + num + choice.name
236
242
  end
@@ -49,11 +49,11 @@ module TTY
49
49
  #
50
50
  # @api private
51
51
  def render_question
52
- header = "#{prompt.prefix}#{message} "
52
+ header = "#{@prefix}#{message} "
53
53
  if echo?
54
54
  masked = "#{@mask * "#{@input}".length}"
55
55
  if @done_masked && !@failure
56
- masked = @prompt.decorate(masked, @color)
56
+ masked = @prompt.decorate(masked, @active_color)
57
57
  end
58
58
  header += masked
59
59
  end
@@ -47,16 +47,26 @@ module TTY
47
47
  @active = @default.last unless @selected.empty?
48
48
  end
49
49
 
50
+ # Generate selected items names
51
+ #
52
+ # @return [String]
53
+ #
54
+ # @api private
55
+ def selected_names
56
+ @selected.map(&:name).join(', ')
57
+ end
58
+
50
59
  # Render initial help text and then currently selected choices
51
60
  #
52
61
  # @api private
53
62
  def render_header
63
+ instructions = @prompt.decorate(help, :bright_black)
54
64
  if @done
55
- @prompt.decorate(@selected.map(&:name).join(', '), :green)
65
+ @prompt.decorate(selected_names, @active_color)
56
66
  elsif @selected.size.nonzero?
57
- @selected.map(&:name).join(', ')
67
+ selected_names + (@first_render ? " #{instructions}" : '')
58
68
  elsif @first_render
59
- @prompt.decorate(help, :bright_black)
69
+ instructions
60
70
  end
61
71
  end
62
72
 
@@ -79,7 +89,7 @@ module TTY
79
89
  indicator = (index + 1 == @active) ? @marker : Symbols::SPACE
80
90
  indicator += Symbols::SPACE
81
91
  message = if @selected.include?(choice)
82
- selected = @prompt.decorate(Symbols::RADIO_CHECKED, :green)
92
+ selected = @prompt.decorate(Symbols::RADIO_CHECKED, @active_color)
83
93
  selected + Symbols::SPACE + num + choice.name
84
94
  else
85
95
  Symbols::RADIO_UNCHECKED + Symbols::SPACE + num + choice.name