tty-prompt 0.1.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 +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +24 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +22 -0
- data/README.md +199 -0
- data/Rakefile +8 -0
- data/lib/tty-prompt.rb +15 -0
- data/lib/tty/prompt.rb +206 -0
- data/lib/tty/prompt/distance.rb +49 -0
- data/lib/tty/prompt/error.rb +26 -0
- data/lib/tty/prompt/history.rb +16 -0
- data/lib/tty/prompt/mode.rb +64 -0
- data/lib/tty/prompt/mode/echo.rb +40 -0
- data/lib/tty/prompt/mode/raw.rb +40 -0
- data/lib/tty/prompt/question.rb +338 -0
- data/lib/tty/prompt/question/modifier.rb +93 -0
- data/lib/tty/prompt/question/validation.rb +92 -0
- data/lib/tty/prompt/reader.rb +113 -0
- data/lib/tty/prompt/response.rb +252 -0
- data/lib/tty/prompt/response_delegation.rb +41 -0
- data/lib/tty/prompt/statement.rb +60 -0
- data/lib/tty/prompt/suggestion.rb +113 -0
- data/lib/tty/prompt/utils.rb +16 -0
- data/lib/tty/prompt/version.rb +7 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/unit/ask_spec.rb +77 -0
- data/spec/unit/distance/distance_spec.rb +75 -0
- data/spec/unit/error_spec.rb +30 -0
- data/spec/unit/question/argument_spec.rb +30 -0
- data/spec/unit/question/character_spec.rb +24 -0
- data/spec/unit/question/default_spec.rb +25 -0
- data/spec/unit/question/in_spec.rb +23 -0
- data/spec/unit/question/initialize_spec.rb +24 -0
- data/spec/unit/question/modifier/apply_to_spec.rb +31 -0
- data/spec/unit/question/modifier/letter_case_spec.rb +22 -0
- data/spec/unit/question/modifier/whitespace_spec.rb +33 -0
- data/spec/unit/question/modify_spec.rb +44 -0
- data/spec/unit/question/valid_spec.rb +46 -0
- data/spec/unit/question/validate_spec.rb +30 -0
- data/spec/unit/question/validation/coerce_spec.rb +24 -0
- data/spec/unit/question/validation/valid_value_spec.rb +22 -0
- data/spec/unit/reader/getc_spec.rb +42 -0
- data/spec/unit/response/read_bool_spec.rb +47 -0
- data/spec/unit/response/read_char_spec.rb +16 -0
- data/spec/unit/response/read_date_spec.rb +20 -0
- data/spec/unit/response/read_email_spec.rb +42 -0
- data/spec/unit/response/read_multiple_spec.rb +23 -0
- data/spec/unit/response/read_number_spec.rb +28 -0
- data/spec/unit/response/read_range_spec.rb +26 -0
- data/spec/unit/response/read_spec.rb +68 -0
- data/spec/unit/response/read_string_spec.rb +19 -0
- data/spec/unit/say_spec.rb +66 -0
- data/spec/unit/statement/initialize_spec.rb +19 -0
- data/spec/unit/suggest_spec.rb +33 -0
- data/spec/unit/warn_spec.rb +30 -0
- data/tasks/console.rake +10 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- data/tty-prompt.gemspec +26 -0
- metadata +194 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
class Question
|
6
|
+
# A class representing String modifications.
|
7
|
+
class Modifier
|
8
|
+
attr_reader :modifiers
|
9
|
+
private :modifiers
|
10
|
+
|
11
|
+
# Initialize a Modifier
|
12
|
+
#
|
13
|
+
# @api public
|
14
|
+
def initialize(*modifiers)
|
15
|
+
@modifiers = Array(modifiers)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Change supplied value according to the given string transformation.
|
19
|
+
# Valid settings are:
|
20
|
+
#
|
21
|
+
# @param [String] value
|
22
|
+
# the string to be modified
|
23
|
+
#
|
24
|
+
# @return [String]
|
25
|
+
#
|
26
|
+
# @api private
|
27
|
+
def apply_to(value)
|
28
|
+
modifiers.reduce(value) do |result, mod|
|
29
|
+
result = Modifier.letter_case mod, result
|
30
|
+
Modifier.whitespace mod, result
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Changes letter casing in a string according to valid modifications.
|
35
|
+
# For invalid modification option the string is preserved.
|
36
|
+
#
|
37
|
+
# @param [Symbol] mod
|
38
|
+
# the modification to change the string
|
39
|
+
#
|
40
|
+
# @option mod [Symbol] :up change to upper case
|
41
|
+
# @option mod [Symbol] :upcase change to upper case
|
42
|
+
# @option mod [Symbol] :uppercase change to upper case
|
43
|
+
# @option mod [Symbol] :down change to lower case
|
44
|
+
# @option mod [Symbol] :downcase change to lower case
|
45
|
+
# @option mod [Symbol] :capitalize change all words to start
|
46
|
+
# with uppercase case letter
|
47
|
+
#
|
48
|
+
# @return [String]
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def self.letter_case(mod, value)
|
52
|
+
case mod
|
53
|
+
when :up, :upcase, :uppercase
|
54
|
+
value.upcase
|
55
|
+
when :down, :downcase, :lowercase
|
56
|
+
value.downcase
|
57
|
+
when :capitalize
|
58
|
+
value.capitalize
|
59
|
+
else
|
60
|
+
value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Changes whitespace in a string according to valid modifications.
|
65
|
+
#
|
66
|
+
# @param [Symbol] mod
|
67
|
+
# the modification to change the string
|
68
|
+
#
|
69
|
+
# @option mod [String] :trim, :strip
|
70
|
+
# remove whitespace for the start and end
|
71
|
+
# @option mod [String] :chomp remove record separator from the end
|
72
|
+
# @option mod [String] :collapse remove any duplicate whitespace
|
73
|
+
# @option mod [String] :remove remove all whitespace
|
74
|
+
#
|
75
|
+
# @api public
|
76
|
+
def self.whitespace(mod, value)
|
77
|
+
case mod
|
78
|
+
when :trim, :strip
|
79
|
+
value.strip
|
80
|
+
when :chomp
|
81
|
+
value.chomp
|
82
|
+
when :collapse
|
83
|
+
value.gsub(/\s+/, ' ')
|
84
|
+
when :remove
|
85
|
+
value.gsub(/\s+/, '')
|
86
|
+
else
|
87
|
+
value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end # Modifier
|
91
|
+
end # Question
|
92
|
+
end # Prompt
|
93
|
+
end # TTY
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
class Question
|
6
|
+
# A class representing question validation.
|
7
|
+
class Validation
|
8
|
+
# @api private
|
9
|
+
attr_reader :validation
|
10
|
+
private :validation
|
11
|
+
|
12
|
+
# Initialize a Validation
|
13
|
+
#
|
14
|
+
# @param [Object] validation
|
15
|
+
#
|
16
|
+
# @return [undefined]
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
def initialize(validation = nil)
|
20
|
+
@validation = validation ? coerce(validation) : validation
|
21
|
+
end
|
22
|
+
|
23
|
+
# Convert validation into known type.
|
24
|
+
#
|
25
|
+
# @param [Object] validation
|
26
|
+
#
|
27
|
+
# @raise [TTY::ValidationCoercion] failed to convert validation
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
def coerce(validation)
|
31
|
+
case validation
|
32
|
+
when Proc
|
33
|
+
validation
|
34
|
+
when Regexp, String
|
35
|
+
Regexp.new(validation.to_s)
|
36
|
+
else
|
37
|
+
fail ValidationCoercion, "Wrong type, got #{validation.class}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Check if validation is required
|
42
|
+
#
|
43
|
+
# @return [Boolean]
|
44
|
+
#
|
45
|
+
# @api public
|
46
|
+
def validate?
|
47
|
+
!!validation
|
48
|
+
end
|
49
|
+
|
50
|
+
# Test if the value matches the validation
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# validation.valid_value?(value) # => true or false
|
54
|
+
#
|
55
|
+
# @param [Object] value
|
56
|
+
# the value to validate
|
57
|
+
#
|
58
|
+
# @return [undefined]
|
59
|
+
#
|
60
|
+
# @api public
|
61
|
+
def valid_value?(value)
|
62
|
+
check_validation(value)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# Check if provided value passes validation
|
68
|
+
#
|
69
|
+
# @param [String] value
|
70
|
+
#
|
71
|
+
# @raise [TTY::InvalidArgument] unkown type of argument
|
72
|
+
#
|
73
|
+
# @return [undefined]
|
74
|
+
#
|
75
|
+
# @api private
|
76
|
+
def check_validation(value)
|
77
|
+
if validate? && value
|
78
|
+
value = value.to_s
|
79
|
+
if validation.is_a?(Regexp) && validation =~ value
|
80
|
+
elsif validation.is_a?(Proc) && validation.call(value)
|
81
|
+
else
|
82
|
+
fail InvalidArgument, "Invalid input for #{value}"
|
83
|
+
end
|
84
|
+
true
|
85
|
+
else
|
86
|
+
false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end # Validation
|
90
|
+
end # Question
|
91
|
+
end # Prompt
|
92
|
+
end # TTY
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
# A class responsible for shell prompt interactions.
|
5
|
+
class Prompt
|
6
|
+
# A class responsible for reading character input from STDIN
|
7
|
+
class Reader
|
8
|
+
# @api private
|
9
|
+
attr_reader :shell
|
10
|
+
private :shell
|
11
|
+
|
12
|
+
attr_reader :mode
|
13
|
+
|
14
|
+
# Key input constants for decimal codes
|
15
|
+
CARRIAGE_RETURN = 13.freeze
|
16
|
+
NEWLINE = 10.freeze
|
17
|
+
BACKSPACE = 127.freeze
|
18
|
+
DELETE = 8.freeze
|
19
|
+
|
20
|
+
# Initialize a Reader
|
21
|
+
#
|
22
|
+
# @api public
|
23
|
+
def initialize(shell = Prompt.new)
|
24
|
+
@shell = shell
|
25
|
+
@mode = Mode.new
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get input in unbuffered mode.
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# buffer do
|
32
|
+
# ...
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# @return [String]
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def buffer(&block)
|
39
|
+
bufferring = shell.output.sync
|
40
|
+
# Immediately flush output
|
41
|
+
shell.output.sync = true
|
42
|
+
|
43
|
+
value = block.call if block_given?
|
44
|
+
|
45
|
+
shell.output.sync = bufferring
|
46
|
+
value
|
47
|
+
end
|
48
|
+
|
49
|
+
# Get a value from STDIN one key at a time. Each key press is echoed back
|
50
|
+
# to the shell masked with character(if given). The input finishes when
|
51
|
+
# enter key is pressed.
|
52
|
+
#
|
53
|
+
# @param [String] mask
|
54
|
+
# the character to use as mask
|
55
|
+
#
|
56
|
+
# @return [String]
|
57
|
+
#
|
58
|
+
# @api public
|
59
|
+
def getc(mask = (not_set = true))
|
60
|
+
value = ''
|
61
|
+
buffer do
|
62
|
+
begin
|
63
|
+
while (char = shell.input.getbyte) &&
|
64
|
+
!(char == CARRIAGE_RETURN || char == NEWLINE)
|
65
|
+
value = handle_char value, char, not_set, mask
|
66
|
+
end
|
67
|
+
ensure
|
68
|
+
mode.echo_on
|
69
|
+
end
|
70
|
+
end
|
71
|
+
value
|
72
|
+
end
|
73
|
+
|
74
|
+
# Get a value from STDIN using line input.
|
75
|
+
#
|
76
|
+
# @api public
|
77
|
+
def gets
|
78
|
+
shell.input.gets
|
79
|
+
end
|
80
|
+
|
81
|
+
# Reads at maximum +maxlen+ characters.
|
82
|
+
#
|
83
|
+
# @param [Integer] maxlen
|
84
|
+
#
|
85
|
+
# @api public
|
86
|
+
def readpartial(maxlen)
|
87
|
+
shell.input.readpartial(maxlen)
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Handle single character by appending to or removing from output
|
93
|
+
#
|
94
|
+
# @api private
|
95
|
+
def handle_char(value, char, not_set, mask)
|
96
|
+
if char == BACKSPACE || char == DELETE
|
97
|
+
value.slice!(-1, 1) unless value.empty?
|
98
|
+
else
|
99
|
+
print_char char, not_set, mask
|
100
|
+
value << char
|
101
|
+
end
|
102
|
+
value
|
103
|
+
end
|
104
|
+
|
105
|
+
# Print out character back to shell STDOUT
|
106
|
+
#
|
107
|
+
# @api private
|
108
|
+
def print_char(char, not_set, mask)
|
109
|
+
shell.output.putc((not_set || !mask) ? char : mask)
|
110
|
+
end
|
111
|
+
end # Reader
|
112
|
+
end # Prompt
|
113
|
+
end # TTY
|
@@ -0,0 +1,252 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
# A class responsible for shell prompt interactions
|
5
|
+
class Prompt
|
6
|
+
# A class representing a shell response
|
7
|
+
class Response
|
8
|
+
VALID_TYPES = [
|
9
|
+
:boolean,
|
10
|
+
:string,
|
11
|
+
:symbol,
|
12
|
+
:integer,
|
13
|
+
:float,
|
14
|
+
:date,
|
15
|
+
:datetime
|
16
|
+
]
|
17
|
+
|
18
|
+
attr_reader :reader
|
19
|
+
private :reader
|
20
|
+
|
21
|
+
attr_reader :question
|
22
|
+
private :question
|
23
|
+
|
24
|
+
# Initialize a Response
|
25
|
+
#
|
26
|
+
# @api public
|
27
|
+
def initialize(question, shell = Shell.new)
|
28
|
+
@question = question
|
29
|
+
@shell = shell
|
30
|
+
@converter = Necromancer.new
|
31
|
+
@reader = Reader.new(@shell)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Read input from STDIN either character or line
|
35
|
+
#
|
36
|
+
# @param [Symbol] type
|
37
|
+
#
|
38
|
+
# @return [undefined]
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
def read(type = nil)
|
42
|
+
question.evaluate_response read_input
|
43
|
+
end
|
44
|
+
|
45
|
+
# @api private
|
46
|
+
def read_input
|
47
|
+
if question.mask? && question.echo?
|
48
|
+
reader.getc(question.mask)
|
49
|
+
else
|
50
|
+
reader.mode.echo(question.echo) do
|
51
|
+
reader.mode.raw(question.raw) do
|
52
|
+
if question.raw?
|
53
|
+
reader.readpartial(10)
|
54
|
+
elsif question.character?
|
55
|
+
reader.getc(question.mask)
|
56
|
+
else
|
57
|
+
reader.gets
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def no_input?(input)
|
65
|
+
!input || input == "\n" || input.empty?
|
66
|
+
end
|
67
|
+
|
68
|
+
# @api private
|
69
|
+
def evaluate_response
|
70
|
+
input = read_input
|
71
|
+
input = if no_input?(input)
|
72
|
+
nil
|
73
|
+
elsif block_given?
|
74
|
+
yield(input)
|
75
|
+
else input
|
76
|
+
end
|
77
|
+
question.evaluate_response(input)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Read answer and cast to String type
|
81
|
+
#
|
82
|
+
# @param [String] error
|
83
|
+
# error to display on failed conversion to string type
|
84
|
+
#
|
85
|
+
# @api public
|
86
|
+
def read_string(error = nil)
|
87
|
+
evaluate_response { |input| String(input).strip }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Read answer's first character
|
91
|
+
#
|
92
|
+
# @api public
|
93
|
+
def read_char
|
94
|
+
question.char(true)
|
95
|
+
evaluate_response { |input| String(input).chars.to_a[0] }
|
96
|
+
end
|
97
|
+
|
98
|
+
# Read multiple line answer and cast to String type
|
99
|
+
#
|
100
|
+
# @api public
|
101
|
+
def read_text
|
102
|
+
evaluate_response { |input| String(input) }
|
103
|
+
end
|
104
|
+
|
105
|
+
# Read ansewr and cast to Symbol type
|
106
|
+
#
|
107
|
+
# @api public
|
108
|
+
def read_symbol(error = nil)
|
109
|
+
evaluate_response { |input| input.to_sym }
|
110
|
+
end
|
111
|
+
|
112
|
+
# Read answer from predifined choicse
|
113
|
+
#
|
114
|
+
# @api public
|
115
|
+
def read_choice(type = nil)
|
116
|
+
question.argument(:required) unless question.default?
|
117
|
+
evaluate_response
|
118
|
+
end
|
119
|
+
|
120
|
+
# Read integer value
|
121
|
+
#
|
122
|
+
# @api public
|
123
|
+
def read_int(error = nil)
|
124
|
+
evaluate_response { |input| @converter.convert(input).to(:integer) }
|
125
|
+
end
|
126
|
+
|
127
|
+
# Read float value
|
128
|
+
#
|
129
|
+
# @api public
|
130
|
+
def read_float(error = nil)
|
131
|
+
evaluate_response { |input| @converter.convert(input).to(:float) }
|
132
|
+
end
|
133
|
+
|
134
|
+
# Read regular expression
|
135
|
+
#
|
136
|
+
# @api public
|
137
|
+
def read_regex(error = nil)
|
138
|
+
evaluate_response { |input| Kernel.send(:Regex, input) }
|
139
|
+
end
|
140
|
+
|
141
|
+
# Read range expression
|
142
|
+
#
|
143
|
+
# @api public
|
144
|
+
def read_range
|
145
|
+
evaluate_response { |input| @converter.convert(input).to(:range, strict: true) }
|
146
|
+
end
|
147
|
+
|
148
|
+
# Read date
|
149
|
+
#
|
150
|
+
# @api public
|
151
|
+
def read_date
|
152
|
+
evaluate_response { |input| @converter.convert(input).to(:date) }
|
153
|
+
end
|
154
|
+
|
155
|
+
# Read datetime
|
156
|
+
#
|
157
|
+
# @api public
|
158
|
+
def read_datetime
|
159
|
+
evaluate_response { |input| @converter.convert(input).to(:datetime) }
|
160
|
+
end
|
161
|
+
|
162
|
+
# Read boolean
|
163
|
+
#
|
164
|
+
# @api public
|
165
|
+
def read_bool(error = nil)
|
166
|
+
evaluate_response { |input| @converter.convert(input).to(:boolean, strict: true) }
|
167
|
+
end
|
168
|
+
|
169
|
+
# Read file contents
|
170
|
+
#
|
171
|
+
# @api public
|
172
|
+
def read_file(error = nil)
|
173
|
+
evaluate_response { |input| File.open(File.join(directory, input)) }
|
174
|
+
end
|
175
|
+
|
176
|
+
# Read string answer and validate against email regex
|
177
|
+
#
|
178
|
+
# @return [String]
|
179
|
+
#
|
180
|
+
# @api public
|
181
|
+
def read_email
|
182
|
+
question.validate(/^[a-z0-9._%+-]+@([a-z0-9-]+\.)+[a-z]{2,6}$/i)
|
183
|
+
question.prompt(question.statement) if question.error
|
184
|
+
with_exception { read_string }
|
185
|
+
end
|
186
|
+
|
187
|
+
# Read answer provided on multiple lines
|
188
|
+
#
|
189
|
+
# @api public
|
190
|
+
def read_multiple
|
191
|
+
response = ''
|
192
|
+
loop do
|
193
|
+
value = evaluate_response
|
194
|
+
break if !value || value == ''
|
195
|
+
next if value !~ /\S/
|
196
|
+
response << value
|
197
|
+
end
|
198
|
+
response
|
199
|
+
end
|
200
|
+
|
201
|
+
# Read password
|
202
|
+
#
|
203
|
+
# @api public
|
204
|
+
def read_password
|
205
|
+
question.echo false
|
206
|
+
evaluate_response
|
207
|
+
end
|
208
|
+
|
209
|
+
# Read a single keypress
|
210
|
+
#
|
211
|
+
# @api public
|
212
|
+
def read_keypress
|
213
|
+
question.echo false
|
214
|
+
question.raw true
|
215
|
+
question.evaluate_response(read_input).tap do |key|
|
216
|
+
raise Interrupt if key == "\x03" # Ctrl-C
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
|
222
|
+
# Ignore exception
|
223
|
+
#
|
224
|
+
# @api private
|
225
|
+
def with_exception(&block)
|
226
|
+
yield
|
227
|
+
rescue
|
228
|
+
question.error? ? block.call : raise
|
229
|
+
end
|
230
|
+
|
231
|
+
# @param [Symbol] type
|
232
|
+
# :boolean, :string, :numeric, :array
|
233
|
+
#
|
234
|
+
# @api private
|
235
|
+
def read_type(class_or_name)
|
236
|
+
raise TypeError, "Type #{type} is not valid" if type && !valid_type?(type)
|
237
|
+
case type
|
238
|
+
when :string, ::String
|
239
|
+
read_string
|
240
|
+
when :symbol, ::Symbol
|
241
|
+
read_symbol
|
242
|
+
when :float, ::Float
|
243
|
+
read_float
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def valid_type?(type)
|
248
|
+
self.class::VALID_TYPES.include? type.to_sym
|
249
|
+
end
|
250
|
+
end # Response
|
251
|
+
end # Prompt
|
252
|
+
end # TTY
|