tty-prompt 0.18.0 → 0.22.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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -0
  3. data/README.md +549 -248
  4. data/lib/tty-prompt.rb +1 -2
  5. data/lib/tty/prompt.rb +187 -143
  6. data/lib/tty/prompt/answers_collector.rb +5 -5
  7. data/lib/tty/prompt/{enum_paginator.rb → block_paginator.rb} +20 -19
  8. data/lib/tty/prompt/choice.rb +5 -7
  9. data/lib/tty/prompt/choices.rb +29 -11
  10. data/lib/tty/prompt/confirm_question.rb +38 -16
  11. data/lib/tty/prompt/const.rb +17 -0
  12. data/lib/tty/prompt/converter_dsl.rb +6 -7
  13. data/lib/tty/prompt/converter_registry.rb +31 -26
  14. data/lib/tty/prompt/converters.rb +139 -32
  15. data/lib/tty/prompt/enum_list.rb +57 -27
  16. data/lib/tty/prompt/errors.rb +31 -0
  17. data/lib/tty/prompt/evaluator.rb +1 -1
  18. data/lib/tty/prompt/expander.rb +39 -13
  19. data/lib/tty/prompt/keypress.rb +31 -36
  20. data/lib/tty/prompt/list.rb +175 -65
  21. data/lib/tty/prompt/mask_question.rb +4 -5
  22. data/lib/tty/prompt/multi_list.rb +124 -33
  23. data/lib/tty/prompt/multiline.rb +7 -6
  24. data/lib/tty/prompt/paginator.rb +38 -26
  25. data/lib/tty/prompt/question.rb +83 -34
  26. data/lib/tty/prompt/question/checks.rb +18 -0
  27. data/lib/tty/prompt/question/validation.rb +3 -3
  28. data/lib/tty/prompt/selected_choices.rb +76 -0
  29. data/lib/tty/prompt/slider.rb +83 -16
  30. data/lib/tty/prompt/statement.rb +3 -3
  31. data/lib/tty/prompt/suggestion.rb +6 -6
  32. data/lib/tty/prompt/symbols.rb +58 -34
  33. data/lib/tty/prompt/test.rb +36 -0
  34. data/lib/tty/prompt/timer.rb +75 -0
  35. data/lib/tty/prompt/utils.rb +1 -3
  36. data/lib/tty/prompt/version.rb +1 -1
  37. metadata +29 -227
  38. data/Rakefile +0 -8
  39. data/examples/ask.rb +0 -7
  40. data/examples/ask_valid.rb +0 -12
  41. data/examples/collect.rb +0 -21
  42. data/examples/echo.rb +0 -11
  43. data/examples/enum_select.rb +0 -7
  44. data/examples/enum_select_disabled.rb +0 -16
  45. data/examples/enum_select_paged.rb +0 -9
  46. data/examples/enum_select_wrapped.rb +0 -15
  47. data/examples/expand.rb +0 -29
  48. data/examples/in.rb +0 -9
  49. data/examples/inputs.rb +0 -10
  50. data/examples/key_events.rb +0 -15
  51. data/examples/keypress.rb +0 -9
  52. data/examples/mask.rb +0 -13
  53. data/examples/multi_select.rb +0 -8
  54. data/examples/multi_select_disabled.rb +0 -17
  55. data/examples/multi_select_paged.rb +0 -9
  56. data/examples/multi_select_wrapped.rb +0 -15
  57. data/examples/multiline.rb +0 -9
  58. data/examples/pause.rb +0 -9
  59. data/examples/select.rb +0 -20
  60. data/examples/select_disabled.rb +0 -18
  61. data/examples/select_enum.rb +0 -8
  62. data/examples/select_filtered.rb +0 -11
  63. data/examples/select_paginated.rb +0 -11
  64. data/examples/select_wrapped.rb +0 -15
  65. data/examples/slider.rb +0 -6
  66. data/examples/validation.rb +0 -9
  67. data/examples/yes_no.rb +0 -7
  68. data/lib/tty/prompt/messages.rb +0 -49
  69. data/lib/tty/prompt/timeout.rb +0 -78
  70. data/lib/tty/test_prompt.rb +0 -20
  71. data/spec/spec_helper.rb +0 -45
  72. data/spec/unit/ask_spec.rb +0 -132
  73. data/spec/unit/choice/eql_spec.rb +0 -22
  74. data/spec/unit/choice/from_spec.rb +0 -96
  75. data/spec/unit/choices/add_spec.rb +0 -12
  76. data/spec/unit/choices/each_spec.rb +0 -13
  77. data/spec/unit/choices/find_by_spec.rb +0 -10
  78. data/spec/unit/choices/new_spec.rb +0 -10
  79. data/spec/unit/choices/pluck_spec.rb +0 -9
  80. data/spec/unit/collect_spec.rb +0 -96
  81. data/spec/unit/converters/convert_bool_spec.rb +0 -58
  82. data/spec/unit/converters/convert_char_spec.rb +0 -11
  83. data/spec/unit/converters/convert_custom_spec.rb +0 -14
  84. data/spec/unit/converters/convert_date_spec.rb +0 -34
  85. data/spec/unit/converters/convert_file_spec.rb +0 -18
  86. data/spec/unit/converters/convert_number_spec.rb +0 -39
  87. data/spec/unit/converters/convert_path_spec.rb +0 -15
  88. data/spec/unit/converters/convert_range_spec.rb +0 -22
  89. data/spec/unit/converters/convert_regex_spec.rb +0 -12
  90. data/spec/unit/converters/convert_string_spec.rb +0 -21
  91. data/spec/unit/converters/on_error_spec.rb +0 -9
  92. data/spec/unit/distance/distance_spec.rb +0 -73
  93. data/spec/unit/enum_paginator_spec.rb +0 -75
  94. data/spec/unit/enum_select_spec.rb +0 -446
  95. data/spec/unit/error_spec.rb +0 -20
  96. data/spec/unit/evaluator_spec.rb +0 -67
  97. data/spec/unit/expand_spec.rb +0 -198
  98. data/spec/unit/keypress_spec.rb +0 -72
  99. data/spec/unit/mask_spec.rb +0 -132
  100. data/spec/unit/multi_select_spec.rb +0 -495
  101. data/spec/unit/multiline_spec.rb +0 -77
  102. data/spec/unit/new_spec.rb +0 -20
  103. data/spec/unit/ok_spec.rb +0 -10
  104. data/spec/unit/paginator_spec.rb +0 -73
  105. data/spec/unit/question/checks_spec.rb +0 -97
  106. data/spec/unit/question/default_spec.rb +0 -31
  107. data/spec/unit/question/echo_spec.rb +0 -38
  108. data/spec/unit/question/in_spec.rb +0 -115
  109. data/spec/unit/question/initialize_spec.rb +0 -12
  110. data/spec/unit/question/modifier/apply_to_spec.rb +0 -24
  111. data/spec/unit/question/modifier/letter_case_spec.rb +0 -41
  112. data/spec/unit/question/modifier/whitespace_spec.rb +0 -51
  113. data/spec/unit/question/modify_spec.rb +0 -41
  114. data/spec/unit/question/required_spec.rb +0 -92
  115. data/spec/unit/question/validate_spec.rb +0 -115
  116. data/spec/unit/question/validation/call_spec.rb +0 -31
  117. data/spec/unit/question/validation/coerce_spec.rb +0 -30
  118. data/spec/unit/result_spec.rb +0 -40
  119. data/spec/unit/say_spec.rb +0 -67
  120. data/spec/unit/select_spec.rb +0 -643
  121. data/spec/unit/slider_spec.rb +0 -100
  122. data/spec/unit/statement/initialize_spec.rb +0 -15
  123. data/spec/unit/subscribe_spec.rb +0 -22
  124. data/spec/unit/suggest_spec.rb +0 -28
  125. data/spec/unit/warn_spec.rb +0 -21
  126. data/spec/unit/yes_no_spec.rb +0 -251
  127. data/tasks/console.rake +0 -11
  128. data/tasks/coverage.rake +0 -11
  129. data/tasks/spec.rake +0 -29
  130. data/tty-prompt.gemspec +0 -33
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'choices'
3
+ require_relative "choices"
4
4
 
