tty-prompt 0.18.1 → 0.19.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/CHANGELOG.md +23 -0
- data/README.md +174 -63
- data/Rakefile +2 -2
- data/examples/ask_blank.rb +9 -0
- data/examples/enum_select_disabled.rb +1 -1
- data/examples/enum_select_paged.rb +1 -1
- data/examples/expand_auto.rb +29 -0
- data/examples/mask.rb +1 -1
- data/examples/multi_select.rb +1 -1
- data/examples/multi_select_disabled_paged.rb +22 -0
- data/examples/multi_select_paged.rb +1 -1
- data/examples/select_disabled_paged.rb +22 -0
- data/examples/select_paginated.rb +1 -1
- data/lib/tty/prompt.rb +46 -10
- data/lib/tty/prompt/{enum_paginator.rb → block_paginator.rb} +19 -18
- data/lib/tty/prompt/choice.rb +1 -3
- data/lib/tty/prompt/enum_list.rb +31 -9
- data/lib/tty/prompt/expander.rb +19 -1
- data/lib/tty/prompt/keypress.rb +30 -35
- data/lib/tty/prompt/list.rb +112 -40
- data/lib/tty/prompt/mask_question.rb +2 -3
- data/lib/tty/prompt/multi_list.rb +36 -12
- data/lib/tty/prompt/paginator.rb +37 -25
- data/lib/tty/prompt/question.rb +29 -5
- data/lib/tty/prompt/slider.rb +16 -8
- data/lib/tty/prompt/symbols.rb +30 -6
- data/lib/tty/prompt/timer.rb +75 -0
- data/lib/tty/prompt/version.rb +1 -1
- data/spec/spec_helper.rb +18 -2
- data/spec/unit/ask_spec.rb +45 -4
- data/spec/unit/{enum_paginator_spec.rb → block_paginator_spec.rb} +14 -5
- data/spec/unit/choice/from_spec.rb +16 -0
- data/spec/unit/enum_select_spec.rb +104 -32
- data/spec/unit/expand_spec.rb +104 -12
- data/spec/unit/keypress_spec.rb +2 -8
- data/spec/unit/mask_spec.rb +9 -1
- data/spec/unit/multi_select_spec.rb +348 -118
- data/spec/unit/paginator_spec.rb +29 -10
- data/spec/unit/select_spec.rb +390 -108
- data/spec/unit/slider_spec.rb +48 -6
- data/spec/unit/timer_spec.rb +29 -0
- data/tty-prompt.gemspec +4 -6
- metadata +17 -46
- data/lib/tty/prompt/timeout.rb +0 -78
@@ -9,7 +9,7 @@ module TTY
|
|
9
9
|
#
|
10
10
|
# @api private
|
11
11
|
class MultiList < List
|
12
|
-
HELP = '(Use arrow%s keys, press Space to select and Enter to finish%s)'
|
12
|
+
HELP = '(Use %s arrow%s keys, press Space to select and Enter to finish%s)'
|
13
13
|
|
14
14
|
# Create instance of TTY::Prompt::MultiList menu.
|
15
15
|
#
|
@@ -17,11 +17,19 @@ module TTY
|
|
17
17
|
# @param [Hash] options
|
18
18
|
#
|
19
19
|
# @api public
|
20
|
-
def initialize(prompt, options)
|
20
|
+
def initialize(prompt, **options)
|
21
21
|
super
|
22
22
|
@selected = []
|
23
23
|
@help = options[:help]
|
24
24
|
@echo = options.fetch(:echo, true)
|
25
|
+
@max = options[:max]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Set a maximum number of choices
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def max(value)
|
32
|
+
@max = value
|
25
33
|
end
|
26
34
|
|
27
35
|
# Callback fired when space key is pressed
|
@@ -32,6 +40,7 @@ module TTY
|
|
32
40
|
if @selected.include?(active_choice)
|
33
41
|
@selected.delete(active_choice)
|
34
42
|
else
|
43
|
+
return if @max && @selected.size >= @max
|
35
44
|
@selected << active_choice
|
36
45
|
end
|
37
46
|
end
|
@@ -62,20 +71,34 @@ module TTY
|
|
62
71
|
@selected.map(&:name).join(', ')
|
63
72
|
end
|
64
73
|
|
74
|
+
# Header part showing the maximum number of choices
|
75
|
+
#
|
76
|
+
# @return [String]
|
77
|
+
#
|
78
|
+
# @api private
|
79
|
+
def max_help
|
80
|
+
"(max. #{@max}) "
|
81
|
+
end
|
82
|
+
|
65
83
|
# Render initial help text and then currently selected choices
|
66
84
|
#
|
67
85
|
# @api private
|
68
86
|
def render_header
|
69
87
|
instructions = @prompt.decorate(help, :bright_black)
|
88
|
+
max_suffix = @max ? max_help : ""
|
89
|
+
|
70
90
|
if @done && @echo
|
71
91
|
@prompt.decorate(selected_names, @active_color)
|
72
92
|
elsif @selected.size.nonzero? && @echo
|
73
93
|
help_suffix = filterable? && @filter.any? ? " #{filter_help}" : ""
|
74
|
-
|
94
|
+
max_suffix + selected_names +
|
95
|
+
(@first_render ? " #{instructions}" : help_suffix)
|
75
96
|
elsif @first_render
|
76
|
-
instructions
|
97
|
+
max_suffix + instructions
|
77
98
|
elsif filterable? && @filter.any?
|
78
|
-
filter_help
|
99
|
+
max_suffix + filter_help
|
100
|
+
elsif @max
|
101
|
+
max_help
|
79
102
|
end
|
80
103
|
end
|
81
104
|
|
@@ -96,21 +119,22 @@ module TTY
|
|
96
119
|
def render_menu
|
97
120
|
output = []
|
98
121
|
|
99
|
-
|
122
|
+
sync_paginators if @paging_changed
|
123
|
+
paginator.paginate(choices, @active, @per_page) do |choice, index|
|
100
124
|
num = enumerate? ? (index + 1).to_s + @enum + ' ' : ''
|
101
|
-
indicator = (index + 1 == @active) ? @marker : ' '
|
125
|
+
indicator = (index + 1 == @active) ? @symbols[:marker] : ' '
|
102
126
|
indicator += ' '
|
103
127
|
message = if @selected.include?(choice) && !choice.disabled?
|
104
|
-
selected = @prompt.decorate(symbols[:radio_on], @active_color)
|
128
|
+
selected = @prompt.decorate(@symbols[:radio_on], @active_color)
|
105
129
|
"#{selected} #{num}#{choice.name}"
|
106
130
|
elsif choice.disabled?
|
107
|
-
@prompt.decorate(symbols[:cross], :red) +
|
131
|
+
@prompt.decorate(@symbols[:cross], :red) +
|
108
132
|
" #{num}#{choice.name} #{choice.disabled}"
|
109
133
|
else
|
110
|
-
"#{symbols[:radio_off]} #{num}#{choice.name}"
|
134
|
+
"#{@symbols[:radio_off]} #{num}#{choice.name}"
|
111
135
|
end
|
112
|
-
|
113
|
-
newline = (index ==
|
136
|
+
end_index = paginated? ? paginator.end_index : choices.size - 1
|
137
|
+
newline = (index == end_index) ? '' : "\n"
|
114
138
|
output << indicator + message + newline
|
115
139
|
end
|
116
140
|
|
data/lib/tty/prompt/paginator.rb
CHANGED
@@ -5,23 +5,33 @@ module TTY
|
|
5
5
|
class Paginator
|
6
6
|
DEFAULT_PAGE_SIZE = 6
|
7
7
|
|
8
|
+
# The 0-based index of the first item on this page
|
9
|
+
attr_accessor :start_index
|
10
|
+
|
11
|
+
# The 0-based index of the last item on this page
|
12
|
+
attr_reader :end_index
|
13
|
+
|
14
|
+
# The 0-based index of the active item on this page
|
15
|
+
attr_reader :current_index
|
16
|
+
|
17
|
+
# The 0-based index of the previously active item on this page
|
18
|
+
attr_reader :last_index
|
19
|
+
|
8
20
|
# Create a Paginator
|
9
21
|
#
|
10
22
|
# @api private
|
11
|
-
def initialize(options
|
23
|
+
def initialize(**options)
|
12
24
|
@last_index = Array(options[:default]).flatten.first || 0
|
13
25
|
@per_page = options[:per_page]
|
14
|
-
@
|
26
|
+
@start_index = Array(options[:default]).flatten.first
|
15
27
|
end
|
16
28
|
|
17
|
-
#
|
29
|
+
# Reset current page indexes
|
18
30
|
#
|
19
|
-
# @
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
raise ArgumentError, 'no max index' unless @per_page
|
24
|
-
@lower_index + @per_page - 1
|
31
|
+
# @api private
|
32
|
+
def reset!
|
33
|
+
@start_index = nil
|
34
|
+
@end_index = nil
|
25
35
|
end
|
26
36
|
|
27
37
|
# Check if page size is valid
|
@@ -43,21 +53,21 @@ module TTY
|
|
43
53
|
# number of choice items per page
|
44
54
|
#
|
45
55
|
# @return [Enumerable]
|
56
|
+
# the list between start and end index
|
46
57
|
#
|
47
58
|
# @api public
|
48
59
|
def paginate(list, active, per_page = nil, &block)
|
49
60
|
current_index = active - 1
|
50
61
|
default_size = (list.size <= DEFAULT_PAGE_SIZE ? list.size : DEFAULT_PAGE_SIZE)
|
51
62
|
@per_page = @per_page || per_page || default_size
|
52
|
-
@lower_index ||= current_index
|
53
|
-
@upper_index ||= max_index
|
54
|
-
|
55
63
|
check_page_size!
|
64
|
+
@start_index ||= (current_index / @per_page) * @per_page
|
65
|
+
@end_index ||= @start_index + @per_page - 1
|
56
66
|
|
57
67
|
# Don't paginate short lists
|
58
68
|
if list.size <= @per_page
|
59
|
-
@
|
60
|
-
@
|
69
|
+
@start_index = 0
|
70
|
+
@end_index = list.size - 1
|
61
71
|
if block
|
62
72
|
return list.each_with_index(&block)
|
63
73
|
else
|
@@ -65,33 +75,35 @@ module TTY
|
|
65
75
|
end
|
66
76
|
end
|
67
77
|
|
78
|
+
step = (current_index - @last_index).abs
|
68
79
|
if current_index > @last_index # going up
|
69
|
-
if current_index
|
70
|
-
|
80
|
+
if current_index >= @end_index && current_index < list.size - 1
|
81
|
+
last_page = list.size - @per_page
|
82
|
+
@start_index = [@start_index + step, last_page].min
|
71
83
|
end
|
72
84
|
elsif current_index < @last_index # going down
|
73
|
-
if current_index
|
74
|
-
@
|
85
|
+
if current_index <= @start_index && current_index > 0
|
86
|
+
@start_index = [@start_index - step, 0].max
|
75
87
|
end
|
76
88
|
end
|
77
89
|
|
78
90
|
# Cycle list
|
79
91
|
if current_index.zero?
|
80
|
-
@
|
92
|
+
@start_index = 0
|
81
93
|
elsif current_index == list.size - 1
|
82
|
-
@
|
94
|
+
@start_index = list.size - 1 - (@per_page - 1)
|
83
95
|
end
|
84
96
|
|
85
|
-
@
|
97
|
+
@end_index = @start_index + (@per_page - 1)
|
86
98
|
@last_index = current_index
|
87
99
|
|
88
|
-
sliced_list = list[@
|
89
|
-
|
100
|
+
sliced_list = list[@start_index..@end_index]
|
101
|
+
page_range = (@start_index..@end_index)
|
90
102
|
|
91
|
-
return sliced_list.zip(
|
103
|
+
return sliced_list.zip(page_range).to_enum unless block_given?
|
92
104
|
|
93
105
|
sliced_list.each_with_index do |item, index|
|
94
|
-
block[item, @
|
106
|
+
block[item, @start_index + index]
|
95
107
|
end
|
96
108
|
end
|
97
109
|
end # Paginator
|
data/lib/tty/prompt/question.rb
CHANGED
@@ -34,7 +34,7 @@ module TTY
|
|
34
34
|
# Initialize a Question
|
35
35
|
#
|
36
36
|
# @api public
|
37
|
-
def initialize(prompt, options
|
37
|
+
def initialize(prompt, **options)
|
38
38
|
@prompt = prompt
|
39
39
|
@prefix = options.fetch(:prefix) { @prompt.prefix }
|
40
40
|
@default = options.fetch(:default) { UndefinedSetting }
|
@@ -47,8 +47,10 @@ module TTY
|
|
47
47
|
@active_color = options.fetch(:active_color) { @prompt.active_color }
|
48
48
|
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
49
49
|
@error_color = options.fetch(:error_color) { :red }
|
50
|
+
@value = options.fetch(:value) { UndefinedSetting }
|
50
51
|
@messages = Utils.deep_copy(options.fetch(:messages) { { } })
|
51
52
|
@done = false
|
53
|
+
@first_render = true
|
52
54
|
@input = nil
|
53
55
|
|
54
56
|
@evaluator = Evaluator.new(self)
|
@@ -94,8 +96,7 @@ module TTY
|
|
94
96
|
# @return [self]
|
95
97
|
#
|
96
98
|
# @api public
|
97
|
-
def call(message, &block)
|
98
|
-
return if Utils.blank?(message)
|
99
|
+
def call(message = '', &block)
|
99
100
|
@message = message
|
100
101
|
block.call(self) if block
|
101
102
|
@prompt.subscribe(self) do
|
@@ -131,7 +132,10 @@ module TTY
|
|
131
132
|
#
|
132
133
|
# @api private
|
133
134
|
def render_question
|
134
|
-
header = [
|
135
|
+
header = []
|
136
|
+
if !Utils.blank?(@prefix) || !Utils.blank?(message)
|
137
|
+
header << "#{@prefix}#{message} "
|
138
|
+
end
|
135
139
|
if !echo?
|
136
140
|
header
|
137
141
|
elsif @done
|
@@ -158,7 +162,12 @@ module TTY
|
|
158
162
|
#
|
159
163
|
# @api private
|
160
164
|
def read_input(question)
|
161
|
-
|
165
|
+
options = {echo: echo}
|
166
|
+
if value? && @first_render
|
167
|
+
options[:value] = @value
|
168
|
+
@first_render = false
|
169
|
+
end
|
170
|
+
@prompt.read_line(question, options).chomp
|
162
171
|
end
|
163
172
|
|
164
173
|
# Handle error condition
|
@@ -266,6 +275,21 @@ module TTY
|
|
266
275
|
@validation = (value || block)
|
267
276
|
end
|
268
277
|
|
278
|
+
# Prepopulate input with custom content
|
279
|
+
#
|
280
|
+
# @api public
|
281
|
+
def value(val)
|
282
|
+
return @value if val.nil?
|
283
|
+
@value = val
|
284
|
+
end
|
285
|
+
|
286
|
+
# Check if custom value is present
|
287
|
+
#
|
288
|
+
# @api private
|
289
|
+
def value?
|
290
|
+
@value != UndefinedSetting
|
291
|
+
end
|
292
|
+
|
269
293
|
def validation?
|
270
294
|
@validation != UndefinedSetting
|
271
295
|
end
|
data/lib/tty/prompt/slider.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'symbols'
|
4
|
-
|
5
3
|
module TTY
|
6
4
|
# A class responsible for shell prompt interactions.
|
7
5
|
class Prompt
|
@@ -9,8 +7,6 @@ module TTY
|
|
9
7
|
#
|
10
8
|
# @api public
|
11
9
|
class Slider
|
12
|
-
include Symbols
|
13
|
-
|
14
10
|
HELP = '(Use arrow keys, press Enter to select)'.freeze
|
15
11
|
|
16
12
|
FORMAT = ':slider %d'.freeze
|
@@ -27,7 +23,7 @@ module TTY
|
|
27
23
|
# @option options [String] :format The display format
|
28
24
|
#
|
29
25
|
# @api public
|
30
|
-
def initialize(prompt, options
|
26
|
+
def initialize(prompt, **options)
|
31
27
|
@prompt = prompt
|
32
28
|
@prefix = options.fetch(:prefix) { @prompt.prefix }
|
33
29
|
@min = options.fetch(:min) { 0 }
|
@@ -37,10 +33,22 @@ module TTY
|
|
37
33
|
@active_color = options.fetch(:active_color) { @prompt.active_color }
|
38
34
|
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
39
35
|
@format = options.fetch(:format) { FORMAT }
|
36
|
+
@symbols = @prompt.symbols.merge(options.fetch(:symbols, {}))
|
40
37
|
@first_render = true
|
41
38
|
@done = false
|
42
39
|
end
|
43
40
|
|
41
|
+
# Change symbols used by this prompt
|
42
|
+
#
|
43
|
+
# @param [Hash] new_symbols
|
44
|
+
# the new symbols to use
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
def symbols(new_symbols = (not_set = true))
|
48
|
+
return @symbols if not_set
|
49
|
+
@symbols.merge!(new_symbols)
|
50
|
+
end
|
51
|
+
|
44
52
|
# Setup initial active position
|
45
53
|
#
|
46
54
|
# @return [Integer]
|
@@ -180,9 +188,9 @@ module TTY
|
|
180
188
|
#
|
181
189
|
# @api private
|
182
190
|
def render_slider
|
183
|
-
slider = (symbols[:line] * @active) +
|
184
|
-
@prompt.decorate(symbols[:
|
185
|
-
(symbols[:line] * (range.size - @active - 1))
|
191
|
+
slider = (@symbols[:line] * @active) +
|
192
|
+
@prompt.decorate(@symbols[:bullet], @active_color) +
|
193
|
+
(@symbols[:line] * (range.size - @active - 1))
|
186
194
|
value = " #{range[@active]}"
|
187
195
|
@format.gsub(':slider', slider) % [value]
|
188
196
|
end
|
data/lib/tty/prompt/symbols.rb
CHANGED
@@ -13,17 +13,29 @@ module TTY
|
|
13
13
|
square: '◼',
|
14
14
|
square_empty: '◻',
|
15
15
|
dot: '•',
|
16
|
-
|
16
|
+
bullet: '●',
|
17
|
+
bullet_empty: '○',
|
18
|
+
marker: '‣',
|
17
19
|
line: '─',
|
18
20
|
pipe: '|',
|
19
|
-
handle: 'O',
|
20
21
|
ellipsis: '…',
|
21
22
|
radio_on: '⬢',
|
22
23
|
radio_off: '⬡',
|
23
24
|
checkbox_on: '☒',
|
24
25
|
checkbox_off: '☐',
|
26
|
+
circle: '◯',
|
25
27
|
circle_on: 'ⓧ',
|
26
|
-
circle_off: 'Ⓘ'
|
28
|
+
circle_off: 'Ⓘ',
|
29
|
+
arrow_up: '↑',
|
30
|
+
arrow_down: '↓',
|
31
|
+
arrow_up_down: '↕',
|
32
|
+
arrow_left: '←',
|
33
|
+
arrow_right: '→',
|
34
|
+
arrow_left_right: '↔',
|
35
|
+
heart: '♥',
|
36
|
+
diamond: '♦',
|
37
|
+
club: '♣',
|
38
|
+
spade: '♠'
|
27
39
|
}.freeze
|
28
40
|
|
29
41
|
WIN_KEYS = {
|
@@ -33,17 +45,29 @@ module TTY
|
|
33
45
|
square: '[█]',
|
34
46
|
square_empty: '[ ]',
|
35
47
|
dot: '.',
|
36
|
-
|
48
|
+
bullet: 'O',
|
49
|
+
bullet_empty: '○',
|
50
|
+
marker: '>',
|
37
51
|
line: '-',
|
38
52
|
pipe: '|',
|
39
|
-
handle: 'O',
|
40
53
|
ellipsis: '...',
|
41
54
|
radio_on: '(*)',
|
42
55
|
radio_off: '( )',
|
43
56
|
checkbox_on: '[×]',
|
44
57
|
checkbox_off: '[ ]',
|
58
|
+
circle: '( )',
|
45
59
|
circle_on: '(x)',
|
46
|
-
circle_off: '( )'
|
60
|
+
circle_off: '( )',
|
61
|
+
arrow_up: '↑',
|
62
|
+
arrow_down: '↓',
|
63
|
+
arrow_up_down: '↕',
|
64
|
+
arrow_left: '←',
|
65
|
+
arrow_right: '→',
|
66
|
+
arrow_left_right: '↔',
|
67
|
+
heart: '♥',
|
68
|
+
diamond: '♦',
|
69
|
+
club: '♣',
|
70
|
+
spade: '♠'
|
47
71
|
}.freeze
|
48
72
|
|
49
73
|
def symbols
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
class Timer
|
6
|
+
attr_reader :duration
|
7
|
+
|
8
|
+
attr_reader :total
|
9
|
+
|
10
|
+
attr_reader :interval
|
11
|
+
|
12
|
+
def initialize(duration, interval)
|
13
|
+
@duration = duration
|
14
|
+
@interval = interval
|
15
|
+
@total = 0.0
|
16
|
+
@current = nil
|
17
|
+
@events = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
return if @current
|
22
|
+
|
23
|
+
@current = time_now
|
24
|
+
end
|
25
|
+
|
26
|
+
def stop
|
27
|
+
return unless @current
|
28
|
+
|
29
|
+
@current = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def runtime
|
33
|
+
time_now - @current
|
34
|
+
end
|
35
|
+
|
36
|
+
def on_tick(&block)
|
37
|
+
@events << block
|
38
|
+
end
|
39
|
+
|
40
|
+
def while_remaining
|
41
|
+
start
|
42
|
+
remaining = duration
|
43
|
+
|
44
|
+
if @duration
|
45
|
+
while remaining >= 0.0
|
46
|
+
if runtime >= total
|
47
|
+
tick = duration - @total
|
48
|
+
@events.each { |block| block.(tick) }
|
49
|
+
@total += @interval
|
50
|
+
end
|
51
|
+
|
52
|
+
yield(remaining)
|
53
|
+
remaining = duration - runtime
|
54
|
+
end
|
55
|
+
else
|
56
|
+
loop { yield }
|
57
|
+
end
|
58
|
+
ensure
|
59
|
+
stop
|
60
|
+
end
|
61
|
+
|
62
|
+
if defined?(Process::CLOCK_MONOTONIC)
|
63
|
+
# Object representing current time
|
64
|
+
def time_now
|
65
|
+
::Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
66
|
+
end
|
67
|
+
else
|
68
|
+
# Object represeting current time
|
69
|
+
def time_now
|
70
|
+
::Time.now
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end # Timer
|
74
|
+
end # Prompt
|
75
|
+
end # TTY
|