tty-prompt 0.18.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
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