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
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
|