tty-prompt 0.10.1 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +0 -1
- data/CHANGELOG.md +30 -0
- data/README.md +39 -9
- data/examples/echo.rb +5 -1
- data/examples/inputs.rb +10 -0
- data/examples/mask.rb +6 -2
- data/examples/multi_select.rb +1 -1
- data/examples/multi_select_paged.rb +9 -0
- data/examples/select.rb +5 -5
- data/examples/slider.rb +1 -1
- data/lib/tty-prompt.rb +2 -36
- data/lib/tty/prompt.rb +49 -8
- data/lib/tty/prompt/choices.rb +2 -0
- data/lib/tty/prompt/confirm_question.rb +6 -1
- data/lib/tty/prompt/converter_dsl.rb +9 -6
- data/lib/tty/prompt/converter_registry.rb +27 -19
- data/lib/tty/prompt/converters.rb +16 -22
- data/lib/tty/prompt/enum_list.rb +8 -4
- data/lib/tty/prompt/enum_paginator.rb +2 -0
- data/lib/tty/prompt/evaluator.rb +1 -1
- data/lib/tty/prompt/expander.rb +1 -1
- data/lib/tty/prompt/list.rb +21 -11
- data/lib/tty/prompt/mask_question.rb +15 -6
- data/lib/tty/prompt/multi_list.rb +12 -10
- data/lib/tty/prompt/question.rb +38 -36
- data/lib/tty/prompt/question/modifier.rb +2 -0
- data/lib/tty/prompt/question/validation.rb +5 -4
- data/lib/tty/prompt/reader.rb +104 -58
- data/lib/tty/prompt/reader/codes.rb +103 -63
- data/lib/tty/prompt/reader/console.rb +57 -0
- data/lib/tty/prompt/reader/key_event.rb +51 -88
- data/lib/tty/prompt/reader/mode.rb +5 -5
- data/lib/tty/prompt/reader/win_api.rb +29 -0
- data/lib/tty/prompt/reader/win_console.rb +49 -0
- data/lib/tty/prompt/slider.rb +10 -6
- data/lib/tty/prompt/suggestion.rb +1 -1
- data/lib/tty/prompt/symbols.rb +52 -10
- data/lib/tty/prompt/version.rb +1 -1
- data/lib/tty/{prompt/test.rb → test_prompt.rb} +2 -1
- data/spec/unit/ask_spec.rb +8 -16
- data/spec/unit/converters/convert_bool_spec.rb +1 -2
- data/spec/unit/converters/on_error_spec.rb +9 -0
- data/spec/unit/enum_paginator_spec.rb +16 -0
- data/spec/unit/enum_select_spec.rb +69 -25
- data/spec/unit/expand_spec.rb +14 -14
- data/spec/unit/mask_spec.rb +66 -29
- data/spec/unit/multi_select_spec.rb +120 -74
- data/spec/unit/new_spec.rb +5 -3
- data/spec/unit/paginator_spec.rb +16 -0
- data/spec/unit/question/default_spec.rb +2 -4
- data/spec/unit/question/echo_spec.rb +2 -3
- data/spec/unit/question/in_spec.rb +9 -14
- data/spec/unit/question/modifier/letter_case_spec.rb +32 -11
- data/spec/unit/question/modifier/whitespace_spec.rb +41 -15
- data/spec/unit/question/required_spec.rb +9 -13
- data/spec/unit/question/validate_spec.rb +7 -10
- data/spec/unit/reader/key_event_spec.rb +36 -50
- data/spec/unit/reader/publish_keypress_event_spec.rb +5 -3
- data/spec/unit/reader/read_keypress_spec.rb +8 -7
- data/spec/unit/reader/read_line_spec.rb +9 -9
- data/spec/unit/reader/read_multiline_spec.rb +8 -7
- data/spec/unit/select_spec.rb +85 -25
- data/spec/unit/slider_spec.rb +43 -16
- data/spec/unit/yes_no_spec.rb +14 -28
- data/tasks/console.rake +1 -0
- data/tty-prompt.gemspec +2 -2
- metadata +14 -7
@@ -48,6 +48,7 @@ module TTY
|
|
48
48
|
#
|
49
49
|
# @api public
|
50
50
|
def self.letter_case(mod, value)
|
51
|
+
return value unless value.is_a?(String)
|
51
52
|
case mod
|
52
53
|
when :up, :upcase, :uppercase
|
53
54
|
value.upcase
|
@@ -73,6 +74,7 @@ module TTY
|
|
73
74
|
#
|
74
75
|
# @api public
|
75
76
|
def self.whitespace(mod, value)
|
77
|
+
return value unless value.is_a?(String)
|
76
78
|
case mod
|
77
79
|
when :trim, :strip
|
78
80
|
value.strip
|
@@ -5,11 +5,12 @@ module TTY
|
|
5
5
|
class Question
|
6
6
|
# A class representing question validation.
|
7
7
|
class Validation
|
8
|
-
|
9
|
-
|
8
|
+
# Available validator names
|
10
9
|
VALIDATORS = {
|
11
10
|
email: /^[a-z0-9._%+-]+@([a-z0-9-]+\.)+[a-z]{2,6}$/i
|
12
|
-
}
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
attr_reader :pattern
|
13
14
|
|
14
15
|
# Initialize a Validation
|
15
16
|
#
|
@@ -37,7 +38,7 @@ module TTY
|
|
37
38
|
when Regexp
|
38
39
|
Regexp.new(pattern.to_s)
|
39
40
|
else
|
40
|
-
|
41
|
+
raise ValidationCoercion, "Wrong type, got #{pattern.class}"
|
41
42
|
end
|
42
43
|
end
|
43
44
|
|
data/lib/tty/prompt/reader.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
require 'wisper'
|
4
|
-
require '
|
5
|
-
|
4
|
+
require 'rbconfig'
|
5
|
+
|
6
|
+
require_relative 'reader/key_event'
|
7
|
+
require_relative 'reader/console'
|
8
|
+
require_relative 'reader/win_console'
|
6
9
|
|
7
10
|
module TTY
|
8
11
|
# A class responsible for shell prompt interactions.
|
@@ -14,92 +17,101 @@ module TTY
|
|
14
17
|
# @api private
|
15
18
|
class Reader
|
16
19
|
include Wisper::Publisher
|
20
|
+
|
17
21
|
# Raised when the user hits the interrupt key(Control-C)
|
18
22
|
#
|
19
23
|
# @api public
|
20
24
|
InputInterrupt = Class.new(StandardError)
|
21
25
|
|
22
|
-
attr_reader :mode
|
23
|
-
|
24
26
|
attr_reader :input
|
25
27
|
|
26
28
|
attr_reader :output
|
27
29
|
|
28
|
-
|
30
|
+
attr_reader :env
|
31
|
+
|
32
|
+
# Key codes
|
29
33
|
CARRIAGE_RETURN = 13
|
30
34
|
NEWLINE = 10
|
31
35
|
BACKSPACE = 127
|
32
36
|
DELETE = 8
|
33
37
|
|
34
|
-
CSI = "\e[".freeze
|
35
|
-
|
36
38
|
# Initialize a Reader
|
37
39
|
#
|
38
40
|
# @api public
|
39
|
-
def initialize(input, output, options = {})
|
41
|
+
def initialize(input = $stdin, output = $stdout, options = {})
|
40
42
|
@input = input
|
41
43
|
@output = output
|
42
|
-
@mode = Mode.new
|
43
44
|
@interrupt = options.fetch(:interrupt) { :error }
|
45
|
+
@env = options.fetch(:env) { ENV }
|
46
|
+
@console = windows? ? WinConsole.new(input) : Console.new(input)
|
44
47
|
end
|
45
48
|
|
46
49
|
# Get input in unbuffered mode.
|
47
50
|
#
|
48
51
|
# @example
|
49
|
-
#
|
52
|
+
# unbufferred do
|
50
53
|
# ...
|
51
54
|
# end
|
52
55
|
#
|
53
|
-
# @return [String]
|
54
|
-
#
|
55
56
|
# @api public
|
56
|
-
def
|
57
|
+
def unbufferred(&block)
|
57
58
|
bufferring = output.sync
|
58
59
|
# Immediately flush output
|
59
60
|
output.sync = true
|
60
|
-
|
61
|
+
block[] if block_given?
|
62
|
+
ensure
|
61
63
|
output.sync = bufferring
|
62
|
-
value
|
63
64
|
end
|
64
65
|
|
65
|
-
# Read a
|
66
|
-
#
|
66
|
+
# Read a keypress including invisible multibyte codes
|
67
|
+
# and return a character as a string.
|
68
|
+
# Nothing is echoed to the console. This call will block for a
|
69
|
+
# single keypress, but will not wait for Enter to be pressed.
|
67
70
|
#
|
68
|
-
# @param [
|
71
|
+
# @param [Hash[Symbol]] options
|
72
|
+
# @option options [Boolean] echo
|
69
73
|
# whether to echo chars back or not, defaults to false
|
74
|
+
# @option options [Boolean] raw
|
75
|
+
# whenther raw mode enabled, defaults to true
|
70
76
|
#
|
71
77
|
# @return [String]
|
72
78
|
#
|
73
79
|
# @api public
|
74
|
-
def read_keypress(
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
80
|
+
def read_keypress(options = {})
|
81
|
+
opts = { echo: false, raw: true }.merge(options)
|
82
|
+
codes = unbufferred { get_codes(opts) }
|
83
|
+
char = codes ? codes.pack('U*') : nil
|
84
|
+
|
85
|
+
trigger_key_event(char) if char
|
86
|
+
handle_interrupt if char == @console.keys[:ctrl_c]
|
87
|
+
char
|
85
88
|
end
|
89
|
+
alias read_char read_keypress
|
86
90
|
|
87
|
-
#
|
91
|
+
# Get input code points
|
88
92
|
#
|
89
|
-
# @
|
90
|
-
#
|
93
|
+
# @param [Hash[Symbol]] options
|
94
|
+
# @param [Array[Integer]] codes
|
91
95
|
#
|
92
|
-
# @return [
|
96
|
+
# @return [Array[Integer]]
|
93
97
|
#
|
94
|
-
# @api
|
95
|
-
def
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
98
|
+
# @api private
|
99
|
+
def get_codes(options = {}, codes = [])
|
100
|
+
opts = { echo: true, raw: false }.merge(options)
|
101
|
+
char = @console.get_char(opts)
|
102
|
+
return if char.nil?
|
103
|
+
codes << char.ord
|
104
|
+
|
105
|
+
condition = proc { |escape|
|
106
|
+
(codes - escape).empty? ||
|
107
|
+
(escape - codes).empty? &&
|
108
|
+
!(64..126).include?(codes.last)
|
109
|
+
}
|
110
|
+
|
111
|
+
while @console.escape_codes.any?(&condition)
|
112
|
+
get_codes(options, codes)
|
101
113
|
end
|
102
|
-
|
114
|
+
codes
|
103
115
|
end
|
104
116
|
|
105
117
|
# Get a single line from STDIN. Each key pressed is echoed
|
@@ -112,19 +124,28 @@ module TTY
|
|
112
124
|
# @return [String]
|
113
125
|
#
|
114
126
|
# @api public
|
115
|
-
def read_line(
|
127
|
+
def read_line(options = {})
|
128
|
+
opts = { echo: true, raw: false }.merge(options)
|
116
129
|
line = ''
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
130
|
+
backspaces = 0
|
131
|
+
delete_char = proc { |c| c == BACKSPACE || c == DELETE }
|
132
|
+
|
133
|
+
while (codes = get_codes(opts)) && (code = codes[0])
|
134
|
+
char = codes.pack('U*')
|
135
|
+
trigger_key_event(char)
|
136
|
+
|
137
|
+
if delete_char[code]
|
138
|
+
line.slice!(-1, 1)
|
139
|
+
backspaces -= 1
|
140
|
+
else
|
141
|
+
line << char
|
142
|
+
backspaces = line.size
|
143
|
+
end
|
144
|
+
|
145
|
+
break if (code == CARRIAGE_RETURN || code == NEWLINE)
|
146
|
+
|
147
|
+
if delete_char[code] && opts[:echo]
|
148
|
+
output.print(' ' + (backspaces >= 0 ? "\b" : ''))
|
128
149
|
end
|
129
150
|
end
|
130
151
|
line
|
@@ -152,18 +173,33 @@ module TTY
|
|
152
173
|
response
|
153
174
|
end
|
154
175
|
|
176
|
+
# Expose event broadcasting
|
177
|
+
#
|
178
|
+
# @api public
|
179
|
+
def trigger(event, *args)
|
180
|
+
publish(event, *args)
|
181
|
+
end
|
182
|
+
|
155
183
|
# Publish event
|
156
184
|
#
|
157
|
-
# @param [String]
|
185
|
+
# @param [String] char
|
158
186
|
# the key pressed
|
159
187
|
#
|
160
188
|
# @return [nil]
|
161
189
|
#
|
162
190
|
# @api public
|
163
|
-
def
|
164
|
-
event = KeyEvent.from(
|
165
|
-
|
166
|
-
|
191
|
+
def trigger_key_event(char)
|
192
|
+
event = KeyEvent.from(@console.keys, char)
|
193
|
+
trigger(:"key#{event.key.name}", event) if event.trigger?
|
194
|
+
trigger(:keypress, event)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Inspect class name and public attributes
|
198
|
+
# @return [String]
|
199
|
+
#
|
200
|
+
# @api public
|
201
|
+
def inspect
|
202
|
+
"#<#{self.class}: @input=#{input}, @output=#{output}>"
|
167
203
|
end
|
168
204
|
|
169
205
|
private
|
@@ -185,6 +221,16 @@ module TTY
|
|
185
221
|
raise InputInterrupt
|
186
222
|
end
|
187
223
|
end
|
224
|
+
|
225
|
+
# Check if Windowz mode
|
226
|
+
#
|
227
|
+
# @return [Boolean]
|
228
|
+
#
|
229
|
+
# @api public
|
230
|
+
def windows?
|
231
|
+
return false if env["TTY_TEST"] == true
|
232
|
+
::File::ALT_SEPARATOR == "\\"
|
233
|
+
end
|
188
234
|
end # Reader
|
189
235
|
end # Prompt
|
190
236
|
end # TTY
|
@@ -4,77 +4,117 @@ module TTY
|
|
4
4
|
class Prompt
|
5
5
|
class Reader
|
6
6
|
module Codes
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
7
|
+
def ctrl_keys
|
8
|
+
{
|
9
|
+
ctrl_a: ?\C-a,
|
10
|
+
ctrl_b: ?\C-b,
|
11
|
+
ctrl_c: ?\C-c,
|
12
|
+
ctrl_d: ?\C-d,
|
13
|
+
ctrl_e: ?\C-e,
|
14
|
+
ctrl_f: ?\C-f,
|
15
|
+
ctrl_g: ?\C-g,
|
16
|
+
ctrl_h: ?\C-h,
|
17
|
+
ctrl_i: ?\C-i,
|
18
|
+
ctrl_j: ?\C-j,
|
19
|
+
ctrl_k: ?\C-k,
|
20
|
+
ctrl_l: ?\C-l,
|
21
|
+
ctrl_m: ?\C-m,
|
22
|
+
ctrl_n: ?\C-n,
|
23
|
+
ctrl_o: ?\C-o,
|
24
|
+
ctrl_p: ?\C-p,
|
25
|
+
ctrl_q: ?\C-q,
|
26
|
+
ctrl_r: ?\C-r,
|
27
|
+
ctrl_s: ?\C-s,
|
28
|
+
ctrl_t: ?\C-t,
|
29
|
+
ctrl_u: ?\C-u,
|
30
|
+
ctrl_v: ?\C-v,
|
31
|
+
ctrl_w: ?\C-w,
|
32
|
+
ctrl_x: ?\C-x,
|
33
|
+
ctrl_y: ?\C-y,
|
34
|
+
ctrl_z: ?\C-z
|
35
|
+
}
|
36
|
+
end
|
37
|
+
module_function :ctrl_keys
|
14
38
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
39
|
+
def keys
|
40
|
+
{
|
41
|
+
tab: "\t",
|
42
|
+
enter: "\n",
|
43
|
+
return: "\r",
|
44
|
+
escape: "\e",
|
45
|
+
space: " ",
|
46
|
+
backspace: ?\C-?,
|
47
|
+
insert: "\e[2~",
|
48
|
+
delete: "\e[3~",
|
49
|
+
page_up: "\e[5~",
|
50
|
+
page_down: "\e[6~",
|
23
51
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
KEY_DELETE_XTERM = "O3"
|
52
|
+
up: "\e[A",
|
53
|
+
down: "\e[B",
|
54
|
+
right: "\e[C",
|
55
|
+
left: "\e[D",
|
56
|
+
clear: "\e[E",
|
57
|
+
end: "\e[F",
|
58
|
+
home: "\e[H",
|
32
59
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
KEY_CLEAR_SHIFT = "[e"
|
60
|
+
f1_xterm: "\eOP",
|
61
|
+
f2_xterm: "\eOQ",
|
62
|
+
f3_xterm: "\eOR",
|
63
|
+
f4_xterm: "\eOS",
|
38
64
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
65
|
+
f1: "\e[11~",
|
66
|
+
f2: "\e[12~",
|
67
|
+
f3: "\e[13~",
|
68
|
+
f4: "\e[14~",
|
69
|
+
f5: "\e[15~",
|
70
|
+
f6: "\e[17~",
|
71
|
+
f7: "\e[18~",
|
72
|
+
f8: "\e[19~",
|
73
|
+
f9: "\e[20~",
|
74
|
+
f10: "\e[21~",
|
75
|
+
f11: "\e[23~",
|
76
|
+
f12: "\e[24~"
|
77
|
+
}.merge(ctrl_keys)
|
78
|
+
end
|
79
|
+
module_function :keys
|
44
80
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
81
|
+
def win_keys
|
82
|
+
{
|
83
|
+
tab: "\t",
|
84
|
+
enter: "\r",
|
85
|
+
return: "\r",
|
86
|
+
escape: "\e",
|
87
|
+
space: " ",
|
88
|
+
backspace: "\b",
|
89
|
+
insert: [224, 82].pack('U*'),
|
90
|
+
delete: [224, 83].pack('U*'),
|
91
|
+
page_up: [224, 73].pack('U*'),
|
92
|
+
page_down: [224, 81].pack('U*'),
|
53
93
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
94
|
+
up: [224, 72].pack('U*'),
|
95
|
+
down: [224, 80].pack('U*'),
|
96
|
+
right: [224, 77].pack('U*'),
|
97
|
+
left: [224, 75].pack('U*'),
|
98
|
+
clear: [224, 83].pack('U*'),
|
99
|
+
end: [224, 79].pack('U*'),
|
100
|
+
home: [224, 71].pack('U*'),
|
58
101
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
102
|
+
f1: "\x00;",
|
103
|
+
f2: "\x00<",
|
104
|
+
f3: "\x00",
|
105
|
+
f4: "\x00=",
|
106
|
+
f5: "\x00?",
|
107
|
+
f6: "\x00@",
|
108
|
+
f7: "\x00A",
|
109
|
+
f8: "\x00B",
|
110
|
+
f9: "\x00C",
|
111
|
+
f10: "\x00D",
|
112
|
+
f11: "\x00\x85",
|
113
|
+
f12: "\x00\x86"
|
114
|
+
}.merge(ctrl_keys)
|
115
|
+
end
|
116
|
+
module_function :win_keys
|
63
117
|
|
64
|
-
F1_WIN = "[[A"
|
65
|
-
F2_WIN = "[[B"
|
66
|
-
F3_WIN = "[[C"
|
67
|
-
F4_WIN = "[[D"
|
68
|
-
F5_WIN = "[[E"
|
69
|
-
|
70
|
-
F5 = "[15~"
|
71
|
-
F6 = "[17~"
|
72
|
-
F7 = "[18~"
|
73
|
-
F8 = "[19~"
|
74
|
-
F9 = "[20~"
|
75
|
-
F10 = "[21~"
|
76
|
-
F11 = "[23~"
|
77
|
-
F12 = "[24~"
|
78
118
|
end # Codes
|
79
119
|
end # Reader
|
80
120
|
end # Prompt
|