tty-prompt 0.11.0 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +66 -7
- data/examples/key_events.rb +11 -0
- data/examples/keypress.rb +3 -5
- data/examples/multiline.rb +9 -0
- data/examples/pause.rb +7 -0
- data/lib/tty/prompt.rb +82 -44
- data/lib/tty/prompt/confirm_question.rb +20 -36
- data/lib/tty/prompt/enum_list.rb +32 -23
- data/lib/tty/prompt/expander.rb +35 -31
- data/lib/tty/prompt/keypress.rb +91 -0
- data/lib/tty/prompt/list.rb +38 -23
- data/lib/tty/prompt/mask_question.rb +4 -7
- data/lib/tty/prompt/multi_list.rb +3 -1
- data/lib/tty/prompt/multiline.rb +71 -0
- data/lib/tty/prompt/question.rb +33 -35
- data/lib/tty/prompt/reader.rb +154 -38
- data/lib/tty/prompt/reader/codes.rb +4 -4
- data/lib/tty/prompt/reader/console.rb +1 -1
- data/lib/tty/prompt/reader/history.rb +145 -0
- data/lib/tty/prompt/reader/key_event.rb +4 -0
- data/lib/tty/prompt/reader/line.rb +162 -0
- data/lib/tty/prompt/reader/mode.rb +2 -2
- data/lib/tty/prompt/reader/win_console.rb +5 -1
- data/lib/tty/prompt/slider.rb +18 -12
- data/lib/tty/prompt/timeout.rb +48 -0
- data/lib/tty/prompt/version.rb +1 -1
- data/spec/unit/ask_spec.rb +15 -0
- data/spec/unit/converters/convert_bool_spec.rb +1 -0
- data/spec/unit/keypress_spec.rb +35 -6
- data/spec/unit/multi_select_spec.rb +18 -0
- data/spec/unit/multiline_spec.rb +67 -9
- data/spec/unit/question/default_spec.rb +1 -0
- data/spec/unit/question/echo_spec.rb +8 -0
- data/spec/unit/question/in_spec.rb +13 -0
- data/spec/unit/question/required_spec.rb +31 -2
- data/spec/unit/question/validate_spec.rb +39 -9
- data/spec/unit/reader/history_spec.rb +172 -0
- data/spec/unit/reader/key_event_spec.rb +12 -8
- data/spec/unit/reader/line_spec.rb +110 -0
- data/spec/unit/reader/publish_keypress_event_spec.rb +11 -0
- data/spec/unit/reader/read_line_spec.rb +32 -2
- data/spec/unit/reader/read_multiline_spec.rb +21 -7
- data/spec/unit/select_spec.rb +40 -1
- data/spec/unit/yes_no_spec.rb +48 -4
- metadata +14 -3
- data/lib/tty/prompt/history.rb +0 -16
@@ -76,12 +76,14 @@ module TTY
|
|
76
76
|
# @return [Array[nil,Object]]
|
77
77
|
#
|
78
78
|
# @api private
|
79
|
-
def
|
79
|
+
def answer
|
80
80
|
@selected.map(&:value)
|
81
81
|
end
|
82
82
|
|
83
83
|
# Render menu with choices to select from
|
84
84
|
#
|
85
|
+
# @return [String]
|
86
|
+
#
|
85
87
|
# @api private
|
86
88
|
def render_menu
|
87
89
|
output = ''
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'question'
|
4
|
+
require_relative 'symbols'
|
5
|
+
|
6
|
+
module TTY
|
7
|
+
class Prompt
|
8
|
+
# A prompt responsible for multi line user input
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class Multiline < Question
|
12
|
+
HELP = '(Press CTRL-D or CTRL-Z to finish)'.freeze
|
13
|
+
|
14
|
+
def initialize(prompt, options = {})
|
15
|
+
super
|
16
|
+
@help = options[:help] || self.class::HELP
|
17
|
+
@first_render = true
|
18
|
+
@lines_count = 0
|
19
|
+
|
20
|
+
@prompt.subscribe(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Provide help information
|
24
|
+
#
|
25
|
+
# @return [String]
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
def help(value = (not_set = true))
|
29
|
+
return @help if not_set
|
30
|
+
@help = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def read_input
|
34
|
+
@prompt.read_multiline
|
35
|
+
end
|
36
|
+
|
37
|
+
def keyreturn(*)
|
38
|
+
@lines_count += 1
|
39
|
+
end
|
40
|
+
alias keyenter keyreturn
|
41
|
+
|
42
|
+
def render_question
|
43
|
+
header = "#{@prefix}#{message} "
|
44
|
+
if !echo?
|
45
|
+
header
|
46
|
+
elsif @done
|
47
|
+
header += @prompt.decorate("#{@input}", @active_color)
|
48
|
+
elsif @first_render
|
49
|
+
header += @prompt.decorate(help, @help_color)
|
50
|
+
@first_render = false
|
51
|
+
end
|
52
|
+
header += "\n"
|
53
|
+
header
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_input(question)
|
57
|
+
@lines = read_input
|
58
|
+
@input = "#{@lines.first.strip} ..." unless @lines.first.to_s.empty?
|
59
|
+
if Utils.blank?(@input)
|
60
|
+
@input = default? ? default : nil
|
61
|
+
end
|
62
|
+
@evaluator.(@lines)
|
63
|
+
end
|
64
|
+
|
65
|
+
def refresh(lines)
|
66
|
+
size = @lines_count + lines + 1
|
67
|
+
@prompt.clear_lines(size)
|
68
|
+
end
|
69
|
+
end # Multiline
|
70
|
+
end # Prompt
|
71
|
+
end # TTY
|
data/lib/tty/prompt/question.rb
CHANGED
@@ -16,7 +16,12 @@ module TTY
|
|
16
16
|
class Question
|
17
17
|
include Checks
|
18
18
|
|
19
|
-
UndefinedSetting =
|
19
|
+
UndefinedSetting = Class.new do
|
20
|
+
def to_s
|
21
|
+
"undefined"
|
22
|
+
end
|
23
|
+
alias inspect to_s
|
24
|
+
end
|
20
25
|
|
21
26
|
# Store question message
|
22
27
|
# @api public
|
@@ -38,7 +43,6 @@ module TTY
|
|
38
43
|
@in = options.fetch(:in) { UndefinedSetting }
|
39
44
|
@modifier = options.fetch(:modifier) { [] }
|
40
45
|
@validation = options.fetch(:validation) { UndefinedSetting }
|
41
|
-
@read = options.fetch(:read) { UndefinedSetting }
|
42
46
|
@convert = options.fetch(:convert) { UndefinedSetting }
|
43
47
|
@active_color = options.fetch(:active_color) { @prompt.active_color }
|
44
48
|
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
@@ -103,22 +107,25 @@ module TTY
|
|
103
107
|
def render
|
104
108
|
@errors = []
|
105
109
|
until @done
|
106
|
-
|
107
|
-
|
110
|
+
question = render_question
|
111
|
+
@prompt.print(question)
|
112
|
+
result = process_input(question)
|
108
113
|
if result.failure?
|
109
114
|
@errors = result.errors
|
110
|
-
render_error(result.errors)
|
115
|
+
@prompt.print(render_error(result.errors))
|
111
116
|
else
|
112
117
|
@done = true
|
113
118
|
end
|
114
|
-
refresh(lines)
|
119
|
+
@prompt.print(refresh(question.lines.count))
|
115
120
|
end
|
116
|
-
render_question
|
121
|
+
@prompt.print(render_question)
|
117
122
|
convert_result(result.value)
|
118
123
|
end
|
119
124
|
|
120
125
|
# Render question
|
121
126
|
#
|
127
|
+
# @return [String]
|
128
|
+
#
|
122
129
|
# @api private
|
123
130
|
def render_question
|
124
131
|
header = "#{@prefix}#{message} "
|
@@ -129,17 +136,15 @@ module TTY
|
|
129
136
|
elsif default? && !Utils.blank?(@default)
|
130
137
|
header += @prompt.decorate("(#{default})", @help_color) + ' '
|
131
138
|
end
|
132
|
-
@
|
133
|
-
|
134
|
-
|
135
|
-
header.lines.count + (@done ? 1 : 0)
|
139
|
+
header << "\n" if @done
|
140
|
+
header
|
136
141
|
end
|
137
142
|
|
138
143
|
# Decide how to handle input from user
|
139
144
|
#
|
140
145
|
# @api private
|
141
|
-
def process_input
|
142
|
-
@input = read_input
|
146
|
+
def process_input(question)
|
147
|
+
@input = read_input(question)
|
143
148
|
if Utils.blank?(@input)
|
144
149
|
@input = default? ? default : nil
|
145
150
|
end
|
@@ -149,43 +154,43 @@ module TTY
|
|
149
154
|
# Process input
|
150
155
|
#
|
151
156
|
# @api private
|
152
|
-
def read_input
|
153
|
-
|
154
|
-
when :keypress
|
155
|
-
@prompt.read_keypress
|
156
|
-
when :multiline
|
157
|
-
@prompt.read_multiline.each(&:chomp!)
|
158
|
-
else
|
159
|
-
@prompt.read_line(echo: echo).chomp
|
160
|
-
end
|
157
|
+
def read_input(question)
|
158
|
+
@prompt.read_line(question, echo: echo).chomp
|
161
159
|
end
|
162
160
|
|
163
161
|
# Handle error condition
|
164
162
|
#
|
163
|
+
# @return [String]
|
164
|
+
#
|
165
165
|
# @api private
|
166
166
|
def render_error(errors)
|
167
|
-
errors.
|
167
|
+
errors.reduce('') do |acc, err|
|
168
168
|
newline = (@echo ? '' : "\n")
|
169
|
-
|
169
|
+
acc << newline + @prompt.decorate('>>', :red) + ' ' + err
|
170
|
+
acc
|
170
171
|
end
|
171
172
|
end
|
172
173
|
|
173
174
|
# Determine area of the screen to clear
|
174
175
|
#
|
175
|
-
# @param [
|
176
|
+
# @param [Integer] lines
|
177
|
+
# number of lines to clear
|
178
|
+
#
|
179
|
+
# @return [String]
|
176
180
|
#
|
177
181
|
# @api private
|
178
182
|
def refresh(lines)
|
183
|
+
output = ''
|
179
184
|
if @done
|
180
185
|
if @errors.count.zero? && @echo
|
181
|
-
@prompt.
|
186
|
+
output << @prompt.cursor.up(lines)
|
182
187
|
else
|
183
188
|
lines += @errors.count
|
184
189
|
end
|
185
190
|
else
|
186
|
-
@prompt.
|
191
|
+
output << @prompt.cursor.up(lines)
|
187
192
|
end
|
188
|
-
@prompt.
|
193
|
+
output + @prompt.clear_lines(lines)
|
189
194
|
end
|
190
195
|
|
191
196
|
# Convert value to expected type
|
@@ -201,13 +206,6 @@ module TTY
|
|
201
206
|
end
|
202
207
|
end
|
203
208
|
|
204
|
-
# Set reader type
|
205
|
-
#
|
206
|
-
# @api public
|
207
|
-
def read(value)
|
208
|
-
@read = value
|
209
|
-
end
|
210
|
-
|
211
209
|
# Specify answer conversion
|
212
210
|
#
|
213
211
|
# @api public
|
data/lib/tty/prompt/reader.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
require 'wisper'
|
4
4
|
require 'rbconfig'
|
5
5
|
|
6
|
+
require_relative 'reader/history'
|
7
|
+
require_relative 'reader/line'
|
6
8
|
require_relative 'reader/key_event'
|
7
9
|
require_relative 'reader/console'
|
8
10
|
require_relative 'reader/win_console'
|
@@ -29,6 +31,11 @@ module TTY
|
|
29
31
|
|
30
32
|
attr_reader :env
|
31
33
|
|
34
|
+
attr_reader :track_history
|
35
|
+
alias track_history? track_history
|
36
|
+
|
37
|
+
attr_reader :console
|
38
|
+
|
32
39
|
# Key codes
|
33
40
|
CARRIAGE_RETURN = 13
|
34
41
|
NEWLINE = 10
|
@@ -37,13 +44,42 @@ module TTY
|
|
37
44
|
|
38
45
|
# Initialize a Reader
|
39
46
|
#
|
47
|
+
# @param [IO] input
|
48
|
+
# the input stream
|
49
|
+
# @param [IO] output
|
50
|
+
# the output stream
|
51
|
+
# @param [Hash] options
|
52
|
+
# @option options [Symbol] :interrupt
|
53
|
+
# handling of Ctrl+C key out of :signal, :exit, :noop
|
54
|
+
# @option options [Boolean] :track_history
|
55
|
+
# disable line history tracking, true by default
|
56
|
+
#
|
40
57
|
# @api public
|
41
58
|
def initialize(input = $stdin, output = $stdout, options = {})
|
42
59
|
@input = input
|
43
60
|
@output = output
|
44
61
|
@interrupt = options.fetch(:interrupt) { :error }
|
45
62
|
@env = options.fetch(:env) { ENV }
|
46
|
-
@
|
63
|
+
@track_history = options.fetch(:track_history) { true }
|
64
|
+
@console = select_console(input)
|
65
|
+
@history = History.new do |h|
|
66
|
+
h.duplicates = false
|
67
|
+
h.exclude = proc { |line| line.strip == '' }
|
68
|
+
end
|
69
|
+
@stop = false # gathering input
|
70
|
+
|
71
|
+
subscribe(self)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Select appropriate console
|
75
|
+
#
|
76
|
+
# @api private
|
77
|
+
def select_console(input)
|
78
|
+
if windows? && !env['TTY_TEST']
|
79
|
+
WinConsole.new(input)
|
80
|
+
else
|
81
|
+
Console.new(input)
|
82
|
+
end
|
47
83
|
end
|
48
84
|
|
49
85
|
# Get input in unbuffered mode.
|
@@ -83,7 +119,7 @@ module TTY
|
|
83
119
|
char = codes ? codes.pack('U*') : nil
|
84
120
|
|
85
121
|
trigger_key_event(char) if char
|
86
|
-
handle_interrupt if char ==
|
122
|
+
handle_interrupt if char == console.keys[:ctrl_c]
|
87
123
|
char
|
88
124
|
end
|
89
125
|
alias read_char read_keypress
|
@@ -98,7 +134,7 @@ module TTY
|
|
98
134
|
# @api private
|
99
135
|
def get_codes(options = {}, codes = [])
|
100
136
|
opts = { echo: true, raw: false }.merge(options)
|
101
|
-
char =
|
137
|
+
char = console.get_char(opts)
|
102
138
|
return if char.nil?
|
103
139
|
codes << char.ord
|
104
140
|
|
@@ -108,7 +144,7 @@ module TTY
|
|
108
144
|
!(64..126).include?(codes.last)
|
109
145
|
}
|
110
146
|
|
111
|
-
while
|
147
|
+
while console.escape_codes.any?(&condition)
|
112
148
|
get_codes(options, codes)
|
113
149
|
end
|
114
150
|
codes
|
@@ -118,60 +154,110 @@ module TTY
|
|
118
154
|
# back to the shell. The input terminates when enter or
|
119
155
|
# return key is pressed.
|
120
156
|
#
|
157
|
+
# @param [String] prompt
|
158
|
+
# the prompt to display before input
|
159
|
+
#
|
121
160
|
# @param [Boolean] echo
|
122
161
|
# if true echo back characters, output nothing otherwise
|
123
162
|
#
|
124
163
|
# @return [String]
|
125
164
|
#
|
126
165
|
# @api public
|
127
|
-
def read_line(
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
166
|
+
def read_line(*args)
|
167
|
+
options = args.last.respond_to?(:to_hash) ? args.pop : {}
|
168
|
+
prompt = args.empty? ? '' : args.pop
|
169
|
+
opts = { echo: true, raw: true }.merge(options)
|
170
|
+
line = Line.new('')
|
171
|
+
ctrls = console.keys.keys.grep(/ctrl/)
|
172
|
+
clear_line = "\e[2K\e[1G"
|
132
173
|
|
133
|
-
while (codes = get_codes(opts)) && (code = codes[0])
|
174
|
+
while (codes = unbufferred { get_codes(opts) }) && (code = codes[0])
|
134
175
|
char = codes.pack('U*')
|
135
176
|
trigger_key_event(char)
|
136
177
|
|
137
|
-
if
|
138
|
-
line.
|
139
|
-
|
178
|
+
if console.keys[:backspace] == char || BACKSPACE == code
|
179
|
+
next if line.start?
|
180
|
+
line.left
|
181
|
+
line.delete
|
182
|
+
elsif console.keys[:delete] == char || DELETE == code
|
183
|
+
line.delete
|
184
|
+
elsif [console.keys[:ctrl_d],
|
185
|
+
console.keys[:ctrl_z]].include?(char)
|
186
|
+
break
|
187
|
+
elsif console.keys[:ctrl_c] == char
|
188
|
+
handle_interrupt
|
189
|
+
elsif ctrls.include?(console.keys.key(char))
|
190
|
+
# skip
|
191
|
+
elsif console.keys[:up] == char
|
192
|
+
next unless history_previous?
|
193
|
+
line.replace(history_previous)
|
194
|
+
elsif console.keys[:down] == char
|
195
|
+
line.replace(history_next? ? history_next : '')
|
196
|
+
elsif console.keys[:left] == char
|
197
|
+
line.left
|
198
|
+
elsif console.keys[:right] == char
|
199
|
+
line.right
|
140
200
|
else
|
141
|
-
|
142
|
-
|
201
|
+
if opts[:raw] && code == CARRIAGE_RETURN
|
202
|
+
char = "\n"
|
203
|
+
line.move_to_end
|
204
|
+
end
|
205
|
+
line.insert(char)
|
206
|
+
end
|
207
|
+
|
208
|
+
if opts[:raw] && opts[:echo]
|
209
|
+
output.print(clear_line)
|
210
|
+
output.print(prompt + line.to_s)
|
211
|
+
if char == "\n"
|
212
|
+
line.move_to_start
|
213
|
+
elsif !line.end?
|
214
|
+
output.print("\e[#{line.size - line.cursor}D")
|
215
|
+
end
|
143
216
|
end
|
144
217
|
|
145
218
|
break if (code == CARRIAGE_RETURN || code == NEWLINE)
|
146
219
|
|
147
|
-
if
|
148
|
-
|
220
|
+
if (console.keys[:backspace] == char || BACKSPACE == code) && opts[:echo]
|
221
|
+
if opts[:raw]
|
222
|
+
output.print("\e[1X") unless line.start?
|
223
|
+
else
|
224
|
+
output.print(?\s + (line.start? ? '' : ?\b))
|
225
|
+
end
|
149
226
|
end
|
150
227
|
end
|
151
|
-
line
|
228
|
+
add_to_history(line.to_s.rstrip) if track_history?
|
229
|
+
line.to_s
|
152
230
|
end
|
153
231
|
|
154
|
-
# Read multiple lines and
|
232
|
+
# Read multiple lines and return them in an array.
|
233
|
+
# Skip empty lines in the returned lines array.
|
234
|
+
# The input gathering is terminated by Ctrl+d or Ctrl+z.
|
235
|
+
#
|
236
|
+
# @param [String] prompt
|
237
|
+
# the prompt displayed before the input
|
155
238
|
#
|
156
239
|
# @yield [String] line
|
157
240
|
#
|
158
241
|
# @return [Array[String]]
|
159
242
|
#
|
160
243
|
# @api public
|
161
|
-
def read_multiline
|
162
|
-
|
244
|
+
def read_multiline(prompt = '')
|
245
|
+
@stop = false
|
246
|
+
lines = []
|
163
247
|
loop do
|
164
|
-
line = read_line
|
248
|
+
line = read_line(prompt)
|
165
249
|
break if !line || line == ''
|
166
|
-
next if line !~ /\S/
|
250
|
+
next if line !~ /\S/ && !@stop
|
167
251
|
if block_given?
|
168
|
-
yield(line)
|
252
|
+
yield(line) unless line.to_s.empty?
|
169
253
|
else
|
170
|
-
|
254
|
+
lines << line unless line.to_s.empty?
|
171
255
|
end
|
256
|
+
break if @stop
|
172
257
|
end
|
173
|
-
|
258
|
+
lines
|
174
259
|
end
|
260
|
+
alias read_lines read_multiline
|
175
261
|
|
176
262
|
# Expose event broadcasting
|
177
263
|
#
|
@@ -180,18 +266,35 @@ module TTY
|
|
180
266
|
publish(event, *args)
|
181
267
|
end
|
182
268
|
|
183
|
-
#
|
269
|
+
# Capture Ctrl+d and Ctrl+z key events
|
184
270
|
#
|
185
|
-
# @
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
def
|
192
|
-
|
193
|
-
|
194
|
-
|
271
|
+
# @api private
|
272
|
+
def keyctrl_d(*)
|
273
|
+
@stop = true
|
274
|
+
end
|
275
|
+
alias keyctrl_z keyctrl_d
|
276
|
+
|
277
|
+
def add_to_history(line)
|
278
|
+
@history.push(line)
|
279
|
+
end
|
280
|
+
|
281
|
+
def history_next?
|
282
|
+
@history.next?
|
283
|
+
end
|
284
|
+
|
285
|
+
def history_next
|
286
|
+
@history.next
|
287
|
+
@history.get
|
288
|
+
end
|
289
|
+
|
290
|
+
def history_previous?
|
291
|
+
@history.previous?
|
292
|
+
end
|
293
|
+
|
294
|
+
def history_previous
|
295
|
+
line = @history.get
|
296
|
+
@history.previous
|
297
|
+
line
|
195
298
|
end
|
196
299
|
|
197
300
|
# Inspect class name and public attributes
|
@@ -204,6 +307,20 @@ module TTY
|
|
204
307
|
|
205
308
|
private
|
206
309
|
|
310
|
+
# Publish event
|
311
|
+
#
|
312
|
+
# @param [String] char
|
313
|
+
# the key pressed
|
314
|
+
#
|
315
|
+
# @return [nil]
|
316
|
+
#
|
317
|
+
# @api private
|
318
|
+
def trigger_key_event(char)
|
319
|
+
event = KeyEvent.from(console.keys, char)
|
320
|
+
trigger(:"key#{event.key.name}", event) if event.trigger?
|
321
|
+
trigger(:keypress, event)
|
322
|
+
end
|
323
|
+
|
207
324
|
# Handle input interrupt based on provided value
|
208
325
|
#
|
209
326
|
# @api private
|
@@ -228,7 +345,6 @@ module TTY
|
|
228
345
|
#
|
229
346
|
# @api public
|
230
347
|
def windows?
|
231
|
-
return false if env["TTY_TEST"] == true
|
232
348
|
::File::ALT_SEPARATOR == "\\"
|
233
349
|
end
|
234
350
|
end # Reader
|