clack 0.4.1 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0622f234b9f83906c6440e54c29a12715ecfc7a2b3777ec68efaf3882defcb7
4
- data.tar.gz: 450c6376c40ad88a27683294e97292f227e2081ccc90a0c076949bba3e5afd1d
3
+ metadata.gz: b5a8d3270e58cb1ad88664e73215c2ea5217211829680707d7616714ba93c926
4
+ data.tar.gz: c8f58966b18a015c26b7669ebd495c0e7ba75d830088ad3135a0971dd03bc874
5
5
  SHA512:
6
- metadata.gz: '08a7d471122e503168817430b65ad3618488eef6a2c79229a50e20a0a3ff3f2df741faef330a81b57875ddca3a37a68d0aebd19e770b7bbe73addd9a737bc9ec'
7
- data.tar.gz: 76e01f5eef142fd64069e1d9ba705edebc5694834ab68d5f7a8370eb345f1bf1473f2f1771d0178ed80e0b6c977c05d032d0059112bf36e72dd2fbe18f3850a3
6
+ metadata.gz: f3dbfd192f557ee0cd476be3ed80f54a57273e786d9120e3372c85527582ad8a30994b4202c83c309135383e6bedaaf275e756961912420961c09a24e53576e2
7
+ data.tar.gz: a692229800f14a3160aee3eca80f6c12b296c288e27dbf57b307b509090fdf0c72ca4e3712213f9515427ed0a48c70ee73534f85db887bb02ea1bfb9f0cf0177
data/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.4] - 2026-02-21
4
+
5
+ ### Fixed
6
+ - INT handler is now exception-safe (catches IOError/SystemCallError when output is unavailable)
7
+
8
+ ### Added
9
+ - SIGTERM handler restores cursor on graceful kill, matching INT handler behavior
10
+ - `Cursor.enabled?` now respects `FORCE_COLOR` environment variable, consistent with Colors module
11
+ - `Spinner#message` returns `self` for method chaining
12
+
13
+ ### Changed
14
+ - `Task` and `TaskResult` now use `Data.define` instead of `Struct.new` (Ruby 3.2+ idiom)
15
+
16
+ ## [0.4.3] - 2026-02-21
17
+
18
+ ### Fixed
19
+ - Multiselect variants now ignore `initial_values` that don't match any option (prevents phantom values in return)
20
+ - SIGWINCH handler now uses `.dup` for safe array iteration inside signal trap
21
+ - `GroupMultiselect` propagates `:hint` into flat items so hints actually render
22
+
23
+ ### Changed
24
+ - `AutocompleteMultiselect` final frame now shows selected labels instead of "N items selected", matching `Multiselect` behavior
25
+ - Removed dead `@mutex` from `Testing` module
26
+ - Removed unused `raw:` parameter from `TaskLog` and `TaskLogGroup` message methods
27
+
28
+ ## [0.4.2] - 2026-02-20
29
+
30
+ ### Fixed
31
+ - `GroupMultiselect` now preserves `:hint` on options (was silently dropped during normalization)
32
+ - `GroupMultiselect` renders hints on active options, matching `Select` behavior
33
+ - `Spinner` no longer raises when finished in timer mode before the animation thread starts
34
+ - YARD docs: corrected `selectable_groups` default from `true` to `false`
35
+
36
+ ### Changed
37
+ - `Range` prompt now accepts `initial_value:` for consistency with all other prompts (`default:` still works)
38
+ - README: corrected Range example to use `initial_value:` and tab completion description
39
+
3
40
  ## [0.4.1] - 2026-02-20
4
41
 
5
42
  ### Fixed
@@ -21,7 +58,7 @@
21
58
  ## [0.4.0] - 2026-02-19
22
59
 
23
60
  ### Added
24
- - `range` slider prompt for numeric selection (`Clack.range(message:, min:, max:, step:, default:)`)
61
+ - `range` slider prompt for numeric selection (`Clack.range(message:, min:, max:, step:, initial_value:)`)
25
62
  - Tab completion on `text` prompt via `completions:` parameter (array or proc)
26
63
  - Minimum terminal width warning (non-blocking, 40 columns)
27
64
 
data/README.md CHANGED
@@ -198,7 +198,7 @@ name = Clack.text(
198
198
  )
199
199
  ```
200
200
 
201
- **Tab completion** - press `Tab` to cycle through matching candidates:
201
+ **Tab completion** - press `Tab` to fill the longest common prefix of matching candidates:
202
202
 
203
203
  ```ruby
