tty-prompt 0.1.0 → 0.2.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 -2
- data/CHANGELOG.md +12 -0
- data/README.md +223 -59
- data/lib/tty/prompt/choice.rb +83 -0
- data/lib/tty/prompt/choices.rb +92 -0
- data/lib/tty/prompt/codes.rb +32 -0
- data/lib/tty/prompt/cursor.rb +131 -0
- data/lib/tty/prompt/list.rb +209 -0
- data/lib/tty/prompt/mode/echo.rb +10 -9
- data/lib/tty/prompt/mode/raw.rb +10 -9
- data/lib/tty/prompt/multi_list.rb +105 -0
- data/lib/tty/prompt/question/validation.rb +12 -27
- data/lib/tty/prompt/question.rb +58 -107
- data/lib/tty/prompt/reader.rb +44 -11
- data/lib/tty/prompt/response.rb +31 -36
- data/lib/tty/prompt/response_delegation.rb +3 -2
- data/lib/tty/prompt/statement.rb +10 -10
- data/lib/tty/prompt/test.rb +15 -0
- data/lib/tty/prompt/version.rb +3 -3
- data/lib/tty/prompt.rb +72 -9
- data/lib/tty-prompt.rb +11 -0
- data/spec/unit/ask_spec.rb +32 -35
- data/spec/unit/choice/eql_spec.rb +24 -0
- data/spec/unit/choice/from_spec.rb +25 -0
- data/spec/unit/choices/add_spec.rb +14 -0
- data/spec/unit/choices/each_spec.rb +15 -0
- data/spec/unit/choices/new_spec.rb +12 -0
- data/spec/unit/choices/pluck_spec.rb +11 -0
- data/spec/unit/cursor/new_spec.rb +74 -0
- data/spec/unit/error_spec.rb +4 -8
- data/spec/unit/multi_select_spec.rb +163 -0
- data/spec/unit/question/character_spec.rb +5 -16
- data/spec/unit/question/default_spec.rb +4 -10
- data/spec/unit/question/in_spec.rb +15 -12
- data/spec/unit/question/initialize_spec.rb +1 -6
- data/spec/unit/question/modify_spec.rb +25 -24
- data/spec/unit/question/required_spec.rb +31 -0
- data/spec/unit/question/validate_spec.rb +25 -17
- data/spec/unit/question/validation/call_spec.rb +22 -0
- data/spec/unit/response/read_bool_spec.rb +38 -27
- data/spec/unit/response/read_char_spec.rb +5 -8
- data/spec/unit/response/read_date_spec.rb +8 -12
- data/spec/unit/response/read_email_spec.rb +25 -22
- data/spec/unit/response/read_multiple_spec.rb +11 -13
- data/spec/unit/response/read_number_spec.rb +12 -16
- data/spec/unit/response/read_range_spec.rb +10 -13
- data/spec/unit/response/read_spec.rb +39 -38
- data/spec/unit/response/read_string_spec.rb +7 -12
- data/spec/unit/say_spec.rb +10 -14
- data/spec/unit/select_spec.rb +192 -0
- data/spec/unit/statement/initialize_spec.rb +0 -4
- data/spec/unit/suggest_spec.rb +6 -9
- data/spec/unit/warn_spec.rb +4 -8
- metadata +32 -8
- data/spec/unit/question/argument_spec.rb +0 -30
- data/spec/unit/question/valid_spec.rb +0 -46
- data/spec/unit/question/validation/valid_value_spec.rb +0 -22
@@ -0,0 +1,131 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
class Cursor
|
6
|
+
ECMA_CSI = "\e[".freeze
|
7
|
+
DEC_RST = 'l'.freeze
|
8
|
+
DEC_SET = 'h'.freeze
|
9
|
+
DEC_TCEM = '?25'.freeze
|
10
|
+
ECMA_CLR = 'K'.freeze
|
11
|
+
|
12
|
+
attr_reader :shell
|
13
|
+
|
14
|
+
def initialize(stream = nil, options = {})
|
15
|
+
@stream = stream || $stdout
|
16
|
+
@shell = options.fetch(:shell, false)
|
17
|
+
@hidden = options.fetch(:hidden, false)
|
18
|
+
end
|
19
|
+
|
20
|
+
def print
|
21
|
+
self.class.new(@stream, shell: true)
|
22
|
+
end
|
23
|
+
|
24
|
+
def show
|
25
|
+
@hidden = false
|
26
|
+
ECMA_CSI + DEC_TCEM + DEC_SET
|
27
|
+
end
|
28
|
+
|
29
|
+
def hide
|
30
|
+
show if @hidden
|
31
|
+
@hidden = true
|
32
|
+
ECMA_CSI + DEC_TCEM + DEC_RST
|
33
|
+
end
|
34
|
+
|
35
|
+
# Switch off cursor for the block
|
36
|
+
# @api public
|
37
|
+
def invisible
|
38
|
+
hide
|
39
|
+
yield
|
40
|
+
ensure
|
41
|
+
show
|
42
|
+
end
|
43
|
+
|
44
|
+
# Save current position
|
45
|
+
# @api public
|
46
|
+
def save
|
47
|
+
ECMA_CSI + "s"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Restore cursor position
|
51
|
+
# @api public
|
52
|
+
def restore
|
53
|
+
ECMA_CSI + "u"
|
54
|
+
end
|
55
|
+
|
56
|
+
def current
|
57
|
+
ECMA_CSI + "6n"
|
58
|
+
end
|
59
|
+
|
60
|
+
# Move cursor relative to its current position
|
61
|
+
# @api public
|
62
|
+
def move(x, y)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Move cursor up by number of lines
|
66
|
+
#
|
67
|
+
# @param [Integer] count
|
68
|
+
#
|
69
|
+
# @api public
|
70
|
+
def move_up(count = nil)
|
71
|
+
ECMA_CSI + "#{(count || 1)}A"
|
72
|
+
end
|
73
|
+
|
74
|
+
def move_down(count = nil)
|
75
|
+
ECMA_CSI + "#{(count || 1)}B"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Move to start of the line
|
79
|
+
#
|
80
|
+
# @api public
|
81
|
+
def move_start
|
82
|
+
ECMA_CSI + '1000D'
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param [Integer] count
|
86
|
+
# how far to go left
|
87
|
+
# @api public
|
88
|
+
def move_left(count = nil)
|
89
|
+
ECMA_CSI + "#{count || 1}D"
|
90
|
+
end
|
91
|
+
|
92
|
+
# @api public
|
93
|
+
def move_right(count = nil)
|
94
|
+
ECMA_CSI + "#{count || 1}C"
|
95
|
+
end
|
96
|
+
|
97
|
+
def next_line
|
98
|
+
ECMA_CSI + 'E'
|
99
|
+
end
|
100
|
+
|
101
|
+
def prev_line
|
102
|
+
ECMA_CSI + 'F'
|
103
|
+
end
|
104
|
+
|
105
|
+
# @api public
|
106
|
+
def clear_line
|
107
|
+
move_start + ECMA_CSI + ECMA_CLR
|
108
|
+
end
|
109
|
+
|
110
|
+
# @api public
|
111
|
+
def clear_lines(amount, direction = :up)
|
112
|
+
amount.times.reduce("") do |acc|
|
113
|
+
dir = direction == :up ? move_up : move_down
|
114
|
+
acc << dir + clear_line
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Clear screen down from current position
|
119
|
+
# @api public
|
120
|
+
def clear_down
|
121
|
+
ECMA_CSI + "J"
|
122
|
+
end
|
123
|
+
|
124
|
+
# Clear screen up from current position
|
125
|
+
# @api public
|
126
|
+
def clear_up
|
127
|
+
ECMA_CSI + "1J"
|
128
|
+
end
|
129
|
+
end # Cursor
|
130
|
+
end # Prompt
|
131
|
+
end # TTY
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
# A class responsible for rendering select list menu
|
6
|
+
# Used by {Prompt} to display interactive menu.
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class List
|
10
|
+
HELP = '(Use arrow keys, press Enter to select)'.freeze
|
11
|
+
|
12
|
+
# Create instance of TTY::Prompt::List menu.
|
13
|
+
#
|
14
|
+
# @param Hash options
|
15
|
+
# the configuration options
|
16
|
+
# @option options [Symbol] :default
|
17
|
+
# the default active choice, defaults to 1
|
18
|
+
# @option options [Symbol] :color
|
19
|
+
# the color for the selected item, defualts to :green
|
20
|
+
# @option options [Symbol] :marker
|
21
|
+
# the marker for the selected item
|
22
|
+
#
|
23
|
+
# @api public
|
24
|
+
def initialize(prompt, options)
|
25
|
+
@prompt = prompt
|
26
|
+
@reader = Reader.new(@prompt)
|
27
|
+
@pastel = Pastel.new
|
28
|
+
@cursor = Cursor.new
|
29
|
+
|
30
|
+
@first_render = true
|
31
|
+
@done = false
|
32
|
+
@default = Array[options.fetch(:default) { 1 }]
|
33
|
+
@active = @default.first
|
34
|
+
@choices = Choices.new
|
35
|
+
@color = options.fetch(:color) { :green }
|
36
|
+
@marker = options.fetch(:marker) { Codes::ITEM_SELECTED }
|
37
|
+
@help = options.fetch(:help) { HELP }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Set marker
|
41
|
+
#
|
42
|
+
# @api public
|
43
|
+
def marker(value)
|
44
|
+
@marker = value
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set default option selected
|
48
|
+
#
|
49
|
+
# @api public
|
50
|
+
def default(*default_values)
|
51
|
+
@default = default_values
|
52
|
+
end
|
53
|
+
|
54
|
+
# Add a single choice
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
def choice(*value, &block)
|
58
|
+
if block
|
59
|
+
@choices << (value << block)
|
60
|
+
else
|
61
|
+
@choices << value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Add multiple choices
|
66
|
+
#
|
67
|
+
# @param [Array[Object]] values
|
68
|
+
# the values to add as choices
|
69
|
+
#
|
70
|
+
# @api public
|
71
|
+
def choices(values)
|
72
|
+
values.each { |val| choice(*val) }
|
73
|
+
end
|
74
|
+
|
75
|
+
# Call the list menu by passing question and choices
|
76
|
+
#
|
77
|
+
# @param [String] question
|
78
|
+
#
|
79
|
+
# @param
|
80
|
+
# @api public
|
81
|
+
def call(question, possibilities, &block)
|
82
|
+
choices(possibilities)
|
83
|
+
@question = question
|
84
|
+
block.call(self) if block
|
85
|
+
setup_defaults
|
86
|
+
render
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# Setup default option and active selection
|
92
|
+
#
|
93
|
+
# @api private
|
94
|
+
def setup_defaults
|
95
|
+
validate_defaults
|
96
|
+
@active = @default.first
|
97
|
+
end
|
98
|
+
|
99
|
+
# Validate default indexes to be within range
|
100
|
+
#
|
101
|
+
# @api private
|
102
|
+
def validate_defaults
|
103
|
+
@default.each do |d|
|
104
|
+
if d < 1 || d > @choices.size
|
105
|
+
fail PromptConfigurationError,
|
106
|
+
"default index `#{d}` out of range (1 - #{@choices.size})"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Render a selection list.
|
112
|
+
#
|
113
|
+
# By default the result is printed out.
|
114
|
+
#
|
115
|
+
# @return [Object] value
|
116
|
+
# return the selected value
|
117
|
+
#
|
118
|
+
# @api private
|
119
|
+
def render
|
120
|
+
@prompt.output.print(@cursor.hide)
|
121
|
+
until @done
|
122
|
+
render_question
|
123
|
+
process_input
|
124
|
+
refresh
|
125
|
+
end
|
126
|
+
render_question
|
127
|
+
answer = render_answer
|
128
|
+
ensure
|
129
|
+
@prompt.output.print(@cursor.show)
|
130
|
+
answer
|
131
|
+
end
|
132
|
+
|
133
|
+
# Find value for the choice selected
|
134
|
+
#
|
135
|
+
# @return [nil, Object]
|
136
|
+
#
|
137
|
+
# @api private
|
138
|
+
def render_answer
|
139
|
+
@choices[@active - 1].value
|
140
|
+
end
|
141
|
+
|
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
|
+
# Determine area of the screen to clear
|
160
|
+
#
|
161
|
+
# @api private
|
162
|
+
def refresh
|
163
|
+
lines = @question.scan("\n").length + @choices.length + 1
|
164
|
+
@prompt.output.print(@cursor.clear_lines(lines))
|
165
|
+
end
|
166
|
+
|
167
|
+
# Render question with instructions and menu
|
168
|
+
#
|
169
|
+
# @api private
|
170
|
+
def render_question
|
171
|
+
header = @question + Codes::SPACE + render_header
|
172
|
+
@prompt.output.puts(header)
|
173
|
+
@first_render = false
|
174
|
+
render_menu unless @done
|
175
|
+
end
|
176
|
+
|
177
|
+
# Render initial help and selected choice
|
178
|
+
#
|
179
|
+
# @return [String]
|
180
|
+
#
|
181
|
+
# @api private
|
182
|
+
def render_header
|
183
|
+
if @done
|
184
|
+
selected_item = "#{@choices[@active - 1].name}"
|
185
|
+
@pastel.decorate(selected_item, @color)
|
186
|
+
elsif @first_render
|
187
|
+
@pastel.decorate(@help, :bright_black)
|
188
|
+
else
|
189
|
+
''
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Render menu with choices to select from
|
194
|
+
#
|
195
|
+
# @api private
|
196
|
+
def render_menu
|
197
|
+
@choices.each_with_index do |choice, index|
|
198
|
+
message = if index + 1 == @active
|
199
|
+
selected = @marker + Codes::SPACE + choice.name
|
200
|
+
@pastel.decorate("#{selected}", @color)
|
201
|
+
else
|
202
|
+
Codes::SPACE * 2 + choice.name
|
203
|
+
end
|
204
|
+
@prompt.output.puts(message)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end # List
|
208
|
+
end # Prompt
|
209
|
+
end # TTY
|
data/lib/tty/prompt/mode/echo.rb
CHANGED
@@ -24,15 +24,16 @@ module TTY
|
|
24
24
|
# @api public
|
25
25
|
def echo(is_on=true, &block)
|
26
26
|
value = nil
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
27
|
+
off unless is_on
|
28
|
+
value = block.call if block_given?
|
29
|
+
rescue NoMethodError, Interrupt => error
|
30
|
+
puts "#{error.class} #{error.message}"
|
31
|
+
puts error.backtrace
|
32
|
+
on
|
33
|
+
exit
|
34
|
+
ensure
|
35
|
+
on
|
36
|
+
return value
|
36
37
|
end
|
37
38
|
end # Echo
|
38
39
|
end # Mode
|
data/lib/tty/prompt/mode/raw.rb
CHANGED
@@ -24,15 +24,16 @@ module TTY
|
|
24
24
|
# @api public
|
25
25
|
def raw(is_on=true, &block)
|
26
26
|
value = nil
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
27
|
+
on if is_on
|
28
|
+
value = block.call if block_given?
|
29
|
+
rescue NoMethodError, Interrupt => error
|
30
|
+
puts "#{error.class} #{error.message}"
|
31
|
+
puts error.backtrace
|
32
|
+
off
|
33
|
+
exit
|
34
|
+
ensure
|
35
|
+
off
|
36
|
+
return value
|
36
37
|
end
|
37
38
|
end # Raw
|
38
39
|
end # Mode
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tty/prompt/list'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
class Prompt
|
7
|
+
# A class responsible for rendering multi select list menu.
|
8
|
+
# Used by {Prompt} to display interactive choice menu.
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class MultiList < List
|
12
|
+
|
13
|
+
HELP = '(Use arrow keys, press Space to select and Enter to finish)'.freeze
|
14
|
+
|
15
|
+
# Create instance of TTY::Prompt::MultiList menu.
|
16
|
+
#
|
17
|
+
# @param [Prompt] :prompt
|
18
|
+
# @param [Hash] options
|
19
|
+
#
|
20
|
+
# @api public
|
21
|
+
def initialize(prompt, options)
|
22
|
+
super
|
23
|
+
@selected = []
|
24
|
+
@help = options.fetch(:help) { HELP }
|
25
|
+
@default = options.fetch(:default) { [] }
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Setup default options and active selection
|
31
|
+
#
|
32
|
+
# @api private
|
33
|
+
def setup_defaults
|
34
|
+
validate_defaults
|
35
|
+
@selected = @choices.values_at(*@default.map { |d| d - 1 })
|
36
|
+
@active = @default.last unless @selected.empty?
|
37
|
+
end
|
38
|
+
|
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
|
+
# Render initial help text and then currently selected choices
|
64
|
+
#
|
65
|
+
# @api private
|
66
|
+
def render_header
|
67
|
+
if @done
|
68
|
+
@pastel.decorate(@selected.map(&:name).join(', '), :green)
|
69
|
+
elsif @selected.size.nonzero?
|
70
|
+
@selected.map(&:name).join(', ')
|
71
|
+
elsif @first_render
|
72
|
+
@pastel.decorate(@help, :bright_black)
|
73
|
+
else
|
74
|
+
''
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# All values for the choices selected
|
79
|
+
#
|
80
|
+
# @return [Array[nil,Object]]
|
81
|
+
#
|
82
|
+
# @api private
|
83
|
+
def render_answer
|
84
|
+
@selected.map(&:value)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Render menu with choices to select from
|
88
|
+
#
|
89
|
+
# @api private
|
90
|
+
def render_menu
|
91
|
+
@choices.each_with_index do |choice, index|
|
92
|
+
indicator = (index + 1 == @active) ? @marker : Codes::SPACE
|
93
|
+
indicator += Codes::SPACE
|
94
|
+
message = if @selected.include?(choice)
|
95
|
+
selected = @pastel.decorate(Codes::RADIO_CHECKED, :green)
|
96
|
+
selected + Codes::SPACE + choice.name
|
97
|
+
else
|
98
|
+
Codes::RADIO_UNCHECKED + Codes::SPACE + choice.name
|
99
|
+
end
|
100
|
+
@prompt.output.puts(indicator + message)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end # MultiList
|
104
|
+
end # Prompt
|
105
|
+
end # TTY
|
@@ -47,39 +47,24 @@ module TTY
|
|
47
47
|
!!validation
|
48
48
|
end
|
49
49
|
|
50
|
-
# Test if the
|
50
|
+
# Test if the input passes the validation
|
51
51
|
#
|
52
52
|
# @example
|
53
|
-
#
|
53
|
+
# Validation.new
|
54
|
+
# validation.valid?(input) # => true
|
54
55
|
#
|
55
|
-
# @param [Object]
|
56
|
-
# the
|
56
|
+
# @param [Object] input
|
57
|
+
# the input to validate
|
57
58
|
#
|
58
|
-
# @return [
|
59
|
+
# @return [Boolean]
|
59
60
|
#
|
60
61
|
# @api public
|
61
|
-
def
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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}"
|
62
|
+
def call(input)
|
63
|
+
if validate? && input
|
64
|
+
input = input.to_s
|
65
|
+
if validation.is_a?(Regexp) && validation =~ input
|
66
|
+
elsif validation.is_a?(Proc) && validation.call(input)
|
67
|
+
else fail InvalidArgument, "Invalid input for #{input}"
|
83
68
|
end
|
84
69
|
true
|
85
70
|
else
|