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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +174 -63
  4. data/Rakefile +2 -2
  5. data/examples/ask_blank.rb +9 -0
  6. data/examples/enum_select_disabled.rb +1 -1
  7. data/examples/enum_select_paged.rb +1 -1
  8. data/examples/expand_auto.rb +29 -0
  9. data/examples/mask.rb +1 -1
  10. data/examples/multi_select.rb +1 -1
  11. data/examples/multi_select_disabled_paged.rb +22 -0
  12. data/examples/multi_select_paged.rb +1 -1
  13. data/examples/select_disabled_paged.rb +22 -0
  14. data/examples/select_paginated.rb +1 -1
  15. data/lib/tty/prompt.rb +46 -10
  16. data/lib/tty/prompt/{enum_paginator.rb → block_paginator.rb} +19 -18
  17. data/lib/tty/prompt/choice.rb +1 -3
  18. data/lib/tty/prompt/enum_list.rb +31 -9
  19. data/lib/tty/prompt/expander.rb +19 -1
  20. data/lib/tty/prompt/keypress.rb +30 -35
  21. data/lib/tty/prompt/list.rb +112 -40
  22. data/lib/tty/prompt/mask_question.rb +2 -3
  23. data/lib/tty/prompt/multi_list.rb +36 -12
  24. data/lib/tty/prompt/paginator.rb +37 -25
  25. data/lib/tty/prompt/question.rb +29 -5
  26. data/lib/tty/prompt/slider.rb +16 -8
  27. data/lib/tty/prompt/symbols.rb +30 -6
  28. data/lib/tty/prompt/timer.rb +75 -0
  29. data/lib/tty/prompt/version.rb +1 -1
  30. data/spec/spec_helper.rb +18 -2
  31. data/spec/unit/ask_spec.rb +45 -4
  32. data/spec/unit/{enum_paginator_spec.rb → block_paginator_spec.rb} +14 -5
  33. data/spec/unit/choice/from_spec.rb +16 -0
  34. data/spec/unit/enum_select_spec.rb +104 -32
  35. data/spec/unit/expand_spec.rb +104 -12
  36. data/spec/unit/keypress_spec.rb +2 -8
  37. data/spec/unit/mask_spec.rb +9 -1
  38. data/spec/unit/multi_select_spec.rb +348 -118
  39. data/spec/unit/paginator_spec.rb +29 -10
  40. data/spec/unit/select_spec.rb +390 -108
  41. data/spec/unit/slider_spec.rb +48 -6
  42. data/spec/unit/timer_spec.rb +29 -0
  43. data/tty-prompt.gemspec +4 -6
  44. metadata +17 -46
  45. 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
- selected_names + (@first_render ? " #{instructions}" : help_suffix)
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
- @paginator.paginate(choices, @active, @per_page) do |choice, index|
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
- max_index = paginated? ? @paginator.max_index : choices.size - 1
113
- newline = (index == max_index) ? '' : "\n"
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
 
@@ -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
- @lower_index = Array(options[:default]).flatten.first
26
+ @start_index = Array(options[:default]).flatten.first
15
27
  end
16
28
 
17
- # Maximum index for current pagination
29
+ # Reset current page indexes
18
30
  #
19
- # @return [Integer]
20
- #
21
- # @api public
22
- def max_index
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
- @lower_index = 0
60
- @upper_index = list.size - 1
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 > @upper_index && current_index < list.size - 1
70
- @lower_index += 1
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 < @lower_index && current_index > 0
74
- @lower_index -= 1
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
- @lower_index = 0
92
+ @start_index = 0
81
93
  elsif current_index == list.size - 1
82
- @lower_index = list.size - 1 - (@per_page - 1)
94
+ @start_index = list.size - 1 - (@per_page - 1)
83
95
  end
84
96
 
85
- @upper_index = @lower_index + (@per_page - 1)
97
+ @end_index = @start_index + (@per_page - 1)
86
98
  @last_index = current_index
87
99
 
88
- sliced_list = list[@lower_index..@upper_index]
89
- indices = (@lower_index..@upper_index)
100
+ sliced_list = list[@start_index..@end_index]
101
+ page_range = (@start_index..@end_index)
90
102
 
91
- return sliced_list.zip(indices).to_enum unless block_given?
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, @lower_index + index]
106
+ block[item, @start_index + index]
95
107
  end
96
108
  end
97
109
  end # Paginator
@@ -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 = ["#{@prefix}#{message} "]
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
- @prompt.read_line(question, echo: echo).chomp
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
@@ -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[:handle], @active_color) +
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
@@ -13,17 +13,29 @@ module TTY
13
13
  square: '◼',
14
14
  square_empty: '◻',
15
15
  dot: '•',
16
- pointer: '',
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
- pointer: '>',
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