204
204
  # Tab completion from a static list
@@ -367,7 +367,7 @@ volume = Clack.range(
367
367
  min: 0,
368
368
  max: 100,
369
369
  step: 5,
370
- default: 50
370
+ initial_value: 50
371
371
  )
372
372
  ```
373
373
 
@@ -14,6 +14,7 @@ module Clack
14
14
  return @enabled unless @enabled.nil?
15
15
 
16
16
  # Default: check if output supports ANSI escape sequences
17
+ return true if ENV["FORCE_COLOR"] && ENV["FORCE_COLOR"] != "0"
17
18
  $stdout.tty? && ENV["TERM"] != "dumb" && !ENV["NO_COLOR"]
18
19
  end
19
20
 
@@ -32,9 +32,6 @@ module Clack
32
32
  MIN_TERMINAL_WIDTH = 40
33
33
 
34
34
  # Track active prompts for SIGWINCH notification.
35
- # Signal handler may fire during register/unregister. We can't use
36
- # .dup (allocates, forbidden in trap context) so we accept a benign
37
- # race: worst case, a prompt misses one resize notification.
38
35
  @active_prompts = []
39
36
 
40
37
  class << self
@@ -57,7 +54,7 @@ module Clack
57
54
  return unless Signal.list.key?("WINCH")
58
55
 
59
56
  Signal.trap("WINCH") do
60
- @active_prompts.each(&:request_redraw)
57
+ @active_prompts.dup.each(&:request_redraw)
61
58
  end
62
59
  end
63
60
  end
@@ -53,7 +53,8 @@ module Clack
53
53
  @cursor = 0
54
54
  @selected_index = 0
55
55
  @scroll_offset = 0
56
- @selected_values = Set.new(initial_values || [])
56
+ valid_values = Set.new(@all_options.map { |o| o[:value] })
57
+ @selected_values = Set.new(initial_values || []) & valid_values
57
58
  update_filtered
58
59
  end
59
60
 
@@ -167,11 +168,9 @@ module Clack
167
168
  lines << "#{bar}\n"
168
169
  lines << "#{symbol_for_state} #{@message}\n"
169
170
 
170
- display = if @state == :cancel
171
- Colors.strikethrough(Colors.dim("cancelled"))
172
- else
173
- Colors.dim("#{@selected_values.size} items selected")
174
- end
171
+ labels = @all_options.select { |o| @selected_values.include?(o[:value]) }.map { |o| o[:label] }
172
+ display_text = labels.join(", ")
173
+ display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display_text)) : Colors.dim(display_text)
175
174
  lines << "#{bar} #{display}\n"
176
175
 
177
176
  lines.join
@@ -55,7 +55,8 @@ module Clack
55
55
  super(message:, **opts)
56
56
  @groups = normalize_groups(options)
57
57
  @flat_items = build_flat_items
58
- @selected = Set.new(initial_values)
58
+ valid_values = Set.new(@flat_items.select { |item| item[:type] == :option }.map { |item| item[:value] })
59
+ @selected = Set.new(initial_values) & valid_values
59
60
  @required = required
60
61
  @selectable_groups = selectable_groups
61
62
  @group_spacing = group_spacing
@@ -152,9 +153,9 @@ module Clack
152
153
  def normalize_option(opt)
153
154
  case opt
154
155
  when Hash
155
- {value: opt[:value], label: opt[:label] || opt[:value].to_s, disabled: opt[:disabled] || false}
156
+ {value: opt[:value], label: opt[:label] || opt[:value].to_s, hint: opt[:hint], disabled: opt[:disabled] || false}
156
157
  else
157
- {value: opt, label: opt.to_s, disabled: false}
158
+ {value: opt, label: opt.to_s, hint: nil, disabled: false}
158
159
  end
159
160
  end
160
161
 
@@ -167,6 +168,7 @@ module Clack
167
168
  type: :option,
168
169
  value: opt[:value],
169
170
  label: opt[:label],
171
+ hint: opt[:hint],
170
172
  disabled: opt[:disabled],
171
173
  group: group,
172
174
  last_in_group: idx == group[:options].length - 1
@@ -261,13 +263,14 @@ module Clack
261
263
  else
262
264
  " "
263
265
  end
266
+ hint = (item[:hint] && active) ? " #{Colors.dim("(#{item[:hint]})")}" : ""
264
267
 
265
268
  if item[:disabled]
266
269
  "#{active_bar} #{Colors.dim(prefix)}#{Colors.dim(Symbols::S_CHECKBOX_INACTIVE)} #{Colors.strikethrough(Colors.dim(item[:label]))}\n"
267
270
  elsif active && selected
268
- "#{active_bar} #{Colors.dim(prefix)}#{Colors.green(Symbols::S_CHECKBOX_SELECTED)} #{item[:label]}\n"
271
+ "#{active_bar} #{Colors.dim(prefix)}#{Colors.green(Symbols::S_CHECKBOX_SELECTED)} #{item[:label]}#{hint}\n"
269
272
  elsif active
270
- "#{active_bar} #{Colors.dim(prefix)}#{Colors.cyan(Symbols::S_CHECKBOX_ACTIVE)} #{item[:label]}\n"
273
+ "#{active_bar} #{Colors.dim(prefix)}#{Colors.cyan(Symbols::S_CHECKBOX_ACTIVE)} #{item[:label]}#{hint}\n"
271
274
  elsif selected
272
275
  "#{active_bar} #{Colors.dim(prefix)}#{Colors.green(Symbols::S_CHECKBOX_SELECTED)} #{Colors.dim(item[:label])}\n"
273
276
  else
@@ -40,7 +40,8 @@ module Clack
40
40
  def initialize(message:, options:, initial_values: [], required: true, max_items: nil, cursor_at: nil, **opts)
41
41
  super(message:, **opts)
42
42
  @options = normalize_options(options)
43
- @selected = Set.new(initial_values)
43
+ valid_values = Set.new(@options.map { |o| o[:value] })
44
+ @selected = Set.new(initial_values) & valid_values
44
45
  @required = required
45
46
  @max_items = max_items
46
47
  @scroll_offset = 0
@@ -10,11 +10,11 @@ module Clack
10
10
  # @example Basic usage
11
11
  # level = Clack.range(message: "Volume", min: 0, max: 100, step: 5)
12
12
  #
13
- # @example With default value
13
+ # @example With initial value
14
14
  # workers = Clack.range(
15
15
  # message: "Concurrency",
16
16
  # min: 1, max: 16,
17
- # step: 1, default: 4
17
+ # step: 1, initial_value: 4
18
18
  # )
19
19
  #
20
20
  class Range < Core::Prompt
@@ -26,9 +26,10 @@ module Clack
26
26
  # @param min [Numeric] minimum value (default: 0)
27
27
  # @param max [Numeric] maximum value (default: 100)
28
28
  # @param step [Numeric] increment size (default: 1)
29
- # @param default [Numeric, nil] initial value (defaults to min)
29
+ # @param initial_value [Numeric, nil] initial value (defaults to min)
30
+ # @param default [Numeric, nil] deprecated alias for initial_value
30
31
  # @param opts [Hash] additional options passed to {Core::Prompt}
31
- def initialize(message:, min: 0, max: 100, step: 1, default: nil, **opts)
32
+ def initialize(message:, min: 0, max: 100, step: 1, initial_value: nil, default: nil, **opts)
32
33
  super(message:, **opts)
33
34
 
34
35
  raise ArgumentError, "min must be less than max" if min >= max
@@ -37,7 +38,7 @@ module Clack
37
38
  @min = min
38
39
  @max = max
39
40
  @step = step
40
- @value = clamp(default || min)
41
+ @value = clamp(initial_value || default || min)
41
42
  end
42
43
 
43
44
  protected
@@ -110,6 +110,7 @@ module Clack
110
110
  # @param msg [String] new message to display
111
111
  def message(msg)
112
112
  @mutex.synchronize { @message = remove_trailing_dots(msg) }
113
+ self
113
114
  end
114
115
 
115
116
  # Clear the spinner without showing a final message.
@@ -178,7 +179,7 @@ module Clack
178
179
  @finished = true
179
180
  @running = false
180
181
  thread_to_join = @thread
181
- suffix = (@indicator == :timer) ? " #{format_timer}" : ""
182
+ suffix = (@indicator == :timer && @start_time) ? " #{format_timer}" : ""
182
183
  [message || @message, suffix]
183
184
  end
184
185
 
@@ -51,7 +51,7 @@ module Clack
51
51
  # @return [Proc] the task to execute
52
52
  # @!attribute [r] enabled
53
53
  # @return [Boolean] whether the task should run (default: true)
54
- Task = Struct.new(:title, :task, :enabled, keyword_init: true)
54
+ Task = Data.define(:title, :task, :enabled)
55
55
 
56
56
  # Result of a completed task, including status and any error.
57
57
  #
@@ -61,7 +61,7 @@ module Clack
61
61
  # @return [Symbol] :success or :error
62
62
  # @!attribute [r] error
63
63
  # @return [String, nil] error message if failed
64
- TaskResult = Struct.new(:title, :status, :error, keyword_init: true)
64
+ TaskResult = Data.define(:title, :status, :error)
65
65
 
66
66
  # @param tasks [Array<Hash>] tasks with :title, :task, and optional :enabled keys
67
67
  # @param output [IO] output stream (default: $stdout)
@@ -32,8 +32,7 @@ module Clack
32
32
 
33
33
  # Add a message to the log
34
34
  # @param msg [String] Message to display
35
- # @param raw [Boolean] If true, don't add newline between messages
36
- def message(msg, raw: false)
35
+ def message(msg)
37
36
  clear_buffer
38
37
  @buffer << msg.to_s.gsub(/\e\[[\d;]*[ABCDEFGHfJKSTsu]/, "") # Strip cursor movement codes
39
38
  apply_limit
@@ -155,7 +154,7 @@ module Clack
155
154
  end
156
155
 
157
156
  # Add a message to this group
158
- def message(msg, raw: false)
157
+ def message(msg)
159
158
  @parent.add_group_message(self, msg)
160
159
  end
161
160
 
data/lib/clack/testing.rb CHANGED
@@ -105,8 +105,6 @@ module Clack
105
105
  end
106
106
  end
107
107
 
108
- @mutex = Mutex.new
109
-
110
108
  class << self
111
109
  # Simulate a prompt interaction by feeding a predefined key sequence.
112
110
  #
data/lib/clack/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Clack
4
4
  # Current gem version.
5
- VERSION = "0.4.1"
5
+ VERSION = "0.4.4"
6
6
  end
data/lib/clack.rb CHANGED
@@ -343,7 +343,7 @@ module Clack
343
343
  # @option opts [Array, nil] :initial_values initially selected values
344
344
  # @option opts [Boolean] :required require at least one selection (default: true)
345
345
  # @option opts [Object, nil] :cursor_at value of initially focused option
346
- # @option opts [Boolean] :selectable_groups allow toggling entire groups (default: true)
346
+ # @option opts [Boolean] :selectable_groups allow toggling entire groups (default: false)
347
347
  # @option opts [Integer] :group_spacing lines between groups (default: 0)
348
348
  # @return [Array, CANCEL] selected values or CANCEL if cancelled
349
349
  def group_multiselect(message:, options:, **opts)
@@ -375,7 +375,7 @@ module Clack
375
375
  # @option opts [Numeric] :min minimum value (default: 0)
376
376
  # @option opts [Numeric] :max maximum value (default: 100)
377
377
  # @option opts [Numeric] :step increment size (default: 1)
378
- # @option opts [Numeric, nil] :default initial value (defaults to min)
378
+ # @option opts [Numeric, nil] :initial_value initial value (defaults to min)
379
379
  # @option opts [Proc, nil] :validate validation proc
380
380
  # @option opts [String, nil] :help help text shown below the message
381
381
  # @return [Numeric, CANCEL] selected value or CANCEL if cancelled
@@ -515,13 +515,32 @@ end
515
515
 
516
516
  # Chain INT handler to restore cursor before passing to previous handler
517
517
  previous_int_handler = trap("INT") do
518
- print "\e[?25h"
518
+ begin
519
+ print "\e[?25h"
520
+ rescue IOError, SystemCallError
521
+ # Output unavailable — nothing we can do
522
+ end
519
523
  case previous_int_handler
520
- when Proc then previous_int_handler.call
524
+ when Proc
525
+ begin
526
+ previous_int_handler.call
527
+ rescue
528
+ exit(130)
529
+ end
521
530
  when "DEFAULT", "SYSTEM_DEFAULT" then exit(130)
522
531
  else exit(130)
523
532
  end
524
533
  end
525
534
 
535
+ # Handle SIGTERM similarly to INT — restore cursor on graceful kill
536
+ trap("TERM") do
537
+ begin
538
+ print "\e[?25h"
539
+ rescue IOError, SystemCallError
540
+ # Output unavailable
541
+ end
542
+ exit(143)
543
+ end
544
+
526
545
  # Set up SIGWINCH handler for terminal resize
527
546
  Clack::Core::Prompt.setup_signal_handler
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Whittaker