5
5
  module TTY
6
6
  class Prompt
@@ -10,8 +10,8 @@ module TTY
10
10
  # @api private
11
11
  class Expander
12
12
  HELP_CHOICE = {
13
- key: 'h',
14
- name: 'print help',
13
+ key: "h",
14
+ name: "print help",
15
15
  value: :help
16
16
  }.freeze
17
17
 
@@ -22,8 +22,10 @@ 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 }
28
+ @quiet = options.fetch(:quiet) { @prompt.quiet }
27
29
  @choices = Choices.new
28
30
  @selected = nil
29
31
  @done = false
@@ -55,15 +57,16 @@ module TTY
55
57
 
56
58
  selected = select_choice(@input)
57
59
 
58
- if selected && selected.key.to_s == 'h'
60
+ if selected && selected.key.to_s == "h"
59
61
  expand
60
62
  @selected = nil
61
- @input = ''
63
+ @input = ""
62
64
  elsif selected
63
65
  @done = true
64
66
  @selected = selected
67
+ @hint = nil
65
68
  else
66
- @input = ''
69
+ @input = ""
67
70
  end
68
71
  end
69
72
  alias keyreturn keyenter
@@ -77,6 +80,7 @@ module TTY
77
80
  elsif event.value =~ /^[^\e\n\r]/
