clack 0.4.6 → 0.6.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.
data/lib/clack/box.rb CHANGED
@@ -39,10 +39,10 @@ module Clack
39
39
  format_border ||= ->(text) { Colors.gray(text) }
40
40
  symbols = corner_symbols(rounded).map(&format_border)
41
41
  lines = message.to_s.lines.map(&:chomp)
42
- box_width = calculate_width(lines, title.length, title_padding, content_padding, width)
42
+ box_width = calculate_width(lines, Clack::Utils.visible_length(title), title_padding, content_padding, width)
43
43
  inner_width = box_width - 2
44
44
  max_title_len = inner_width - (title_padding * 2)
45
- display_title = (title.length > max_title_len) ? "#{title[0, max_title_len - 3]}..." : title
45
+ display_title = Clack::Utils.truncate(title, max_title_len)
46
46
 
47
47
  {
48
48
  symbols: symbols,
@@ -56,7 +56,7 @@ module Clack
56
56
 
57
57
  def render_content_lines(output, ctx, content_align, content_padding)
58
58
  ctx[:lines].each do |line|
59
- left_pad, right_pad = padding_for_line(line.length, ctx[:inner_width], content_padding, content_align)
59
+ left_pad, right_pad = padding_for_line(Clack::Utils.visible_length(line), ctx[:inner_width], content_padding, content_align)
60
60
  output.puts "#{ctx[:v_symbol]}#{" " * left_pad}#{line}#{" " * right_pad}#{ctx[:v_symbol]}"
61
61
  end
62
62
  end
@@ -82,8 +82,8 @@ module Clack
82
82
  def calculate_width(lines, title_len, title_padding, content_padding, width)
83
83
  return width + 2 if width.is_a?(Integer) # Add 2 for borders
84
84
 
85
- # Auto width: fit to content
86
- max_line = lines.map(&:length).max || 0
85
+ # Auto width: fit to content using display width
86
+ max_line = lines.map { |l| Clack::Utils.visible_length(l) }.max || 0
87
87
  title_with_padding = title_len + (title_padding * 2)
88
88
  content_with_padding = max_line + (content_padding * 2)
89
89
 
@@ -94,7 +94,7 @@ module Clack
94
94
  if title.empty?
95
95
  "#{symbols[0]}#{h_symbol * inner_width}#{symbols[1]}"
96
96
  else
97
- left_pad, right_pad = padding_for_line(title.length, inner_width, title_padding, title_align)
97
+ left_pad, right_pad = padding_for_line(Clack::Utils.visible_length(title), inner_width, title_padding, title_align)
98
98
  "#{symbols[0]}#{h_symbol * left_pad}#{title}#{h_symbol * right_pad}#{symbols[1]}"
99
99
  end
100
100
  end
@@ -109,10 +109,10 @@ module Clack
109
109
 
110
110
  def best_score(opt, query, q_down)
111
111
  scores = [
112
- score(query, opt[:label], q_down: q_down),
113
- score(query, opt[:value].to_s, q_down: q_down)
112
+ score(query, opt.label, q_down: q_down),
113
+ score(query, opt.value.to_s, q_down: q_down)
114
114
  ]
115
- scores << score(query, opt[:hint], q_down: q_down) if opt[:hint]
115
+ scores << score(query, opt.hint, q_down: q_down) if opt.hint
116
116
  scores.max
117
117
  end
118
118
  end
@@ -16,34 +16,44 @@ module Clack
16
16
  SEQUENCE_TIMEOUT = 0.01
17
17
 
18
18
  class << self
19
- # Read a single keystroke from the terminal in raw mode.
20
- # Handles multi-byte escape sequences (arrow keys, etc.).
19
+ # Read a single keystroke in raw mode.
20
+ # When input is an IO backed by a console, uses raw mode.
21
+ # When input is a StringIO or test double, reads directly.
21
22
  #
23
+ # @param input [IO, nil] input stream (defaults to IO.console)
22
24
  # @return [String, nil] the key code, or nil on EOF
23
- # @raise [IOError] if no console is available
24
- def read
25
- console = IO.console
26
- raise IOError, "No console available (not a TTY?)" unless console
25
+ def read(input = nil)
26
+ io = input || IO.console
27
+ raise IOError, "No console available (not a TTY?)" unless io
27
28
 
28
- console.raw do |io|
29
- char = io.getc
30
- return char if char.nil? # EOF
31
- return char unless char == "\e"
29
+ # StringIO / test doubles don't support raw mode
30
+ return read_from(io) unless io.respond_to?(:raw)
32
31
 
33
- # Check for escape sequence - wait briefly for follow-up
34
- return char unless IO.select([io], nil, nil, ESCAPE_TIMEOUT)
35
-
36
- seq = io.getc.to_s
37
- return "\e#{seq}" unless seq == "["
38
-
39
- # Read CSI sequence until no more characters arrive
40
- seq += io.getc.to_s while IO.select([io], nil, nil, SEQUENCE_TIMEOUT)
41
- "\e[#{seq[1..]}"
42
- end
32
+ io.raw { |raw_io| read_from(raw_io) }
43
33
  rescue Errno::EIO, Errno::EBADF, IOError
44
34
  # Terminal disconnected or closed - treat as cancel
45
35
  "\u0003" # Ctrl+C
46
36
  end
37
+
38
+ private
39
+
40
+ def read_from(io)
41
+ char = io.getc
42
+ return char if char.nil? # EOF
43
+ return char unless char == "\e"
44
+
45
+ # Check for escape sequence - wait briefly for follow-up
46
+ return char unless io.respond_to?(:wait_readable) && io.wait_readable(ESCAPE_TIMEOUT)
47
+
48
+ seq = io.getc.to_s
49
+ return "\e#{seq}" unless seq == "["
50
+
51
+ # Read CSI sequence until no more characters arrive
52
+ while io.respond_to?(:wait_readable) && io.wait_readable(SEQUENCE_TIMEOUT)
53
+ seq += io.getc.to_s
54
+ end
55
+ "\e[#{seq[1..]}"
56
+ end
47
57
  end
48
58
  end
49
59
  end
@@ -2,8 +2,41 @@
2
2
 
3
3
  module Clack
4
4
  module Core
5
- # Shared functionality for option-based prompts (Select, Multiselect).
5
+ # Value object for normalized select-style options.
6
+ Option = Data.define(:value, :label, :hint, :disabled) do
7
+ def to_s = label.to_s
8
+
9
+ # Hash-style read access for backward compatibility with code (e.g.
10
+ # custom autocomplete filters) written against the old option hashes.
11
+ def [](key) = to_h[key]
12
+ end
13
+
14
+ # Value object for select_key options (includes :key).
15
+ SelectKeyOption = Data.define(:value, :label, :key, :hint) do
16
+ def to_s = label.to_s
17
+ def [](key) = to_h[key]
18
+ end
19
+
20
+ # Value objects for group_multiselect flat list (replaces :type discriminator hashes).
21
+ # GroupOption carries extra fields only used within GroupMultiselect for layout.
22
+ GroupHeader = Data.define(:label, :options) do
23
+ def [](key) = to_h[key]
24
+ end
25
+ GroupOption = Data.define(:value, :label, :hint, :disabled, :group, :last_in_group) do
26
+ def to_s = label.to_s
27
+ def [](key) = to_h[key]
28
+ end
29
+
30
+ # Shared functionality for option-based prompts (Select, Multiselect, Autocomplete, etc.).
6
31
  # Handles option normalization, cursor navigation, and scrolling.
32
+ #
33
+ # Including classes must define:
34
+ # - +@max_items+ [Integer, nil] maximum visible items (nil = show all)
35
+ # - +@option_index+ [Integer] current selection index
36
+ # - +@scroll_offset+ [Integer] current scroll position
37
+ #
38
+ # Including classes must implement:
39
+ # - +navigable_items+ [Array] returns the current list to navigate
7
40
  module OptionsHelper
8
41
  # Normalize options to a consistent hash format.
9
42
  # Accepts strings, symbols, or hashes with value/label/hint/disabled keys.
@@ -14,18 +47,23 @@ module Clack
14
47
  def normalize_options(options)
15
48
  raise ArgumentError, "options cannot be empty" if options.nil? || options.empty?
16
49
 
17
- options.map do |opt|
18
- case opt
19
- when Hash
20
- {
21
- value: opt[:value],
22
- label: opt[:label] || opt[:value].to_s,
23
- hint: opt[:hint],
24
- disabled: opt[:disabled] || false
25
- }
26
- else
27
- {value: opt, label: opt.to_s, hint: nil, disabled: false}
28
- end
50
+ options.map { |opt| OptionsHelper.normalize_option(opt) }
51
+ end
52
+
53
+ # Normalize a single option to an Option value object.
54
+ # @param opt [Hash, String, Symbol] Raw option
55
+ # @return [Option] Normalized option
56
+ def self.normalize_option(opt)
57
+ case opt
58
+ when Hash
59
+ Option.new(
60
+ value: opt[:value],
61
+ label: opt[:label] || opt[:value].to_s,
62
+ hint: opt[:hint],
63
+ disabled: opt[:disabled] || false
64
+ )
65
+ else
66
+ Option.new(value: opt, label: opt.to_s, hint: nil, disabled: false)
29
67
  end
30
68
  end
31
69
 
@@ -36,11 +74,12 @@ module Clack
36
74
  # @param delta [Integer] Direction (+1 for forward, -1 for backward)
37
75
  # @return [Integer] Index of next enabled option, or from if all disabled
38
76
  def find_next_enabled(from, delta)
39
- max = @options.length
77
+ items = navigable_items
78
+ max = items.length
40
79
  idx = (from + delta) % max
41
80
 
42
81
  max.times do
43
- return idx unless @options[idx][:disabled]
82
+ return idx unless items[idx].disabled
44
83
 
45
84
  idx = (idx + delta) % max
46
85
  end
@@ -48,11 +87,29 @@ module Clack
48
87
  from
49
88
  end
50
89
 
51
- # Move cursor in the given direction, skipping disabled options.
90
+ # Index of the first enabled option.
91
+ # @return [Integer]
92
+ def first_enabled_index
93
+ find_next_enabled(-1, 1)
94
+ end
95
+
96
+ # Move option_index in the given direction, skipping disabled options.
52
97
  #
53
98
  # @param delta [Integer] Direction (+1 for down/right, -1 for up/left)
54
99
  def move_cursor(delta)
55
- @cursor = find_next_enabled(@cursor, delta)
100
+ @option_index = find_next_enabled(@option_index, delta)
101
+ update_scroll
102
+ end
103
+
104
+ # Move the selection index by delta, wrapping around.
105
+ # Unlike move_cursor, does not skip disabled items.
106
+ #
107
+ # @param delta [Integer] direction (+1 for down, -1 for up)
108
+ def move_selection(delta)
109
+ items = navigable_items
110
+ return if items.empty?
111
+
112
+ @option_index = (@option_index + delta) % items.length
56
113
  update_scroll
57
114
  end
58
115
 
@@ -60,19 +117,21 @@ module Clack
60
117
  #
61
118
  # @return [Array<Hash>] Visible options
62
119
  def visible_options
63
- return @options unless @max_items && @options.length > @max_items
120
+ items = navigable_items
121
+ return items unless @max_items && items.length > @max_items
64
122
 
65
- @options[@scroll_offset, @max_items]
123
+ items[@scroll_offset, @max_items]
66
124
  end
67
125
 
68
- # Update scroll offset to keep cursor visible within the window.
126
+ # Update scroll offset to keep option_index visible within the window.
69
127
  def update_scroll
70
- return unless @max_items && @options.length > @max_items
128
+ items = navigable_items
129
+ return unless @max_items && items.length > @max_items
71
130
 
72
- if @cursor < @scroll_offset
73
- @scroll_offset = @cursor
74
- elsif @cursor >= @scroll_offset + @max_items
75
- @scroll_offset = @cursor - @max_items + 1
131
+ if @option_index < @scroll_offset
132
+ @scroll_offset = @option_index
133
+ elsif @option_index >= @scroll_offset + @max_items
134
+ @scroll_offset = @option_index - @max_items + 1
76
135
  end
77
136
  end
78
137
 
@@ -81,15 +140,23 @@ module Clack
81
140
  # @param initial_value [Object, nil] Initial value to select
82
141
  # @return [Integer] Cursor position
83
142
  def find_initial_cursor(initial_value)
84
- return 0 if @options.empty?
143
+ items = navigable_items
144
+ return 0 if items.empty?
85
145
 
86
146
  if initial_value.nil?
87
147
  # Start at first enabled option
88
- return @options[0][:disabled] ? find_next_enabled(-1, 1) : 0
148
+ return items[0].disabled ? first_enabled_index : 0
89
149
  end
90
150
 
91
- idx = @options.find_index { |o| o[:value] == initial_value }
92
- (idx && !@options[idx][:disabled]) ? idx : find_next_enabled(-1, 1)
151
+ idx = items.find_index { |o| o.value == initial_value }
152
+ (idx && !items[idx].disabled) ? idx : first_enabled_index
153
+ end
154
+
155
+ # The list of items to navigate. Override in subclasses that use
156
+ # a filtered or dynamic list (e.g., Autocomplete uses @filtered).
157
+ # @return [Array]
158
+ def navigable_items
159
+ @options
93
160
  end
94
161
  end
95
162
  end
@@ -33,9 +33,12 @@ module Clack
33
33
 
34
34
  # Track active prompts for SIGWINCH notification.
35
35
  @active_prompts = []
36
+ # Signal-safe flag set by SIGWINCH handler; checked in render loop.
37
+ @resize_pending = false
36
38
 
37
39
  class << self
38
40
  attr_reader :active_prompts
41
+ attr_accessor :resize_pending
39
42
 
40
43
  # Register a prompt instance for resize notifications
41
44
  def register(prompt)
@@ -47,14 +50,23 @@ module Clack
47
50
  @active_prompts.delete(prompt)
48
51
  end
49
52
 
53
+ # Notify all active prompts of a pending resize.
54
+ # Called from the render loop, not from the signal handler.
55
+ def flush_resize
56
+ return unless @resize_pending
57
+
58
+ @resize_pending = false
59
+ @active_prompts.each(&:request_redraw)
60
+ end
61
+
50
62
  # Set up SIGWINCH handler (called once on load).
51
- # Signal handlers must avoid mutex/complex operations.
63
+ # Signal handler only sets a flag -- no allocation or iteration.
52
64
  def setup_signal_handler
53
65
  return if Clack::Environment.windows?
54
66
  return unless Signal.list.key?("WINCH")
55
67
 
56
68
  Signal.trap("WINCH") do
57
- @active_prompts.dup.each(&:request_redraw)
69
+ @resize_pending = true
58
70
  end
59
71
  end
60
72
  end
@@ -87,7 +99,6 @@ module Clack
87
99
  @warning_message = nil
88
100
  @warning_confirmed = false
89
101
  @prev_frame = nil
90
- @cursor = 0
91
102
  @needs_redraw = false
92
103
  end
93
104
 
@@ -117,7 +128,8 @@ module Clack
117
128
  @state = :active
118
129
 
119
130
  loop do
120
- key = KeyReader.read
131
+ Prompt.flush_resize
132
+ key = KeyReader.read(@input)
121
133
  dispatch_key(key)
122
134
  render
123
135
 
@@ -175,14 +187,13 @@ module Clack
175
187
  # Process a keypress and update state accordingly.
176
188
  # Delegates to {#handle_input} for prompt-specific behavior.
177
189
  #
178
- # Override this in subclasses that need custom key dispatch (e.g., Select,
179
- # Confirm, Autocomplete). The warning state and error-clearing are handled
180
- # by {#dispatch_key} before this method is called, so overrides do not need
181
- # to manage those transitions.
190
+ # Extension points (simplest to most powerful):
191
+ # - Override {#handle_input} for navigation/printable input only
192
+ # - Override {#can_submit?} to guard enter (e.g., disabled options)
193
+ # - Override {#handle_key} for full custom key dispatch
182
194
  #
183
- # For prompts that only need custom handling of printable/navigation input
184
- # (not cancel/enter), override {#handle_input} instead. That is simpler and
185
- # preserves the default cancel/enter behavior from this method.
195
+ # The warning state and error-clearing are handled by {#dispatch_key}
196
+ # before this method is called.
186
197
  #
187
198
  # @param key [String] the key code from {KeyReader}
188
199
  def handle_key(key)
@@ -194,12 +205,17 @@ module Clack
194
205
  when :cancel
195
206
  @state = :cancel
196
207
  when :enter
197
- submit
208
+ submit if can_submit?
198
209
  else
199
210
  handle_input(key, action)
200
211
  end
201
212
  end
202
213
 
214
+ # Guard hook for submit. Override to prevent submission in certain states
215
+ # (e.g., when cursor is on a disabled option in Select).
216
+ # @return [Boolean] true if submit should proceed
217
+ def can_submit? = true
218
+
203
219
  # Handle prompt-specific input. Override in subclasses.
204
220
  #
205
221
  # This is the simplest extension point for prompts that only need to handle
@@ -348,6 +364,7 @@ module Clack
348
364
  submit
349
365
  if @state == :error
350
366
  $stderr.print "#{Colors.yellow("!")} #{Colors.yellow("CI mode: validation failed for")} \"#{@message}\": #{@error_message}\n"
367
+ return CANCEL
351
368
  end
352
369
  @value
353
370
  end
@@ -418,6 +435,22 @@ module Clack
418
435
  end
419
436
  end
420
437
 
438
+ # Common frame header: bar + symbol/message + help line.
439
+ # @return [String] header lines
440
+ def frame_header
441
+ "#{bar}\n#{symbol_for_state} #{@message}\n#{help_line}"
442
+ end
443
+
444
+ # Common frame footer: validation messages or bar_end.
445
+ # Replaces the repeated pattern of splicing validation lines.
446
+ # @return [String] footer lines
447
+ def frame_footer
448
+ vlns = validation_message_lines
449
+ return vlns.join if vlns.any?
450
+
451
+ "#{bar_end}\n"
452
+ end
453
+
421
454
  # Build validation message lines for error or warning states.
422
455
  # Returns array of lines to append, or empty array if no validation message.
423
456
  def validation_message_lines
@@ -2,52 +2,21 @@
2
2
 
3
3
  module Clack
4
4
  module Core
5
- # Shared scroll/navigation logic for filterable option lists.
6
- #
7
- # Used by prompts that have a dynamically filtered list (Autocomplete,
8
- # AutocompleteMultiselect, Path) where the user navigates a subset of
9
- # options that changes as they type.
10
- #
11
- # Including classes must define:
12
- # - +@max_items+ [Integer] maximum visible items
13
- # - +@selected_index+ [Integer] current selection index
14
- # - +@scroll_offset+ [Integer] current scroll position
15
- #
16
- # Including classes must implement:
17
- # - +scroll_items+ [Array] returns the current filterable list
5
+ # @deprecated Use {OptionsHelper} directly. ScrollHelper is now an alias
6
+ # for backwards compatibility.
18
7
  module ScrollHelper
19
- # Get the currently visible slice of items based on scroll position.
20
- #
21
- # @return [Array] visible items
22
- def visible_items
23
- items = scroll_items
24
- return items if items.length <= @max_items
25
-
26
- items[@scroll_offset, @max_items]
8
+ def self.included(base)
9
+ base.include(OptionsHelper) unless base.ancestors.include?(OptionsHelper)
27
10
  end
28
11
 
29
- # Move the selection index by delta, wrapping around.
30
- #
31
- # @param delta [Integer] direction (+1 for down, -1 for up)
32
- def move_selection(delta)
33
- items = scroll_items
34
- return if items.empty?
35
-
36
- @selected_index = (@selected_index + delta) % items.length
37
- update_selection_scroll
12
+ # Alias for visible_options (old name from when ScrollHelper was separate)
13
+ def visible_items
14
+ visible_options
38
15
  end
39
16
 
40
- private
41
-
42
- # Update scroll offset to keep the selected index visible.
43
- def update_selection_scroll
44
- return unless scroll_items.length > @max_items
45
-
46
- if @selected_index < @scroll_offset
47
- @scroll_offset = @selected_index
48
- elsif @selected_index >= @scroll_offset + @max_items
49
- @scroll_offset = @selected_index - @max_items + 1
50
- end
17
+ # Alias for navigable_items (old name from when ScrollHelper was separate)
18
+ def scroll_items
19
+ navigable_items
51
20
  end
52
21
  end
53
22
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clack
4
+ module Core
5
+ # Shared selection management for multi-select prompts.
6
+ # Handles toggle, toggle-all, required validation, and value tracking.
7
+ #
8
+ # Including classes must define:
9
+ # - +@selected+ [Set] the set of selected values
10
+ # - +@required+ [Boolean] whether at least one selection is required
11
+ module SelectionManager
12
+ REQUIRED_ERROR = "Please select at least one option. Press %s to select, %s to submit"
13
+
14
+ # Toggle a value in the selection set.
15
+ # @param value [Object] the value to toggle
16
+ def toggle_value(value)
17
+ if @selected.include?(value)
18
+ @selected.delete(value)
19
+ else
20
+ @selected.add(value)
21
+ end
22
+ end
23
+
24
+ # Validate that selection meets requirements before submit.
25
+ # Sets error state if required and empty.
26
+ # @return [Boolean] true if valid to proceed with submit
27
+ def validate_selection
28
+ if @required && @selected.empty?
29
+ @error_message = format(REQUIRED_ERROR, Colors.cyan("space"), Colors.cyan("enter"))
30
+ @state = :error
31
+ return false
32
+ end
33
+ true
34
+ end
35
+
36
+ # Update @value from the current selection.
37
+ def update_selection_value
38
+ @value = @selected.to_a
39
+ end
40
+
41
+ # Build the final display string from selected options.
42
+ # @param all_options [Array<Hash>] the full options list to match against
43
+ # @return [String] comma-separated labels
44
+ def selected_labels(all_options)
45
+ all_options.select { |o| @selected.include?(o.value) }.map { |o| o.label }.join(", ")
46
+ end
47
+ end
48
+ end
49
+ end
data/lib/clack/note.rb CHANGED
@@ -14,14 +14,15 @@ module Clack
14
14
  lines = message.to_s.lines.map(&:chomp)
15
15
  # Add empty lines at start and end like original
16
16
  lines = ["", *lines, ""]
17
- title_len = title&.length || 0
17
+ title_len = Clack::Utils.visible_length(title)
18
18
  width = calculate_width(lines, title_len)
19
19
 
20
20
  output.puts Colors.gray(Symbols::S_BAR)
21
21
  output.puts build_top_border(title, title_len, width)
22
22
 
23
23
  lines.each do |line|
24
- padded = line.ljust(width)
24
+ pad = width - Clack::Utils.visible_length(line)
25
+ padded = pad.positive? ? line + (" " * pad) : line
25
26
  output.puts "#{Colors.gray(Symbols::S_BAR)} #{Colors.dim(padded)}#{Colors.gray(Symbols::S_BAR)}"
26
27
  end
27
28
 
@@ -31,7 +32,7 @@ module Clack
31
32
  private
32
33
 
33
34
  def calculate_width(lines, title_len)
34
- max_line = lines.map(&:length).max || 0
35
+ max_line = lines.map { |line| Clack::Utils.visible_length(line) }.max || 0
35
36
  [max_line, title_len].max + 2
36
37
  end
37
38
 
@@ -29,13 +29,12 @@ module Clack
29
29
  # Clack.autocomplete(
30
30
  # message: "Select command",
31
31
  # options: commands,
32
- # filter: ->(opt, query) { opt[:label].start_with?(query) }
32
+ # filter: ->(opt, query) { opt.label.start_with?(query) }
33
33
  # )
34
34
  #
35
35
  class Autocomplete < Core::Prompt
36
36
  include Core::OptionsHelper
37
37
  include Core::TextInputHelper
38
- include Core::ScrollHelper
39
38
 
40
39
  # @param message [String] the prompt message
41
40
  # @param options [Array<Hash, String>] list of options to filter
@@ -51,9 +50,9 @@ module Clack
51
50
  @max_items = max_items
52
51
  @placeholder = placeholder
53
52
  @filter = filter
54
- @value = ""
53
+ @search_text = ""
55
54
  @cursor = 0
56
- @selected_index = 0
55
+ @option_index = 0
57
56
  @scroll_offset = 0
58
57
  update_filtered
59
58
  end
@@ -81,10 +80,17 @@ module Clack
81
80
  end
82
81
  end
83
82
 
83
+ # Use @search_text as text input backing store
84
+ def text_value = @search_text
85
+
86
+ def text_value=(val)
87
+ @search_text = val
88
+ end
89
+
84
90
  def handle_text_input(key)
85
91
  return unless super
86
92
 
87
- @selected_index = 0
93
+ @option_index = 0
88
94
  @scroll_offset = 0
89
95
  update_filtered
90
96
  end
@@ -96,7 +102,7 @@ module Clack
96
102
  return
97
103
  end
98
104
 
99
- @value = @filtered[@selected_index][:value]
105
+ @value = @filtered[@option_index].value
100
106
  submit
101
107
  end
102
108
 
@@ -107,9 +113,9 @@ module Clack
107
113
  lines << help_line
108
114
  lines << "#{active_bar} #{input_display}\n"
109
115
 
110
- visible_items.each_with_index do |opt, idx|
116
+ visible_options.each_with_index do |opt, idx|
111
117
  actual_idx = @scroll_offset + idx
112
- lines << "#{bar} #{option_display(opt, actual_idx == @selected_index)}\n"
118
+ lines << "#{bar} #{option_display(opt, actual_idx == @option_index)}\n"
113
119
  end
114
120
 
115
121
  if @state in :error | :warning
@@ -121,26 +127,26 @@ module Clack
121
127
  lines.join
122
128
  end
123
129
 
124
- def final_display = @filtered[@selected_index]&.[](:label) || @value
130
+ def final_display = @filtered[@option_index]&.label || @value
125
131
 
126
132
  private
127
133
 
128
134
  def update_filtered
129
135
  @filtered = if @filter
130
- @all_options.select { |opt| @filter.call(opt, @value) }
136
+ @all_options.select { |opt| @filter.call(opt, @search_text) }
131
137
  else
132
- Core::FuzzyMatcher.filter(@all_options, @value)
138
+ Core::FuzzyMatcher.filter(@all_options, @search_text)
133
139
  end
134
140
  end
135
141
 
136
- def scroll_items = @filtered
142
+ def navigable_items = @filtered
137
143
 
138
144
  def option_display(opt, active)
139
- hint = (opt[:hint] && active) ? Colors.dim(" (#{opt[:hint]})") : ""
145
+ hint = (opt.hint && active) ? Colors.dim(" (#{opt.hint})") : ""
140
146
  if active
141
- "#{Colors.green(Symbols::S_RADIO_ACTIVE)} #{opt[:label]}#{hint}"
147
+ "#{Colors.green(Symbols::S_RADIO_ACTIVE)} #{opt.label}#{hint}"
142
148
  else
143
- "#{Colors.dim(Symbols::S_RADIO_INACTIVE)} #{Colors.dim(opt[:label])}"
149
+ "#{Colors.dim(Symbols::S_RADIO_INACTIVE)} #{Colors.dim(opt.label)}"
144
150
  end
145
151
  end
146
152
  end