tty-prompt 0.2.0 → 0.3.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 +9 -6
- data/CHANGELOG.md +40 -3
- data/Gemfile +0 -1
- data/README.md +246 -65
- data/examples/ask.rb +7 -0
- data/examples/echo.rb +7 -0
- data/examples/in.rb +7 -0
- data/examples/mask.rb +9 -0
- data/examples/multi_select.rb +8 -0
- data/examples/select.rb +8 -0
- data/examples/validation.rb +9 -0
- data/examples/yes_no.rb +7 -0
- data/lib/tty-prompt.rb +6 -4
- data/lib/tty/prompt.rb +100 -25
- data/lib/tty/prompt/choice.rb +1 -1
- data/lib/tty/prompt/converter_dsl.rb +19 -0
- data/lib/tty/prompt/converter_registry.rb +56 -0
- data/lib/tty/prompt/converters.rb +77 -0
- data/lib/tty/prompt/evaluator.rb +29 -0
- data/lib/tty/prompt/list.rb +38 -36
- data/lib/tty/prompt/mask_question.rb +85 -0
- data/lib/tty/prompt/multi_list.rb +21 -32
- data/lib/tty/prompt/question.rb +184 -162
- data/lib/tty/prompt/question/checks.rb +85 -0
- data/lib/tty/prompt/question/modifier.rb +4 -5
- data/lib/tty/prompt/question/validation.rb +29 -35
- data/lib/tty/prompt/reader.rb +98 -52
- data/lib/tty/prompt/reader/codes.rb +63 -0
- data/lib/tty/prompt/reader/key_event.rb +67 -0
- data/lib/tty/prompt/reader/mode.rb +66 -0
- data/lib/tty/prompt/reader/mode/echo.rb +43 -0
- data/lib/tty/prompt/reader/mode/raw.rb +43 -0
- data/lib/tty/prompt/result.rb +42 -0
- data/lib/tty/prompt/statement.rb +9 -14
- data/lib/tty/prompt/suggestion.rb +4 -2
- data/lib/tty/prompt/symbols.rb +13 -0
- data/lib/tty/prompt/test.rb +3 -2
- data/lib/tty/prompt/utils.rb +1 -1
- data/lib/tty/prompt/version.rb +1 -1
- data/spec/unit/ask_spec.rb +31 -48
- data/spec/unit/choice/eql_spec.rb +0 -2
- data/spec/unit/choice/from_spec.rb +0 -2
- data/spec/unit/choices/add_spec.rb +0 -2
- data/spec/unit/choices/each_spec.rb +0 -2
- data/spec/unit/choices/new_spec.rb +0 -2
- data/spec/unit/choices/pluck_spec.rb +0 -2
- data/spec/unit/converters/convert_bool_spec.rb +58 -0
- data/spec/unit/{response/read_char_spec.rb → converters/convert_char_spec.rb} +2 -4
- data/spec/unit/converters/convert_custom_spec.rb +14 -0
- data/spec/unit/converters/convert_date_spec.rb +25 -0
- data/spec/unit/converters/convert_file_spec.rb +14 -0
- data/spec/unit/{response/read_number_spec.rb → converters/convert_number_spec.rb} +5 -7
- data/spec/unit/converters/convert_path_spec.rb +15 -0
- data/spec/unit/{response/read_range_spec.rb → converters/convert_range_spec.rb} +3 -5
- data/spec/unit/converters/convert_regex_spec.rb +12 -0
- data/spec/unit/converters/convert_string_spec.rb +21 -0
- data/spec/unit/distance/distance_spec.rb +0 -2
- data/spec/unit/error_spec.rb +0 -6
- data/spec/unit/evaluator_spec.rb +67 -0
- data/spec/unit/keypress_spec.rb +19 -0
- data/spec/unit/mask_spec.rb +95 -0
- data/spec/unit/multi_select_spec.rb +36 -24
- data/spec/unit/multiline_spec.rb +19 -0
- data/spec/unit/new_spec.rb +18 -0
- data/spec/unit/ok_spec.rb +10 -0
- data/spec/unit/question/default_spec.rb +17 -4
- data/spec/unit/question/echo_spec.rb +31 -0
- data/spec/unit/question/in_spec.rb +48 -16
- data/spec/unit/question/initialize_spec.rb +2 -9
- data/spec/unit/question/modifier/apply_to_spec.rb +9 -16
- data/spec/unit/question/modifier/letter_case_spec.rb +0 -2
- data/spec/unit/question/modifier/whitespace_spec.rb +12 -20
- data/spec/unit/question/modify_spec.rb +3 -7
- data/spec/unit/question/required_spec.rb +20 -14
- data/spec/unit/question/validate_spec.rb +20 -19
- data/spec/unit/question/validation/call_spec.rb +15 -6
- data/spec/unit/question/validation/coerce_spec.rb +17 -11
- data/spec/unit/reader/publish_keypress_event_spec.rb +81 -0
- data/spec/unit/reader/read_keypress_spec.rb +22 -0
- data/spec/unit/reader/read_line_spec.rb +31 -0
- data/spec/unit/reader/read_multiline_spec.rb +37 -0
- data/spec/unit/result_spec.rb +40 -0
- data/spec/unit/say_spec.rb +18 -23
- data/spec/unit/select_spec.rb +37 -32
- data/spec/unit/statement/initialize_spec.rb +4 -4
- data/spec/unit/suggest_spec.rb +0 -2
- data/spec/unit/warn_spec.rb +0 -5
- data/spec/unit/yes_no_spec.rb +70 -0
- data/tty-prompt.gemspec +7 -4
- metadata +123 -40
- data/lib/tty/prompt/codes.rb +0 -32
- data/lib/tty/prompt/cursor.rb +0 -131
- data/lib/tty/prompt/error.rb +0 -26
- data/lib/tty/prompt/mode.rb +0 -64
- data/lib/tty/prompt/mode/echo.rb +0 -41
- data/lib/tty/prompt/mode/raw.rb +0 -41
- data/lib/tty/prompt/response.rb +0 -247
- data/lib/tty/prompt/response_delegation.rb +0 -42
- data/spec/unit/cursor/new_spec.rb +0 -74
- data/spec/unit/question/character_spec.rb +0 -13
- data/spec/unit/reader/getc_spec.rb +0 -42
- data/spec/unit/response/read_bool_spec.rb +0 -58
- data/spec/unit/response/read_date_spec.rb +0 -16
- data/spec/unit/response/read_email_spec.rb +0 -45
- data/spec/unit/response/read_multiple_spec.rb +0 -21
- data/spec/unit/response/read_spec.rb +0 -69
- data/spec/unit/response/read_string_spec.rb +0 -14
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tty/prompt/result'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
class Prompt
|
7
|
+
# Evaluates provided parameters and stops if any of them fails
|
8
|
+
# @api private
|
9
|
+
class Evaluator
|
10
|
+
attr_reader :results
|
11
|
+
|
12
|
+
def initialize(question, &block)
|
13
|
+
@question = question
|
14
|
+
@results = []
|
15
|
+
instance_eval(&block) if block
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(initial)
|
19
|
+
seed = Result::Success.new(@question, initial)
|
20
|
+
results.reduce(seed, &:with)
|
21
|
+
end
|
22
|
+
|
23
|
+
def check(proc = nil, &block)
|
24
|
+
results << (proc || block)
|
25
|
+
end
|
26
|
+
alias_method :<<, :check
|
27
|
+
end # Evaluator
|
28
|
+
end # Prompt
|
29
|
+
end # TTY
|
data/lib/tty/prompt/list.rb
CHANGED
@@ -21,20 +21,18 @@ module TTY
|
|
21
21
|
# the marker for the selected item
|
22
22
|
#
|
23
23
|
# @api public
|
24
|
-
def initialize(prompt, options)
|
25
|
-
@prompt
|
26
|
-
@reader = Reader.new(@prompt)
|
27
|
-
@pastel = Pastel.new
|
28
|
-
@cursor = Cursor.new
|
29
|
-
|
24
|
+
def initialize(prompt, options = {})
|
25
|
+
@prompt = prompt
|
30
26
|
@first_render = true
|
31
27
|
@done = false
|
32
28
|
@default = Array[options.fetch(:default) { 1 }]
|
33
29
|
@active = @default.first
|
34
30
|
@choices = Choices.new
|
35
31
|
@color = options.fetch(:color) { :green }
|
36
|
-
@marker = options.fetch(:marker) {
|
32
|
+
@marker = options.fetch(:marker) { Symbols::ITEM_SELECTED }
|
37
33
|
@help = options.fetch(:help) { HELP }
|
34
|
+
|
35
|
+
@prompt.subscribe(self)
|
38
36
|
end
|
39
37
|
|
40
38
|
# Set marker
|
@@ -86,6 +84,26 @@ module TTY
|
|
86
84
|
render
|
87
85
|
end
|
88
86
|
|
87
|
+
def keyescape(event)
|
88
|
+
exit 130
|
89
|
+
end
|
90
|
+
|
91
|
+
def keyspace(event)
|
92
|
+
@done = true
|
93
|
+
end
|
94
|
+
|
95
|
+
def keyreturn(event)
|
96
|
+
@done = true
|
97
|
+
end
|
98
|
+
|
99
|
+
def keyup(event)
|
100
|
+
@active = (@active == 1) ? @choices.length : @active - 1
|
101
|
+
end
|
102
|
+
|
103
|
+
def keydown(event)
|
104
|
+
@active = (@active == @choices.length) ? 1 : @active + 1
|
105
|
+
end
|
106
|
+
|
89
107
|
private
|
90
108
|
|
91
109
|
# Setup default option and active selection
|
@@ -117,16 +135,16 @@ module TTY
|
|
117
135
|
#
|
118
136
|
# @api private
|
119
137
|
def render
|
120
|
-
@prompt.
|
138
|
+
@prompt.print(@prompt.hide)
|
121
139
|
until @done
|
122
140
|
render_question
|
123
|
-
|
141
|
+
@prompt.read_keypress
|
124
142
|
refresh
|
125
143
|
end
|
126
144
|
render_question
|
127
145
|
answer = render_answer
|
128
146
|
ensure
|
129
|
-
@prompt.
|
147
|
+
@prompt.print(@prompt.show)
|
130
148
|
answer
|
131
149
|
end
|
132
150
|
|
@@ -139,37 +157,20 @@ module TTY
|
|
139
157
|
@choices[@active - 1].value
|
140
158
|
end
|
141
159
|
|
142
|
-
# Process keyboard input
|
143
|
-
#
|
144
|
-
# @api private
|
145
|
-
def process_input
|
146
|
-
chars = @reader.read_keypress
|
147
|
-
case chars
|
148
|
-
when Codes::SIGINT, Codes::ESCAPE
|
149
|
-
exit 130
|
150
|
-
when Codes::RETURN, Codes::SPACE
|
151
|
-
@done = true
|
152
|
-
when Codes::KEY_UP, Codes::CTRL_K, Codes::CTRL_P
|
153
|
-
@active = (@active == 1) ? @choices.length : @active - 1
|
154
|
-
when Codes::KEY_DOWN, Codes::CTRL_J, Codes::CTRL_N
|
155
|
-
@active = (@active == @choices.length) ? 1 : @active + 1
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
160
|
# Determine area of the screen to clear
|
160
161
|
#
|
161
162
|
# @api private
|
162
163
|
def refresh
|
163
164
|
lines = @question.scan("\n").length + @choices.length + 1
|
164
|
-
@prompt.
|
165
|
+
@prompt.print(@prompt.clear_lines(lines))
|
165
166
|
end
|
166
167
|
|
167
168
|
# Render question with instructions and menu
|
168
169
|
#
|
169
170
|
# @api private
|
170
171
|
def render_question
|
171
|
-
header = @question
|
172
|
-
@prompt.
|
172
|
+
header = "#{@prompt.prefix}#{@question} #{render_header}"
|
173
|
+
@prompt.puts(header)
|
173
174
|
@first_render = false
|
174
175
|
render_menu unless @done
|
175
176
|
end
|
@@ -182,9 +183,9 @@ module TTY
|
|
182
183
|
def render_header
|
183
184
|
if @done
|
184
185
|
selected_item = "#{@choices[@active - 1].name}"
|
185
|
-
@
|
186
|
+
@prompt.decorate(selected_item, @color)
|
186
187
|
elsif @first_render
|
187
|
-
@
|
188
|
+
@prompt.decorate(@help, :bright_black)
|
188
189
|
else
|
189
190
|
''
|
190
191
|
end
|
@@ -196,12 +197,13 @@ module TTY
|
|
196
197
|
def render_menu
|
197
198
|
@choices.each_with_index do |choice, index|
|
198
199
|
message = if index + 1 == @active
|
199
|
-
selected = @marker +
|
200
|
-
@
|
200
|
+
selected = @marker + Symbols::SPACE + choice.name
|
201
|
+
@prompt.decorate("#{selected}", @color)
|
201
202
|
else
|
202
|
-
|
203
|
+
Symbols::SPACE * 2 + choice.name
|
203
204
|
end
|
204
|
-
@
|
205
|
+
newline = (index == @choices.length - 1) ? '' : "\n"
|
206
|
+
@prompt.print(message + newline)
|
205
207
|
end
|
206
208
|
end
|
207
209
|
end # List
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
class MaskQuestion < Question
|
6
|
+
# Create masked question
|
7
|
+
#
|
8
|
+
# @param [Hash] options
|
9
|
+
# @option options [String] :mask
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
def initialize(prompt, options = {})
|
13
|
+
super
|
14
|
+
@mask = options.fetch(:mask) { Symbols::ITEM_SECURE }
|
15
|
+
@done_masked = false
|
16
|
+
@failure = false
|
17
|
+
@prompt.subscribe(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Set character for masking the STDIN input
|
21
|
+
#
|
22
|
+
# @param [String] char
|
23
|
+
#
|
24
|
+
# @return [self]
|
25
|
+
#
|
26
|
+
# @api public
|
27
|
+
def mask(char = (not_set = true))
|
28
|
+
return @mask if not_set
|
29
|
+
@mask = char
|
30
|
+
end
|
31
|
+
|
32
|
+
def keyreturn(event)
|
33
|
+
@done_masked = true
|
34
|
+
end
|
35
|
+
|
36
|
+
def keyenter(event)
|
37
|
+
@done_masked = true
|
38
|
+
end
|
39
|
+
|
40
|
+
def keypress(event)
|
41
|
+
if [:backspace, :delete].include?(event.key.name)
|
42
|
+
@input.chop! unless @input.empty?
|
43
|
+
elsif event.value =~ /^[^\e\n\r]/
|
44
|
+
@input += event.value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Render question and input replaced with masked character
|
49
|
+
#
|
50
|
+
# @api private
|
51
|
+
def render_question
|
52
|
+
header = "#{prompt.prefix}#{message} "
|
53
|
+
if echo?
|
54
|
+
masked = "#{@mask * "#{@input}".length}"
|
55
|
+
if @done_masked && !@failure
|
56
|
+
masked = @prompt.decorate(masked, @color)
|
57
|
+
end
|
58
|
+
header += masked
|
59
|
+
end
|
60
|
+
@prompt.print(header)
|
61
|
+
@prompt.print("\n") if @done
|
62
|
+
end
|
63
|
+
|
64
|
+
def render_error_or_finish(result)
|
65
|
+
@failure = result.failure?
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
69
|
+
# Read input from user masked by character
|
70
|
+
#
|
71
|
+
# @private
|
72
|
+
def read_input
|
73
|
+
@done_masked = false
|
74
|
+
@input = ''
|
75
|
+
until @done_masked
|
76
|
+
@prompt.read_keypress(echo?)
|
77
|
+
@prompt.print(@prompt.clear_line)
|
78
|
+
render_question
|
79
|
+
end
|
80
|
+
@prompt.print("\n")
|
81
|
+
@input
|
82
|
+
end
|
83
|
+
end # MaskQuestion
|
84
|
+
end # Prompt
|
85
|
+
end # TTY
|
@@ -25,6 +25,18 @@ module TTY
|
|
25
25
|
@default = options.fetch(:default) { [] }
|
26
26
|
end
|
27
27
|
|
28
|
+
# Callback fired when space key is pressed
|
29
|
+
#
|
30
|
+
# @api private
|
31
|
+
def keyspace(event)
|
32
|
+
active_choice = @choices[@active - 1]
|
33
|
+
if @selected.include?(active_choice)
|
34
|
+
@selected.delete(active_choice)
|
35
|
+
else
|
36
|
+
@selected << active_choice
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
28
40
|
private
|
29
41
|
|
30
42
|
# Setup default options and active selection
|
@@ -36,40 +48,16 @@ module TTY
|
|
36
48
|
@active = @default.last unless @selected.empty?
|
37
49
|
end
|
38
50
|
|
39
|
-
# Process keyboard input and maintain selected choices
|
40
|
-
#
|
41
|
-
# @api private
|
42
|
-
def process_input
|
43
|
-
chars = @reader.read_keypress
|
44
|
-
case chars
|
45
|
-
when Codes::SIGINT, Codes::ESCAPE
|
46
|
-
exit 130
|
47
|
-
when Codes::RETURN
|
48
|
-
@done = true
|
49
|
-
when Codes::KEY_UP, Codes::CTRL_K, Codes::CTRL_P
|
50
|
-
@active = (@active == 1) ? @choices.length : @active - 1
|
51
|
-
when Codes::KEY_DOWN, Codes::CTRL_J, Codes::CTRL_N
|
52
|
-
@active = (@active == @choices.length) ? 1 : @active + 1
|
53
|
-
when Codes::SPACE
|
54
|
-
active_choice = @choices[@active - 1]
|
55
|
-
if @selected.include?(active_choice)
|
56
|
-
@selected.delete(active_choice)
|
57
|
-
else
|
58
|
-
@selected << active_choice
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
51
|
# Render initial help text and then currently selected choices
|
64
52
|
#
|
65
53
|
# @api private
|
66
54
|
def render_header
|
67
55
|
if @done
|
68
|
-
@
|
56
|
+
@prompt.decorate(@selected.map(&:name).join(', '), :green)
|
69
57
|
elsif @selected.size.nonzero?
|
70
58
|
@selected.map(&:name).join(', ')
|
71
59
|
elsif @first_render
|
72
|
-
@
|
60
|
+
@prompt.decorate(@help, :bright_black)
|
73
61
|
else
|
74
62
|
''
|
75
63
|
end
|
@@ -89,15 +77,16 @@ module TTY
|
|
89
77
|
# @api private
|
90
78
|
def render_menu
|
91
79
|
@choices.each_with_index do |choice, index|
|
92
|
-
indicator = (index + 1 == @active) ? @marker :
|
93
|
-
indicator +=
|
80
|
+
indicator = (index + 1 == @active) ? @marker : Symbols::SPACE
|
81
|
+
indicator += Symbols::SPACE
|
94
82
|
message = if @selected.include?(choice)
|
95
|
-
selected = @
|
96
|
-
selected +
|
83
|
+
selected = @prompt.decorate(Symbols::RADIO_CHECKED, :green)
|
84
|
+
selected + Symbols::SPACE + choice.name
|
97
85
|
else
|
98
|
-
|
86
|
+
Symbols::RADIO_UNCHECKED + Symbols::SPACE + choice.name
|
99
87
|
end
|
100
|
-
@
|
88
|
+
newline = (index == @choices.length - 1) ? '' : "\n"
|
89
|
+
@prompt.print(indicator + message + newline)
|
101
90
|
end
|
102
91
|
end
|
103
92
|
end # MultiList
|
data/lib/tty/prompt/question.rb
CHANGED
@@ -2,61 +2,61 @@
|
|
2
2
|
|
3
3
|
require 'tty/prompt/question/modifier'
|
4
4
|
require 'tty/prompt/question/validation'
|
5
|
-
require 'tty/prompt/
|
5
|
+
require 'tty/prompt/question/checks'
|
6
|
+
require 'tty/prompt/converter_dsl'
|
7
|
+
require 'tty/prompt/converters'
|
6
8
|
|
7
9
|
module TTY
|
8
10
|
# A class responsible for shell prompt interactions.
|
9
11
|
class Prompt
|
10
|
-
# A class
|
12
|
+
# A class responsible for gathering user input
|
13
|
+
#
|
14
|
+
# @api public
|
11
15
|
class Question
|
12
|
-
include
|
16
|
+
include Checks
|
17
|
+
include Converters
|
18
|
+
|
19
|
+
BLANK_REGEX = /\A[[:space:]]*\z/o.freeze
|
20
|
+
|
21
|
+
UndefinedSetting = Module.new
|
13
22
|
|
14
23
|
# Store question message
|
15
24
|
# @api public
|
16
25
|
attr_reader :message
|
17
26
|
|
18
|
-
# Store default value.
|
19
|
-
#
|
20
|
-
# @api private
|
21
|
-
attr_reader :default_value
|
22
|
-
|
23
|
-
attr_reader :validation
|
24
|
-
|
25
|
-
# Controls character processing of the answer
|
26
|
-
#
|
27
|
-
# @api public
|
28
27
|
attr_reader :modifier
|
29
28
|
|
30
|
-
attr_reader :error
|
31
|
-
|
32
|
-
# Returns character mode
|
33
|
-
#
|
34
|
-
# @api public
|
35
|
-
attr_reader :character
|
36
|
-
|
37
|
-
# @api private
|
38
29
|
attr_reader :prompt
|
39
30
|
|
31
|
+
attr_reader :validation
|
32
|
+
|
40
33
|
# Initialize a Question
|
41
34
|
#
|
42
35
|
# @api public
|
43
36
|
def initialize(prompt, options = {})
|
44
|
-
@prompt
|
45
|
-
@
|
46
|
-
@
|
47
|
-
@
|
48
|
-
@
|
49
|
-
@
|
50
|
-
@
|
51
|
-
@
|
52
|
-
@
|
53
|
-
@
|
54
|
-
@
|
55
|
-
@
|
56
|
-
|
37
|
+
@prompt = prompt
|
38
|
+
@default = options.fetch(:default) { UndefinedSetting }
|
39
|
+
@required = options.fetch(:required) { false }
|
40
|
+
@echo = options.fetch(:echo) { true }
|
41
|
+
@in = options.fetch(:in) { UndefinedSetting }
|
42
|
+
@modifier = options.fetch(:modifier) { [] }
|
43
|
+
@validation = options.fetch(:validation) { UndefinedSetting }
|
44
|
+
@read = options.fetch(:read) { UndefinedSetting }
|
45
|
+
@convert = options.fetch(:convert) { UndefinedSetting }
|
46
|
+
@color = options.fetch(:color) { :green }
|
47
|
+
@done = false
|
48
|
+
@input = nil
|
49
|
+
|
50
|
+
@evaluator = Evaluator.new(self)
|
51
|
+
|
52
|
+
@evaluator << CheckRequired
|
53
|
+
@evaluator << CheckDefault
|
54
|
+
@evaluator << CheckRange
|
55
|
+
@evaluator << CheckValidation
|
56
|
+
@evaluator << CheckModifier
|
57
57
|
end
|
58
58
|
|
59
|
-
# Call the
|
59
|
+
# Call the question
|
60
60
|
#
|
61
61
|
# @param [String] message
|
62
62
|
#
|
@@ -64,160 +64,222 @@ module TTY
|
|
64
64
|
#
|
65
65
|
# @api public
|
66
66
|
def call(message, &block)
|
67
|
+
return if blank?(message)
|
67
68
|
@message = message
|
68
69
|
block.call(self) if block
|
69
|
-
prompt.output.print("#{prompt.prefix}#{message}")
|
70
70
|
render
|
71
71
|
end
|
72
72
|
|
73
|
-
#
|
73
|
+
# Read answer and convert to type
|
74
74
|
#
|
75
75
|
# @api private
|
76
76
|
def render
|
77
|
-
|
77
|
+
until @done
|
78
|
+
render_question
|
79
|
+
result = process_input
|
80
|
+
errors = result.errors
|
81
|
+
render_error_or_finish(result)
|
82
|
+
refresh(errors.count)
|
83
|
+
end
|
84
|
+
render_question
|
85
|
+
convert_result(result.value)
|
78
86
|
end
|
79
87
|
|
80
|
-
#
|
88
|
+
# Render question
|
81
89
|
#
|
82
|
-
# @api
|
83
|
-
def
|
84
|
-
|
85
|
-
@
|
90
|
+
# @api private
|
91
|
+
def render_question
|
92
|
+
header = "#{prompt.prefix}#{message} "
|
93
|
+
if @convert == :bool && !@done
|
94
|
+
header += @prompt.decorate('(Y/n)', :bright_black) + ' '
|
95
|
+
elsif !echo?
|
96
|
+
header
|
97
|
+
elsif @done
|
98
|
+
header += @prompt.decorate("#{@input}", @color)
|
99
|
+
elsif default?
|
100
|
+
header += @prompt.decorate("(#{default})", :bright_black) + ' '
|
101
|
+
end
|
102
|
+
@prompt.print(header)
|
103
|
+
@prompt.print("\n") if @done
|
86
104
|
end
|
87
105
|
|
88
|
-
#
|
106
|
+
# Decide how to handle input from user
|
89
107
|
#
|
90
|
-
# @
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
108
|
+
# @api private
|
109
|
+
def process_input
|
110
|
+
@input = read_input
|
111
|
+
if blank?(@input)
|
112
|
+
@input = default? ? default : nil
|
113
|
+
end
|
114
|
+
@evaluator.(@input)
|
95
115
|
end
|
96
116
|
|
97
|
-
#
|
98
|
-
#
|
99
|
-
# @return [Boolean]
|
117
|
+
# Process input
|
100
118
|
#
|
101
|
-
# @api
|
102
|
-
def
|
103
|
-
@
|
119
|
+
# @api private
|
120
|
+
def read_input
|
121
|
+
case @read
|
122
|
+
when :keypress
|
123
|
+
@prompt.read_keypress
|
124
|
+
when :multiline
|
125
|
+
@prompt.read_multiline
|
126
|
+
else
|
127
|
+
@prompt.read_line(echo)
|
128
|
+
end
|
104
129
|
end
|
105
130
|
|
106
|
-
#
|
131
|
+
# Handle error condition
|
107
132
|
#
|
108
|
-
# @
|
133
|
+
# @api private
|
134
|
+
def render_error_or_finish(result)
|
135
|
+
if result.failure?
|
136
|
+
result.errors.each do |err|
|
137
|
+
@prompt.print(@prompt.clear_line)
|
138
|
+
@prompt.print(@prompt.decorate('>>', :red) + ' ' + err)
|
139
|
+
end
|
140
|
+
@prompt.print(@prompt.cursor.up(result.errors.count))
|
141
|
+
else
|
142
|
+
@done = true
|
143
|
+
if result.errors.count.nonzero?
|
144
|
+
@prompt.print(@prompt.cursor.down(result.errors.count))
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Determine area of the screen to clear
|
109
150
|
#
|
110
|
-
# @
|
151
|
+
# @param [Integer] errors
|
111
152
|
#
|
112
|
-
# @api
|
113
|
-
def
|
114
|
-
|
153
|
+
# @api private
|
154
|
+
def refresh(errors = nil)
|
155
|
+
lines = @message.scan("\n").length
|
156
|
+
lines += ((!echo? || errors.nonzero?) ? 1 : 2) # clear user enter
|
157
|
+
|
158
|
+
if errors.nonzero? && @done
|
159
|
+
lines += errors
|
160
|
+
end
|
161
|
+
|
162
|
+
@prompt.print(@prompt.clear_lines(lines))
|
115
163
|
end
|
116
164
|
|
117
|
-
#
|
165
|
+
# Convert value to expected type
|
118
166
|
#
|
119
|
-
# @param [
|
167
|
+
# @param [Object] value
|
120
168
|
#
|
121
|
-
# @api
|
122
|
-
def
|
123
|
-
|
169
|
+
# @api private
|
170
|
+
def convert_result(value)
|
171
|
+
if convert? & !blank?(value)
|
172
|
+
converter_registry.(@convert, value)
|
173
|
+
else
|
174
|
+
value
|
175
|
+
end
|
124
176
|
end
|
125
177
|
|
126
|
-
#
|
178
|
+
# Set reader type
|
127
179
|
#
|
128
180
|
# @api public
|
129
|
-
def
|
130
|
-
@
|
181
|
+
def read(value)
|
182
|
+
@read = value
|
131
183
|
end
|
132
184
|
|
133
|
-
#
|
185
|
+
# Specify answer conversion
|
134
186
|
#
|
135
187
|
# @api public
|
136
|
-
def
|
137
|
-
|
188
|
+
def convert(value)
|
189
|
+
@convert = value
|
138
190
|
end
|
139
191
|
|
140
|
-
#
|
141
|
-
#
|
192
|
+
# Check if conversion is set
|
193
|
+
#
|
194
|
+
# @return [Boolean]
|
142
195
|
#
|
143
196
|
# @api public
|
144
|
-
def
|
145
|
-
|
146
|
-
@echo = value
|
197
|
+
def convert?
|
198
|
+
@convert != UndefinedSetting
|
147
199
|
end
|
148
200
|
|
149
|
-
#
|
201
|
+
# Set default value.
|
150
202
|
#
|
151
203
|
# @api public
|
152
|
-
def
|
153
|
-
|
204
|
+
def default(value = (not_set = true))
|
205
|
+
return @default if not_set
|
206
|
+
@default = value
|
154
207
|
end
|
155
208
|
|
156
|
-
#
|
209
|
+
# Check if default value is set
|
210
|
+
#
|
211
|
+
# @return [Boolean]
|
157
212
|
#
|
158
213
|
# @api public
|
159
|
-
def
|
160
|
-
|
161
|
-
@raw = value
|
214
|
+
def default?
|
215
|
+
@default != UndefinedSetting
|
162
216
|
end
|
163
217
|
|
164
|
-
#
|
218
|
+
# Ensure that passed argument is present or not
|
219
|
+
#
|
220
|
+
# @return [Boolean]
|
165
221
|
#
|
166
222
|
# @api public
|
167
|
-
def
|
168
|
-
|
223
|
+
def required(value = (not_set = true))
|
224
|
+
return @required if not_set
|
225
|
+
@required = value
|
169
226
|
end
|
227
|
+
alias_method :required?, :required
|
170
228
|
|
171
|
-
# Set
|
229
|
+
# Set validation rule for an argument
|
172
230
|
#
|
173
|
-
# @param [
|
231
|
+
# @param [Object] value
|
174
232
|
#
|
175
|
-
# @return [
|
233
|
+
# @return [Question]
|
176
234
|
#
|
177
235
|
# @api public
|
178
|
-
def
|
179
|
-
|
180
|
-
@mask = char
|
236
|
+
def validate(value = nil, &block)
|
237
|
+
@validation = (value || block)
|
181
238
|
end
|
182
239
|
|
183
|
-
|
240
|
+
def validation?
|
241
|
+
@validation != UndefinedSetting
|
242
|
+
end
|
243
|
+
|
244
|
+
# Modify string according to the rule given.
|
184
245
|
#
|
185
|
-
# @
|
246
|
+
# @param [Symbol] rule
|
186
247
|
#
|
187
248
|
# @api public
|
188
|
-
def
|
189
|
-
|
249
|
+
def modify(*rules)
|
250
|
+
@modifier = rules
|
190
251
|
end
|
191
252
|
|
192
|
-
#
|
193
|
-
#
|
194
|
-
# @param [Boolean] value
|
195
|
-
#
|
196
|
-
# @return [self]
|
253
|
+
# Turn terminal echo on or off. This is used to secure the display so
|
254
|
+
# that the entered characters are not echoed back to the screen.
|
197
255
|
#
|
198
256
|
# @api public
|
199
|
-
def
|
200
|
-
return @
|
201
|
-
@
|
257
|
+
def echo(value = nil)
|
258
|
+
return @echo if value.nil?
|
259
|
+
@echo = value
|
202
260
|
end
|
261
|
+
alias_method :echo?, :echo
|
203
262
|
|
204
|
-
#
|
205
|
-
#
|
206
|
-
# @return [Boolean]
|
263
|
+
# Turn raw mode on or off. This enables character-based input.
|
207
264
|
#
|
208
265
|
# @api public
|
209
|
-
def
|
210
|
-
|
266
|
+
def raw(value = nil)
|
267
|
+
return @raw if value.nil?
|
268
|
+
@raw = value
|
211
269
|
end
|
270
|
+
alias_method :raw?, :raw
|
212
271
|
|
213
|
-
# Set
|
272
|
+
# Set expected range of values
|
214
273
|
#
|
215
274
|
# @param [String] value
|
216
275
|
#
|
217
276
|
# @api public
|
218
|
-
def in(value =
|
219
|
-
|
220
|
-
|
277
|
+
def in(value = (not_set = true))
|
278
|
+
if in? && !@in.is_a?(Range)
|
279
|
+
@in = converter_registry.(:range, @in)
|
280
|
+
end
|
281
|
+
return @in if not_set
|
282
|
+
@in = converter_registry.(:range, value)
|
221
283
|
end
|
222
284
|
|
223
285
|
# Check if range is set
|
@@ -226,63 +288,23 @@ module TTY
|
|
226
288
|
#
|
227
289
|
# @api public
|
228
290
|
def in?
|
229
|
-
|
291
|
+
@in != UndefinedSetting
|
230
292
|
end
|
231
293
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
# @return [Object]
|
237
|
-
#
|
238
|
-
# @api private
|
239
|
-
def evaluate_response(input)
|
240
|
-
return @default if !input && default?
|
241
|
-
check_required(input)
|
242
|
-
return if input.nil?
|
243
|
-
|
244
|
-
within?(input)
|
245
|
-
validation.(input)
|
246
|
-
modifier.apply_to(input)
|
247
|
-
end
|
248
|
-
|
249
|
-
# Reset question object.
|
250
|
-
#
|
251
|
-
# @api public
|
252
|
-
def clean
|
253
|
-
@message = nil
|
254
|
-
@default = nil
|
255
|
-
@required = false
|
256
|
-
@modifier = nil
|
294
|
+
def blank?(value)
|
295
|
+
value.nil? ||
|
296
|
+
value.respond_to?(:empty?) && value.empty? ||
|
297
|
+
BLANK_REGEX === value
|
257
298
|
end
|
258
299
|
|
259
300
|
def to_s
|
260
301
|
"#{message}"
|
261
302
|
end
|
262
303
|
|
304
|
+
# String representation of this question
|
305
|
+
# @api public
|
263
306
|
def inspect
|
264
|
-
"
|
265
|
-
end
|
266
|
-
|
267
|
-
private
|
268
|
-
|
269
|
-
# Check if value is present
|
270
|
-
#
|
271
|
-
# @api private
|
272
|
-
def check_required(value)
|
273
|
-
if @required && !default? && value.nil?
|
274
|
-
fail ArgumentRequired, 'No value provided for required'
|
275
|
-
end
|
276
|
-
end
|
277
|
-
|
278
|
-
# Check if value is within expected range
|
279
|
-
#
|
280
|
-
# @api private
|
281
|
-
def within?(value)
|
282
|
-
if in? && value
|
283
|
-
@in.include?(value) || fail(InvalidArgument,
|
284
|
-
"Value #{value} is not included in the range #{@in}")
|
285
|
-
end
|
307
|
+
"#<#{self.class.name} @message=#{message}, @input=#{@input}>"
|
286
308
|
end
|
287
309
|
end # Question
|
288
310
|
end # Prompt
|