78
81
  @input += event.value
79
82
  end
83
+
80
84
  @selected = select_choice(@input)
81
85
  if @selected && !@default_key && collapsed?
82
86
  @hint = @selected.name
@@ -100,6 +104,13 @@ module TTY
100
104
  @default = value
101
105
  end
102
106
 
107
+ # Set quiet mode.
108
+ #
109
+ # @api public
110
+ def quiet(value)
111
+ @quiet = value
112
+ end
113
+
103
114
  # Add a single choice
104
115
  #
105
116
  # @api public
@@ -151,19 +162,19 @@ module TTY
151
162
  elsif @input.to_s.empty? && default_key
152
163
  keys[@default - 1] = @prompt.decorate(default_key, @active_color)
153
164
  end
154
- keys.join(',')
165
+ keys.join(",")
155
166
  end
156
167
 
157
168
  # @api private
158
169
  def render
159
- @input = ''
170
+ @input = ""
160
171
  until @done
161
172
  question = render_question
162
173
  @prompt.print(question)
163
174
  read_input
164
175
  @prompt.print(refresh(question.lines.count))
165
176
  end
166
- @prompt.print(render_question)
177
+ @prompt.print(render_question) unless @quiet
167
178
  answer
168
179
  end
169
180
 
@@ -196,7 +207,7 @@ module TTY
196
207
  #
197
208
  # @api private
198
209
  def render_hint
199
- "\n" + @prompt.decorate('>> ', @active_color) +
210
+ "\n" + @prompt.decorate(">> ", @active_color) +
200
211
  @hint +
201
212
  @prompt.cursor.prev_line +
202
213
  @prompt.cursor.forward(@prompt.strip(render_header).size)
@@ -208,6 +219,7 @@ module TTY
208
219
  #
209
220
  # @api private
210
221
  def render_question
222
+ load_auto_hint if @auto_hint
211
223
  header = render_header
212
224
  header << render_hint if @hint
213
225
  header << "\n" if @done
@@ -219,6 +231,20 @@ module TTY
219
231
  header
220
232
  end
221
233
 
