tty-prompt 0.10.1 → 0.11.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.
- 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
|