tty-prompt 0.1.0 → 0.2.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 -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
|