234
+ def load_auto_hint
235
+ if @hint.nil? && collapsed?
236
+ if @selected
237
+ @hint = @selected.name
238
+ else
239
+ if @input.empty?
240
+ @hint = @choices[@default - 1].name
241
+ else
242
+ @hint = "invalid option"
243
+ end
244
+ end
245
+ end
246
+ end
247
+
222
248
  def render_footer
223
249
  " Choice [#{@choices[@default - 1].key}]: #{@input}"
224
250
  end
@@ -235,7 +261,7 @@ module TTY
235
261
  #
236
262
  # @api private
237
263
  def refresh(lines)
238
- if @hint && (!@selected || @done)
264
+ if (@hint && (!@selected || @done)) || (@auto_hint && collapsed?)
239
265
  @hint = nil
240
266
  @prompt.clear_lines(lines, :down) +
241
267
  @prompt.cursor.prev_line
@@ -256,7 +282,7 @@ module TTY
256
282
  if @selected && @selected.key == choice.key
257
283
  chosen = @prompt.decorate(chosen, @active_color)
258
284
  end
259
- output << ' ' + chosen + "\n"
285
+ output << " " + chosen + "\n"
260
286
  end
261
287
  output.join
262
288
  end
@@ -276,7 +302,7 @@ module TTY
276
302
  if choice.key.length != 1
277
303
  errors << "Choice key `#{choice.key}` is more than one character long."
278
304
  end
279
- if choice.key.to_s == 'h'
305
+ if choice.key.to_s == "h"
280
306
  errors << "Choice key `#{choice.key}` is reserved for help menu."
281
307
  end
