tty-prompt 0.18.1 → 0.19.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/tty/prompt/expander.rb
CHANGED
@@ -22,6 +22,7 @@ module TTY
|
|
22
22
|
@prompt = prompt
|
23
23
|
@prefix = options.fetch(:prefix) { @prompt.prefix }
|
24
24
|
@default = options.fetch(:default) { 1 }
|
25
|
+
@auto_hint = options.fetch(:auto_hint) { false }
|
25
26
|
@active_color = options.fetch(:active_color) { @prompt.active_color }
|
26
27
|
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
27
28
|
@choices = Choices.new
|
@@ -62,6 +63,7 @@ module TTY
|
|
62
63
|
elsif selected
|
63
64
|
@done = true
|
64
65
|
@selected = selected
|
66
|
+
@hint = nil
|
65
67
|
else
|
66
68
|
@input = ''
|
67
69
|
end
|
@@ -77,6 +79,7 @@ module TTY
|
|
77
79
|
elsif event.value =~ /^[^\e\n\r]/
|
78
80
|
@input += event.value
|
79
81
|
end
|
82
|
+
|
80
83
|
@selected = select_choice(@input)
|
81
84
|
if @selected && !@default_key && collapsed?
|
82
85
|
@hint = @selected.name
|
@@ -208,6 +211,7 @@ module TTY
|
|
208
211
|
#
|
209
212
|
# @api private
|
210
213
|
def render_question
|
214
|
+
load_auto_hint if @auto_hint
|
211
215
|
header = render_header
|
212
216
|
header << render_hint if @hint
|
213
217
|
header << "\n" if @done
|
@@ -219,6 +223,20 @@ module TTY
|
|
219
223
|
header
|
220
224
|
end
|
221
225
|
|
226
|
+
def load_auto_hint
|
227
|
+
if @hint.nil? && collapsed?
|
228
|
+
if @selected
|
229
|
+
@hint = @selected.name
|
230
|
+
else
|
231
|
+
if @input.empty?
|
232
|
+
@hint = @choices[@default - 1].name
|
233
|
+
else
|
234
|
+
@hint = "invalid option"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
222
240
|
def render_footer
|
223
241
|
" Choice [#{@choices[@default - 1].key}]: #{@input}"
|
224
242
|
end
|
@@ -235,7 +253,7 @@ module TTY
|
|
235
253
|
#
|
236
254
|
# @api private
|
237
255
|
def refresh(lines)
|
238
|
-
if @hint && (!@selected || @done)
|
256
|
+
if (@hint && (!@selected || @done)) || (@auto_hint && collapsed?)
|
239
257
|
@hint = nil
|
240
258
|
@prompt.clear_lines(lines, :down) +
|
241
259
|
@prompt.cursor.prev_line
|
data/lib/tty/prompt/keypress.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'question'
|
4
|
-
require_relative '
|
5
|
-
require_relative 'timeout'
|
4
|
+
require_relative 'timer'
|
6
5
|
|
7
6
|
module TTY
|
8
7
|
class Prompt
|
@@ -13,7 +12,7 @@ module TTY
|
|
13
12
|
# @param [Hash] options
|
14
13
|
#
|
15
14
|
# @api public
|
16
|
-
def initialize(prompt, options
|
15
|
+
def initialize(prompt, **options)
|
17
16
|
super
|
18
17
|
@echo = options.fetch(:echo) { false }
|
19
18
|
@keys = options.fetch(:keys) { UndefinedSetting }
|
@@ -21,18 +20,10 @@ module TTY
|
|
21
20
|
@interval = options.fetch(:interval) {
|
22
21
|
(@timeout != UndefinedSetting && @timeout < 1) ? @timeout : 1
|
23
22
|
}
|
23
|
+
@decimals = (@interval.to_s.split('.')[1] || []).size
|
24
24
|
@countdown = @timeout
|
25
|
-
|
26
|
-
|
27
|
-
question = render_question
|
28
|
-
line_size = question.size
|
29
|
-
total_lines = @prompt.count_screen_lines(line_size)
|
30
|
-
@prompt.print(refresh(question.lines.count, total_lines))
|
31
|
-
countdown(time)
|
32
|
-
@prompt.print(render_question)
|
33
|
-
end
|
34
|
-
}
|
35
|
-
@scheduler = Timeout.new(interval_handler: @interval_handler)
|
25
|
+
time = timeout? ? Float(@timeout) : nil
|
26
|
+
@timer = Timer.new(time, Float(@interval))
|
36
27
|
|
37
28
|
@prompt.subscribe(self)
|
38
29
|
end
|
@@ -55,10 +46,8 @@ module TTY
|
|
55
46
|
def keypress(event)
|
56
47
|
if any_key?
|
57
48
|
@done = true
|
58
|
-
@scheduler.cancel
|
59
49
|
elsif @keys.is_a?(Array) && @keys.include?(event.key.name)
|
60
50
|
@done = true
|
61
|
-
@scheduler.cancel
|
62
51
|
else
|
63
52
|
@done = false
|
64
53
|
end
|
@@ -66,36 +55,42 @@ module TTY
|
|
66
55
|
|
67
56
|
def render_question
|
68
57
|
header = super
|
69
|
-
|
58
|
+
if timeout?
|
59
|
+
header.gsub!(/:countdown/, format("%.#{@decimals}f", countdown))
|
60
|
+
end
|
70
61
|
header
|
71
62
|
end
|
72
63
|
|
64
|
+
def interval_handler(time)
|
65
|
+
return if @done
|
66
|
+
|
67
|
+
question = render_question
|
68
|
+
line_size = question.size
|
69
|
+
total_lines = @prompt.count_screen_lines(line_size)
|
70
|
+
@prompt.print(refresh(question.lines.count, total_lines))
|
71
|
+
countdown(time)
|
72
|
+
@prompt.print(render_question)
|
73
|
+
end
|
74
|
+
|
73
75
|
def process_input(question)
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
76
|
+
@prompt.print(render_question)
|
77
|
+
|
78
|
+
@timer.on_tick do |time|
|
79
|
+
interval_handler(time)
|
80
|
+
end
|
81
|
+
|
82
|
+
@timer.while_remaining do |remaining|
|
83
|
+
break if @done
|
84
|
+
@input = @prompt.read_keypress(nonblock: true)
|
79
85
|
end
|
86
|
+
countdown(0) unless @done
|
87
|
+
|
80
88
|
@evaluator.(@input)
|
81
89
|
end
|
82
90
|
|
83
91
|
def refresh(lines, lines_to_clear)
|
84
92
|
@prompt.clear_lines(lines)
|
85
93
|
end
|
86
|
-
|
87
|
-
# Wait for keypress or timeout
|
88
|
-
#
|
89
|
-
# @api private
|
90
|
-
def time(&job)
|
91
|
-
if timeout?
|
92
|
-
time = Float(@timeout)
|
93
|
-
interval = Float(@interval)
|
94
|
-
@scheduler.timeout(time, interval, &job)
|
95
|
-
else
|
96
|
-
job.()
|
97
|
-
end
|
98
|
-
end
|
99
94
|
end # Keypress
|
100
95
|
end # Prompt
|
101
96
|
end # TTY
|
data/lib/tty/prompt/list.rb
CHANGED
@@ -4,7 +4,7 @@ require 'English'
|
|
4
4
|
|
5
5
|
require_relative 'choices'
|
6
6
|
require_relative 'paginator'
|
7
|
-
require_relative '
|
7
|
+
require_relative 'block_paginator'
|
8
8
|
|
9
9
|
module TTY
|
10
10
|
class Prompt
|
@@ -13,11 +13,7 @@ module TTY
|
|
13
13
|
#
|
14
14
|
# @api private
|
15
15
|
class List
|
16
|
-
|
17
|
-
|
18
|
-
HELP = '(Use arrow%s keys, press Enter to select%s)'
|
19
|
-
|
20
|
-
PAGE_HELP = '(Move up or down to reveal more choices)'
|
16
|
+
HELP = '(Use %s arrow%s keys, press Enter to select%s)'
|
21
17
|
|
22
18
|
# Allowed keys for filter, along with backspace and canc.
|
23
19
|
FILTER_KEYS_MATCHER = /\A([[:alnum:]]|[[:punct:]])\Z/.freeze
|
@@ -36,7 +32,7 @@ module TTY
|
|
36
32
|
# the delimiter for the item index
|
37
33
|
#
|
38
34
|
# @api public
|
39
|
-
def initialize(prompt, options
|
35
|
+
def initialize(prompt, **options)
|
40
36
|
check_options_consistency(options)
|
41
37
|
|
42
38
|
@prompt = prompt
|
@@ -46,23 +42,30 @@ module TTY
|
|
46
42
|
@choices = Choices.new
|
47
43
|
@active_color = options.fetch(:active_color) { @prompt.active_color }
|
48
44
|
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
49
|
-
@marker = options.fetch(:marker) { symbols[:pointer] }
|
50
45
|
@cycle = options.fetch(:cycle) { false }
|
51
46
|
@filterable = options.fetch(:filter) { false }
|
47
|
+
@symbols = @prompt.symbols.merge(options.fetch(:symbols, {}))
|
52
48
|
@filter = []
|
49
|
+
@filter_cache = {}
|
53
50
|
@help = options[:help]
|
54
51
|
@first_render = true
|
55
52
|
@done = false
|
56
53
|
@per_page = options[:per_page]
|
57
|
-
@page_help = options[:page_help] || PAGE_HELP
|
58
54
|
@paginator = Paginator.new
|
55
|
+
@block_paginator = BlockPaginator.new
|
56
|
+
@by_page = false
|
57
|
+
@paging_changed = false
|
59
58
|
end
|
60
59
|
|
61
|
-
#
|
60
|
+
# Change symbols used by this prompt
|
61
|
+
#
|
62
|
+
# @param [Hash] new_symbols
|
63
|
+
# the new symbols to use
|
62
64
|
#
|
63
65
|
# @api public
|
64
|
-
def
|
65
|
-
@
|
66
|
+
def symbols(new_symbols = (not_set = true))
|
67
|
+
return @symbols if not_set
|
68
|
+
@symbols.merge!(new_symbols)
|
66
69
|
end
|
67
70
|
|
68
71
|
# Set default option selected
|
@@ -72,6 +75,32 @@ module TTY
|
|
72
75
|
@default = default_values
|
73
76
|
end
|
74
77
|
|
78
|
+
# Select paginator based on the current navigation key
|
79
|
+
#
|
80
|
+
# @return [Paginator]
|
81
|
+
#
|
82
|
+
# @api private
|
83
|
+
def paginator
|
84
|
+
@by_page ? @block_paginator : @paginator
|
85
|
+
end
|
86
|
+
|
87
|
+
# Synchronize paginators start positions
|
88
|
+
#
|
89
|
+
# @api private
|
90
|
+
def sync_paginators
|
91
|
+
if @by_page
|
92
|
+
if @paginator.start_index
|
93
|
+
@block_paginator.reset!
|
94
|
+
@block_paginator.start_index = @paginator.start_index
|
95
|
+
end
|
96
|
+
else
|
97
|
+
if @block_paginator.start_index
|
98
|
+
@paginator.reset!
|
99
|
+
@paginator.start_index = @block_paginator.start_index
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
75
104
|
# Set number of items per page
|
76
105
|
#
|
77
106
|
# @api public
|
@@ -92,13 +121,6 @@ module TTY
|
|
92
121
|
choices.size > page_size
|
93
122
|
end
|
94
123
|
|
95
|
-
# @param [String] text
|
96
|
-
# the help text to display per page
|
97
|
-
# @api pbulic
|
98
|
-
def page_help(text)
|
99
|
-
@page_help = text
|
100
|
-
end
|
101
|
-
|
102
124
|
# Provide help information
|
103
125
|
#
|
104
126
|
# @param [String] value
|
@@ -113,6 +135,21 @@ module TTY
|
|
113
135
|
@help = (@help.nil? && !not_set) ? value : default_help
|
114
136
|
end
|
115
137
|
|
138
|
+
# Information about arrow keys
|
139
|
+
#
|
140
|
+
# @return [String]
|
141
|
+
#
|
142
|
+
# @api private
|
143
|
+
def arrows_help
|
144
|
+
up_down = @symbols[:arrow_up] + "/" + @symbols[:arrow_down]
|
145
|
+
left_right = @symbols[:arrow_left] + "/" + @symbols[:arrow_right]
|
146
|
+
|
147
|
+
arrows = [up_down]
|
148
|
+
arrows << " and " if paginated?
|
149
|
+
arrows << left_right if paginated?
|
150
|
+
arrows.join
|
151
|
+
end
|
152
|
+
|
116
153
|
# Default help text
|
117
154
|
#
|
118
155
|
# @api public
|
@@ -126,7 +163,7 @@ module TTY
|
|
126
163
|
['', '']
|
127
164
|
end
|
128
165
|
|
129
|
-
format(self.class::HELP, *tokens)
|
166
|
+
format(self.class::HELP, arrows_help, *tokens)
|
130
167
|
end
|
131
168
|
|
132
169
|
# Set selecting active index using number pad
|
@@ -140,6 +177,7 @@ module TTY
|
|
140
177
|
#
|
141
178
|
# @api public
|
142
179
|
def choice(*value, &block)
|
180
|
+
@filter_cache = {}
|
143
181
|
if block
|
144
182
|
@choices << (value << block)
|
145
183
|
else
|
@@ -159,12 +197,14 @@ module TTY
|
|
159
197
|
if !filterable? || @filter.empty?
|
160
198
|
@choices
|
161
199
|
else
|
162
|
-
@
|
200
|
+
filter_value = @filter.join.downcase
|
201
|
+
@filter_cache[filter_value] ||= @choices.select do |choice|
|
163
202
|
!choice.disabled? &&
|
164
|
-
choice.name.downcase.include?(
|
203
|
+
choice.name.downcase.include?(filter_value)
|
165
204
|
end
|
166
205
|
end
|
167
206
|
else
|
207
|
+
@filter_cache = {}
|
168
208
|
values.each { |val| @choices << val }
|
169
209
|
end
|
170
210
|
end
|
@@ -223,6 +263,9 @@ module TTY
|
|
223
263
|
|
224
264
|
@active = prev_active if prev_active
|
225
265
|
end
|
266
|
+
|
267
|
+
@paging_changed = @by_page
|
268
|
+
@by_page = false
|
226
269
|
end
|
227
270
|
|
228
271
|
def keydown(*)
|
@@ -237,9 +280,50 @@ module TTY
|
|
237
280
|
|
238
281
|
@active = next_active if next_active
|
239
282
|
end
|
283
|
+
@paging_changed = @by_page
|
284
|
+
@by_page = false
|
240
285
|
end
|
241
286
|
alias keytab keydown
|
242
287
|
|
288
|
+
# Moves all choices page by page keeping the current selected item
|
289
|
+
# at the same level on each page.
|
290
|
+
#
|
291
|
+
# When the choice on a page is outside of next page range then
|
292
|
+
# adjust it to the last item, otherwise leave unchanged.
|
293
|
+
def keyright(*)
|
294
|
+
if (@active + page_size) <= @choices.size
|
295
|
+
searchable = ((@active + page_size)..choices.length)
|
296
|
+
@active = search_choice_in(searchable)
|
297
|
+
elsif @active <= @choices.size # last page shorter
|
298
|
+
current = @active % page_size
|
299
|
+
remaining = @choices.size % page_size
|
300
|
+
if current.zero? || (remaining > 0 && current > remaining)
|
301
|
+
searchable = @choices.size.downto(0).to_a
|
302
|
+
@active = search_choice_in(searchable)
|
303
|
+
elsif @cycle
|
304
|
+
searchable = ((current.zero? ? page_size : current)..choices.length)
|
305
|
+
@active = search_choice_in(searchable)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
@paging_changed = !@by_page
|
310
|
+
@by_page = true
|
311
|
+
end
|
312
|
+
alias keypage_down keyright
|
313
|
+
|
314
|
+
def keyleft(*)
|
315
|
+
if (@active - page_size) > 0
|
316
|
+
searchable = ((@active - page_size)..choices.length)
|
317
|
+
@active = search_choice_in(searchable)
|
318
|
+
elsif @cycle
|
319
|
+
searchable = @choices.size.downto(1).to_a
|
320
|
+
@active = search_choice_in(searchable)
|
321
|
+
end
|
322
|
+
@paging_changed = !@by_page
|
323
|
+
@by_page = true
|
324
|
+
end
|
325
|
+
alias keypage_up keyleft
|
326
|
+
|
243
327
|
def keypress(event)
|
244
328
|
return unless filterable?
|
245
329
|
|
@@ -372,7 +456,6 @@ module TTY
|
|
372
456
|
@first_render = false
|
373
457
|
unless @done
|
374
458
|
header << render_menu
|
375
|
-
header << render_footer
|
376
459
|
end
|
377
460
|
header.join
|
378
461
|
end
|
@@ -419,36 +502,25 @@ module TTY
|
|
419
502
|
def render_menu
|
420
503
|
output = []
|
421
504
|
|
422
|
-
|
505
|
+
sync_paginators if @paging_changed
|
506
|
+
paginator.paginate(choices, @active, @per_page) do |choice, index|
|
423
507
|
num = enumerate? ? (index + 1).to_s + @enum + ' ' : ''
|
424
508
|
message = if index + 1 == @active && !choice.disabled?
|
425
|
-
selected = "#{@marker} #{num}#{choice.name}"
|
509
|
+
selected = "#{@symbols[:marker]} #{num}#{choice.name}"
|
426
510
|
@prompt.decorate(selected.to_s, @active_color)
|
427
511
|
elsif choice.disabled?
|
428
|
-
@prompt.decorate(symbols[:cross], :red) +
|
512
|
+
@prompt.decorate(@symbols[:cross], :red) +
|
429
513
|
" #{num}#{choice.name} #{choice.disabled}"
|
430
514
|
else
|
431
515
|
" #{num}#{choice.name}"
|
432
516
|
end
|
433
|
-
|
434
|
-
newline = (index ==
|
517
|
+
end_index = paginated? ? paginator.end_index : choices.size - 1
|
518
|
+
newline = (index == end_index) ? '' : "\n"
|
435
519
|
output << (message + newline)
|
436
520
|
end
|
437
521
|
|
438
522
|
output.join
|
439
523
|
end
|
440
|
-
|
441
|
-
# Render page info footer
|
442
|
-
#
|
443
|
-
# @return [String]
|
444
|
-
#
|
445
|
-
# @api private
|
446
|
-
def render_footer
|
447
|
-
return '' unless paginated?
|
448
|
-
|
449
|
-
colored_footer = @prompt.decorate(@page_help, @help_color)
|
450
|
-
"\n" + colored_footer
|
451
|
-
end
|
452
524
|
end # List
|
453
525
|
end # Prompt
|
454
526
|
end # TTY
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'question'
|
4
|
-
require_relative 'symbols'
|
5
4
|
|
6
5
|
module TTY
|
7
6
|
class Prompt
|
@@ -12,9 +11,9 @@ module TTY
|
|
12
11
|
# @option options [String] :mask
|
13
12
|
#
|
14
13
|
# @api public
|
15
|
-
def initialize(prompt, options
|
14
|
+
def initialize(prompt, **options)
|
16
15
|
super
|
17
|
-
@mask = options.fetch(:mask) {
|
16
|
+
@mask = options.fetch(:mask) { @prompt.symbols[:dot] }
|
18
17
|
@done_masked = false
|
19
18
|
@failure = false
|
20
19
|
end
|