282
308
  if keys.include?(choice.key)
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'question'
4
- require_relative 'symbols'
5
- require_relative 'timeout'
3
+ require_relative "question"
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
- @interval_handler = proc { |time|
26
- unless @done
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
- header.gsub!(/:countdown/, countdown.to_s)
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
- time do
75
- @prompt.print(render_question)
76
- until @done
77
- @input = @prompt.read_keypress
78
- end
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
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'English'
3
+ require "English"
4
4
 
5
- require_relative 'choices'
6
- require_relative 'paginator'
7
- require_relative 'symbols'
5
+ require_relative "choices"
6
+ require_relative "paginator"
7
+ require_relative "block_paginator"
8
8
 
9
9
  module TTY
10
10
  class Prompt
@@ -13,14 +13,8 @@ module TTY
13
13
  #
14
14
  # @api private
15
15
  class List
16
- include Symbols
17
-
18
- HELP = '(Use arrow%s keys, press Enter to select%s)'.freeze
19
-
20
- PAGE_HELP = '(Move up or down to reveal more choices)'.freeze
21
-
22
16
  # Allowed keys for filter, along with backspace and canc.
23
- FILTER_KEYS_MATCHER = /\A([[:alnum:]]|[[:punct:]])\Z/
17
+ FILTER_KEYS_MATCHER = /\A([[:alnum:]]|[[:punct:]])\Z/.freeze
24
18
 
25
19
  # Create instance of TTY::Prompt::List menu.
26
20
  #
@@ -36,34 +30,42 @@ module TTY
36
30
  # the delimiter for the item index
37
31
  #
38
32
  # @api public
39
- def initialize(prompt, options = {})
33
+ def initialize(prompt, **options)
40
34
  check_options_consistency(options)
41
35
 
42
36
  @prompt = prompt
43
37
  @prefix = options.fetch(:prefix) { @prompt.prefix }
44
38
  @enum = options.fetch(:enum) { nil }
45
- @default = Array[options.fetch(:default) { 1 }]
46
- @active = @default.first
39
+ @default = Array(options[:default])
47
40
  @choices = Choices.new
48
41
  @active_color = options.fetch(:active_color) { @prompt.active_color }
49
42
  @help_color = options.fetch(:help_color) { @prompt.help_color }
50
- @marker = options.fetch(:marker) { symbols[:pointer] }
51
43
  @cycle = options.fetch(:cycle) { false }
52
44
  @filterable = options.fetch(:filter) { false }
45
+ @symbols = @prompt.symbols.merge(options.fetch(:symbols, {}))
46
+ @quiet = options.fetch(:quiet) { @prompt.quiet }
53
47
  @filter = []
48
+ @filter_cache = {}
54
49
  @help = options[:help]
50
+ @show_help = options.fetch(:show_help) { :start }
55
51
  @first_render = true
56
52
  @done = false
57
53
  @per_page = options[:per_page]
58
- @page_help = options[:page_help] || PAGE_HELP
59
54
  @paginator = Paginator.new
55
+ @block_paginator = BlockPaginator.new
56
+ @by_page = false
57
+ @paging_changed = false
60
58
  end
61
59
 
62
- # Set marker
60
+ # Change symbols used by this prompt
61
+ #
62
+ # @param [Hash] new_symbols
63
+ # the new symbols to use
63
64
  #
64
65
  # @api public
65
- def marker(value)
66
- @marker = value
66
+ def symbols(new_symbols = (not_set = true))
67
+ return @symbols if not_set
68
+ @symbols.merge!(new_symbols)
67
69
  end
68
70
 
69
71
  # Set default option selected
@@ -73,6 +75,32 @@ module TTY
73
75
  @default = default_values
74
76
  end
75
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
+
76
104
  # Set number of items per page
77
105
  #
78
106
  # @api public
@@ -93,13 +121,6 @@ module TTY
93
121
  choices.size > page_size
94
122
  end
95
123
 
96
- # @param [String] text
97
- # the help text to display per page
98
- # @api pbulic
99
- def page_help(text)
100
- @page_help = text
101
- end
102
-
103
124
  # Provide help information
104
125
  #
105
126
  # @param [String] value
@@ -114,20 +135,46 @@ module TTY
114
135
  @help = (@help.nil? && !not_set) ? value : default_help
115
136
  end
116
137
 
117
- # Default help text
138
+ # Change when help is displayed
118
139
  #
119
140
  # @api public
120
- def default_help
121
- # Note that enumeration and filter are mutually exclusive
122
- tokens = if enumerate?
123
- [" or number (1-#{choices.size})", '']
124
- elsif filterable?
125
- ['', ", and letter keys to filter"]
126
- else
127
- ['', '']
128
- end
141
+ def show_help(value = (not_set = true))
142
+ return @show_ehlp if not_set
143
+
144
+ @show_help = value
145
+ end
146
+
147
+ # Information about arrow keys
148
+ #
149
+ # @return [String]
150
+ #
151
+ # @api private
152
+ def arrows_help
153
+ up_down = @symbols[:arrow_up] + "/" + @symbols[:arrow_down]
154
+ left_right = @symbols[:arrow_left] + "/" + @symbols[:arrow_right]
155
+
156
+ arrows = [up_down]
157
+ arrows << "/" if paginated?
158
+ arrows << left_right if paginated?
159
+ arrows.join
160
+ end
129
161
 
130
- format(self.class::HELP, *tokens)
162
+ # Default help text
163
+ #
164
+ # Note that enumeration and filter are mutually exclusive
165
+ #
166
+ # @a public
167
+ def default_help
168
+ str = []
169
+ str << "(Press "
170
+ str << "#{arrows_help} arrow"
171
+ str << " or 1-#{choices.size} number" if enumerate?
172
+ str << " to move"
173
+ str << (filterable? ? "," : " and")
174
+ str << " Enter to select"
175
+ str << " and letters to filter" if filterable?
176
+ str << ")"
177
+ str.join
131
178
  end
132
179
 
133
180
  # Set selecting active index using number pad
@@ -137,10 +184,18 @@ module TTY
137
184
  @enum = value
138
185
  end
139
186
 
187
+ # Set whether selected answers are echoed
188
+ #
189
+ # @api public
190
+ def quiet(value)
191
+ @quiet = value
192
+ end
193
+
140
194
  # Add a single choice
141
195
  #
142
196
  # @api public
143
197
  def choice(*value, &block)
198
+ @filter_cache = {}
144
199
  if block
145
200
  @choices << (value << block)
146
201
  else
@@ -160,12 +215,13 @@ module TTY
160
215
  if !filterable? || @filter.empty?
161
216
  @choices
162
217
  else
163
- @choices.select do |_choice|
164
- !_choice.disabled? &&
165
- _choice.name.downcase.include?(@filter.join.downcase)
218
+ filter_value = @filter.join.downcase
219
+ @filter_cache[filter_value] ||= @choices.enabled.select do |choice|
220
+ choice.name.to_s.downcase.include?(filter_value)
166
221
  end
167
222
  end
168
223
  else
224
+ @filter_cache = {}
169
225
  values.each { |val| @choices << val }
170
226
  end
171
227
  end
@@ -195,6 +251,7 @@ module TTY
195
251
 
196
252
  def keynum(event)
197
253
  return unless enumerate?
254
+
198
255
  value = event.value.to_i
199
256
  return unless (1..choices.count).cover?(value)
200
257
  return if choices[value - 1].disabled?
@@ -218,11 +275,14 @@ module TTY
218
275
  if prev_active
219
276
  @active = prev_active
220
277
  elsif @cycle
221
- searchable = (choices.length).downto(1).to_a
278
+ searchable = choices.length.downto(1).to_a
222
279
  prev_active = search_choice_in(searchable)
223
280
 
224
281
  @active = prev_active if prev_active
225
282
  end
283
+
284
+ @paging_changed = @by_page
285
+ @by_page = false
226
286
  end
227
287
 
228
288
  def keydown(*)
@@ -237,9 +297,50 @@ module TTY
237
297
 
238
298
  @active = next_active if next_active
239
299
  end
300
+ @paging_changed = @by_page
301
+ @by_page = false
240
302
  end
241
303
  alias keytab keydown
242
304
 
305
+ # Moves all choices page by page keeping the current selected item
306
+ # at the same level on each page.
307
+ #
308
+ # When the choice on a page is outside of next page range then
309
+ # adjust it to the last item, otherwise leave unchanged.
310
+ def keyright(*)
311
+ if (@active + page_size) <= @choices.size
312
+ searchable = ((@active + page_size)..choices.length)
313
+ @active = search_choice_in(searchable)
314
+ elsif @active <= @choices.size # last page shorter
315
+ current = @active % page_size
316
+ remaining = @choices.size % page_size
317
+ if current.zero? || (remaining > 0 && current > remaining)
318
+ searchable = @choices.size.downto(0).to_a
319
+ @active = search_choice_in(searchable)
320
+ elsif @cycle
321
+ searchable = ((current.zero? ? page_size : current)..choices.length)
322
+ @active = search_choice_in(searchable)
323
+ end
324
+ end
325
+
326
+ @paging_changed = !@by_page
327
+ @by_page = true
328
+ end
329
+ alias keypage_down keyright
330
+
331
+ def keyleft(*)
332
+ if (@active - page_size) > 0
333
+ searchable = ((@active - page_size)..choices.length)
334
+ @active = search_choice_in(searchable)
335
+ elsif @cycle
336
+ searchable = @choices.size.downto(1).to_a
337
+ @active = search_choice_in(searchable)
338
+ end
339
+ @paging_changed = !@by_page
340
+ @by_page = true
341
+ end
342
+ alias keypage_up keyleft
343
+
243
344
  def keypress(event)
244
345
  return unless filterable?
245
346
 
@@ -277,7 +378,12 @@ module TTY
277
378
  # @api private
278
379
  def setup_defaults
279
380
  validate_defaults
280
- @active = @default.first
381
+
382
+ if !@default.empty?
383
+ @active = @default.first
384
+ else
385
+ @active = @choices.index { |choice| !choice.disabled? } + 1
386
+ end
281
387
  end
282
388
 
283
389
  # Validate default indexes to be within range
@@ -322,7 +428,7 @@ module TTY
322
428
 
323
429
  @prompt.print(refresh(question_lines_count(question_lines)))
324
430
  end
325
- @prompt.print(render_question)
431
+ @prompt.print(render_question) unless @quiet
326
432
  answer
327
433
  ensure
328
434
  @prompt.print(@prompt.show)
@@ -367,7 +473,6 @@ module TTY
367
473
  @first_render = false
368
474
  unless @done
369
475
  header << render_menu
370
- header << render_footer
371
476
  end
372
477
  header.join
373
478
  end
@@ -390,6 +495,20 @@ module TTY
390
495
  "(Filter: #{@filter.join.inspect})"
391
496
  end
392
497
 
498
+ # Check if help is shown only on start
499
+ #
500
+ # @api private
501
+ def help_start?
502
+ @show_help =~ /start/i
503
+ end
504
+
505
+ # Check if help is always displayed
506
+ #
507
+ # @api private
508
+ def help_always?
509
+ @show_help =~ /always/i
510
+ end
511
+
393
512
  # Render initial help and selected choice
394
513
  #
395
514
  # @return [String]
@@ -398,8 +517,9 @@ module TTY
398
517
  def render_header
399
518
  if @done
400
519
  selected_item = choices[@active - 1].name
401
- @prompt.decorate(selected_item, @active_color)
402
- elsif @first_render
520
+ @prompt.decorate(selected_item.to_s, @active_color)
521
+ elsif (@first_render && (help_start? || help_always?)) ||
522
+ (help_always? && !@filter.any?)
403
523
  @prompt.decorate(help, @help_color)
404
524
  elsif filterable? && @filter.any?
405
525
  @prompt.decorate(filter_help, @help_color)
@@ -414,35 +534,25 @@ module TTY
414
534
  def render_menu
415
535
  output = []
416
536
 
417
- @paginator.paginate(choices, @active, @per_page) do |choice, index|
418
- num = enumerate? ? (index + 1).to_s + @enum + ' ' : ''
537
+ sync_paginators if @paging_changed
538
+ paginator.paginate(choices, @active, @per_page) do |choice, index|
539
+ num = enumerate? ? (index + 1).to_s + @enum + " " : ""
419
540
  message = if index + 1 == @active && !choice.disabled?
420
- selected = @marker + ' ' + num + choice.name
541
+ selected = "#{@symbols[:marker]} #{num}#{choice.name}"
421
542
  @prompt.decorate(selected.to_s, @active_color)
422
543
  elsif choice.disabled?
423
- @prompt.decorate(symbols[:cross], :red) +
424
- ' ' + num + choice.name + ' ' + choice.disabled.to_s
544
+ @prompt.decorate(@symbols[:cross], :red) +
545
+ " #{num}#{choice.name} #{choice.disabled}"
425
546
  else
426
- ' ' * 2 + num + choice.name
547
+ " #{num}#{choice.name}"
427
548
  end
428
- max_index = paginated? ? @paginator.max_index : choices.size - 1
429
- newline = (index == max_index) ? '' : "\n"
549
+ end_index = paginated? ? paginator.end_index : choices.size - 1
550
+ newline = (index == end_index) ? "" : "\n"
430
551
  output << (message + newline)
431
552
  end
432
553
 
433
554
  output.join
434
555
  end
435
-
436
- # Render page info footer
437
- #
438
- # @return [String]
439
- #
440
- # @api private
441
- def render_footer
442
- return '' unless paginated?
443
- colored_footer = @prompt.decorate(@page_help, @help_color)
444
- "\n" + colored_footer
445
- end
446
556
  end # List
447
557
  end # Prompt
448
558
  end # TTY