clack 0.4.5 → 0.4.6

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: efda51d5b5f8c7aac33181bc36f72761117fc3b3e6c5707d8197d55d2fde99c0
4
- data.tar.gz: 1e324cda3ac98ccefd939e213827e89f36e4cdd22aa237186412ce82bb43d915
3
+ metadata.gz: e27f6e6ab9835d2855570ebdcd9b74a581becaffad5ba6a814e54ec5d44097b2
4
+ data.tar.gz: 8788e05802f917a1a6abe06697a4ca47ff451da0235d7336b560adf8558451a0
5
5
  SHA512:
6
- metadata.gz: 96263cb6a717237125ba7e48cc6bdce5166a49b7ed65d7755c86a0ca3ff0db5e7a4616cc4e84b88577dd67cbda189a02dc7f8dc6f2b2b998c72b7c2475c5ca65
7
- data.tar.gz: 5a3179bd6b5d393bbc4dc871584101a929b21fc1ee081b109845905a716e19a54ed2dbd39f6a13b5d3acdfa20c13a8f73d1cb9fd05991d16b4dbcbc74d48a3f5
6
+ metadata.gz: 173ead18da184ea230092d263bb4e607eb623c788fcdfae7d4315afbebe140bbe5335e42fa7c7ee460481916e46a6fda464f9b13f8d011228648dcf13a799704
7
+ data.tar.gz: 56fd6c8cbca6c610cb73941f565c84e62c6bcc38d5b48741c35c52b86c14e45ff858219e5973669d1db7beb4bb045259d11daa60d172d3e2603ad16c04e2e812
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.6] - 2026-03-22
4
+
5
+ ### Fixed
6
+ - `Settings.printable?` now accepts combining characters and multi-codepoint grapheme clusters (accented letters, emoji)
7
+ - Signal handlers use `write_nonblock` instead of `print` for async safety
8
+ - `S_STEP_ERROR` ASCII fallback changed from `x` to `!` (was identical to `S_STEP_CANCEL`)
9
+ - CI mode validation warnings now print to stderr instead of stdout
10
+
11
+ ### Changed
12
+ - Colors, Cursor, and Symbols ANSI detection unified through `Environment.colors_supported?`
13
+ - Extracted `build_final_frame` template into `Core::Prompt` with `final_display` hook (11 prompt classes simplified)
14
+ - Date prompt: consolidated duplicated segment logic into `update_segment`
15
+
3
16
  ## [0.4.5] - 2026-02-22
4
17
 
5
18
  ### Fixed
data/lib/clack/colors.rb CHANGED
@@ -8,12 +8,7 @@ module Clack
8
8
  # - FORCE_COLOR environment variable forces colors on
9
9
  module Colors
10
10
  class << self
11
- def enabled?
12
- return true if ENV["FORCE_COLOR"] && ENV["FORCE_COLOR"] != "0"
13
- return false if ENV["NO_COLOR"]
14
-
15
- $stdout.tty?
16
- end
11
+ def enabled? = Environment.colors_supported?
17
12
 
18
13
  # @!group Foreground Colors (standard)
19
14
 
@@ -13,9 +13,7 @@ module Clack
13
13
  def enabled?
14
14
  return @enabled unless @enabled.nil?
15
15
 
16
- # Default: check if output supports ANSI escape sequences
17
- return true if ENV["FORCE_COLOR"] && ENV["FORCE_COLOR"] != "0"
18
- $stdout.tty? && ENV["TERM"] != "dumb" && !ENV["NO_COLOR"]
16
+ Environment.colors_supported?
19
17
  end
20
18
 
21
19
  # Visibility
@@ -316,13 +316,23 @@ module Clack
316
316
  end
317
317
 
318
318
  # Build the final frame shown after interaction ends.
319
- # Override to show a different view for completed prompts.
319
+ # Default renders a one-line summary: bar, symbol+message, styled final value.
320
+ # Override {#final_display} to customize what value is shown.
321
+ # Override this entirely for multi-line final output (e.g. MultilineText).
320
322
  #
321
323
  # @return [String] the final frame content
322
324
  def build_final_frame
323
- build_frame
325
+ "#{bar}\n" \
326
+ "#{symbol_for_state} #{@message}\n" \
327
+ "#{bar} #{styled_final_display}\n"
324
328
  end
325
329
 
330
+ # The text to display in the final frame after submit/cancel.
331
+ # Override in subclasses to customize (e.g. masked password, formatted date).
332
+ #
333
+ # @return [String]
334
+ def final_display = @value.to_s
335
+
326
336
  # Check if prompt has reached a terminal state.
327
337
  #
328
338
  # @return [Boolean] true if state is :submit or :cancel
@@ -337,13 +347,18 @@ module Clack
337
347
  def run_ci_mode
338
348
  submit
339
349
  if @state == :error
340
- @output.print "#{Colors.yellow("!")} #{Colors.yellow("CI mode: validation failed for")} \"#{@message}\": #{@error_message}\n"
350
+ $stderr.print "#{Colors.yellow("!")} #{Colors.yellow("CI mode: validation failed for")} \"#{@message}\": #{@error_message}\n"
341
351
  end
342
352
  @value
343
353
  end
344
354
 
345
355
  private
346
356
 
357
+ def styled_final_display
358
+ text = final_display
359
+ (@state == :cancel) ? Colors.strikethrough(Colors.dim(text)) : Colors.dim(text)
360
+ end
361
+
347
362
  def warn_narrow_terminal
348
363
  return unless Environment.tty?(@output)
349
364
 
@@ -91,9 +91,9 @@ module Clack
91
91
  aliases[key] if ACTIONS.include?(aliases[key])
92
92
  end
93
93
 
94
- # Check if a key is a printable character
94
+ # Check if a key is a printable character (handles combining marks and multi-codepoint grapheme clusters)
95
95
  def printable?(key)
96
- key && key.length == 1 && key.ord >= PRINTABLE_CHAR_MIN
96
+ key && key.grapheme_clusters.length == 1 && key.ord >= PRINTABLE_CHAR_MIN
97
97
  end
98
98
 
99
99
  # Check if a key is a backspace/delete
@@ -121,17 +121,7 @@ module Clack
121
121
  lines.join
122
122
  end
123
123
 
124
- def build_final_frame
125
- lines = []
126
- lines << "#{bar}\n"
127
- lines << "#{symbol_for_state} #{@message}\n"
128
-
129
- display_value = @filtered[@selected_index]&.[](:label) || @value
130
- display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display_value)) : Colors.dim(display_value)
131
- lines << "#{bar} #{display}\n"
132
-
133
- lines.join
134
- end
124
+ def final_display = @filtered[@selected_index]&.[](:label) || @value
135
125
 
136
126
  private
137
127
 
@@ -130,17 +130,8 @@ module Clack
130
130
  lines.join
131
131
  end
132
132
 
133
- def build_final_frame
134
- lines = []
135
- lines << "#{bar}\n"
136
- lines << "#{symbol_for_state} #{@message}\n"
137
-
138
- labels = @all_options.select { |o| @selected.include?(o[:value]) }.map { |o| o[:label] }
139
- display_text = labels.join(", ")
140
- display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display_text)) : Colors.dim(display_text)
141
- lines << "#{bar} #{display}\n"
142
-
143
- lines.join
133
+ def final_display
134
+ @all_options.select { |o| @selected.include?(o[:value]) }.map { |o| o[:label] }.join(", ")
144
135
  end
145
136
 
146
137
  private
@@ -71,17 +71,7 @@ module Clack
71
71
  lines.join
72
72
  end
73
73
 
74
- def build_final_frame
75
- lines = []
76
- lines << "#{bar}\n"
77
- lines << "#{symbol_for_state} #{@message}\n"
78
-
79
- selected = @value ? @active_label : @inactive_label
80
- display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(selected)) : Colors.dim(selected)
81
- lines << "#{bar} #{display}\n"
82
-
83
- lines.join
84
- end
74
+ def final_display = @value ? @active_label : @inactive_label
85
75
 
86
76
  private
87
77
 
@@ -99,17 +99,7 @@ module Clack
99
99
  lines.join
100
100
  end
101
101
 
102
- def build_final_frame
103
- lines = []
104
- lines << "#{bar}\n"
105
- lines << "#{symbol_for_state} #{@message}\n"
106
-
107
- display_text = formatted_date
108
- display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display_text)) : Colors.dim(display_text)
109
- lines << "#{bar} #{display}\n"
110
-
111
- lines.join
112
- end
102
+ def final_display = formatted_date
113
103
 
114
104
  private
115
105
 
@@ -164,28 +154,7 @@ module Clack
164
154
  def adjust_segment(delta)
165
155
  commit_input_buffer
166
156
  @input_buffer = ""
167
-
168
- case current_segment_type
169
- when :year
170
- @year = (@year + delta).clamp(1, 9999)
171
- clamp_day_to_month
172
- when :month
173
- @month += delta
174
- @month = wrap_value(@month, 1, 12)
175
- clamp_day_to_month
176
- when :day
177
- max_day = days_in_month(@year, @month)
178
- @day = wrap_value(@day + delta, 1, max_day)
179
- end
180
-
181
- enforce_bounds
182
- end
183
-
184
- def wrap_value(val, min, max)
185
- return min if val > max
186
- return max if val < min
187
-
188
- val
157
+ update_segment(segment_value + delta, wrap: true)
189
158
  end
190
159
 
191
160
  def handle_digit(digit)
@@ -201,23 +170,38 @@ module Clack
201
170
  def commit_input_buffer
202
171
  return if @input_buffer.empty?
203
172
 
204
- value = @input_buffer.to_i
173
+ update_segment(@input_buffer.to_i)
205
174
  @input_buffer = ""
175
+ end
206
176
 
177
+ def segment_value
207
178
  case current_segment_type
208
- when :year
209
- @year = value.clamp(1, 9999)
210
- clamp_day_to_month
211
- when :month
212
- @month = value.clamp(1, 12)
213
- clamp_day_to_month
214
- when :day
215
- @day = value.clamp(1, days_in_month(@year, @month))
179
+ when :year then @year
180
+ when :month then @month
181
+ when :day then @day
216
182
  end
183
+ end
184
+
185
+ def update_segment(value, wrap: false)
186
+ constrain = wrap ? method(:wrap_value) : :clamp.to_proc
217
187
 
188
+ case current_segment_type
189
+ when :year then @year = value.clamp(1, 9999)
190
+ when :month then @month = constrain.call(value, 1, 12)
191
+ when :day then @day = constrain.call(value, 1, days_in_month(@year, @month))
192
+ end
193
+
194
+ clamp_day_to_month
218
195
  enforce_bounds
219
196
  end
220
197
 
198
+ def wrap_value(val, min, max)
199
+ return min if val > max
200
+ return max if val < min
201
+
202
+ val
203
+ end
204
+
221
205
  def current_segment_type = FORMATS[@format][:order][@segment]
222
206
 
223
207
  def days_in_month(year, month)
@@ -126,18 +126,7 @@ module Clack
126
126
  lines.join
127
127
  end
128
128
 
129
- def build_final_frame
130
- lines = []
131
- lines << "#{bar}\n"
132
- lines << "#{symbol_for_state} #{@message}\n"
133
-
134
- labels = selected_options.map { |o| o[:label] }
135
- display_text = labels.join(", ")
136
- display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display_text)) : Colors.dim(display_text)
137
- lines << "#{bar} #{display}\n"
138
-
139
- lines.join
140
- end
129
+ def final_display = selected_options.map { |o| o[:label] }.join(", ")
141
130
 
142
131
  private
143
132
 
@@ -103,17 +103,8 @@ module Clack
103
103
  lines.join
104
104
  end
105
105
 
106
- def build_final_frame
107
- lines = []
108
- lines << "#{bar}\n"
109
- lines << "#{symbol_for_state} #{@message}\n"
110
-
111
- labels = @options.select { |o| @selected.include?(o[:value]) }.map { |o| o[:label] }
112
- display_text = labels.join(", ")
113
- display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display_text)) : Colors.dim(display_text)
114
- lines << "#{bar} #{display}\n"
115
-
116
- lines.join
106
+ def final_display
107
+ @options.select { |o| @selected.include?(o[:value]) }.map { |o| o[:label] }.join(", ")
117
108
  end
118
109
 
119
110
  private
@@ -51,17 +51,7 @@ module Clack
51
51
  lines.join
52
52
  end
53
53
 
54
- def build_final_frame
55
- lines = []
56
- lines << "#{bar}\n"
57
- lines << "#{symbol_for_state} #{@message}\n"
58
-
59
- masked = @mask * @value.grapheme_clusters.length
60
- display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(masked)) : Colors.dim(masked)
61
- lines << "#{bar} #{display}\n"
62
-
63
- lines.join
64
- end
54
+ def final_display = @mask * @value.grapheme_clusters.length
65
55
 
66
56
  private
67
57
 
@@ -124,17 +124,6 @@ module Clack
124
124
  lines.join
125
125
  end
126
126
 
127
- def build_final_frame
128
- lines = []
129
- lines << "#{bar}\n"
130
- lines << "#{symbol_for_state} #{@message}\n"
131
-
132
- display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(@value)) : Colors.dim(@value)
133
- lines << "#{bar} #{display}\n"
134
-
135
- lines.join
136
- end
137
-
138
127
  private
139
128
 
140
129
  def update_suggestions
@@ -67,17 +67,7 @@ module Clack
67
67
  lines.join
68
68
  end
69
69
 
70
- def build_final_frame
71
- lines = []
72
- lines << "#{bar}\n"
73
- lines << "#{symbol_for_state} #{@message}\n"
74
-
75
- display = format_value(@value)
76
- styled = (@state == :cancel) ? Colors.strikethrough(Colors.dim(display)) : Colors.dim(display)
77
- lines << "#{bar} #{styled}\n"
78
-
79
- lines.join
80
- end
70
+ def final_display = format_value(@value)
81
71
 
82
72
  private
83
73
 
@@ -80,17 +80,7 @@ module Clack
80
80
  lines.join
81
81
  end
82
82
 
83
- def build_final_frame
84
- lines = []
85
- lines << "#{bar}\n"
86
- lines << "#{symbol_for_state} #{@message}\n"
87
-
88
- label = current_option[:label]
89
- display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(label)) : Colors.dim(label)
90
- lines << "#{bar} #{display}\n"
91
-
92
- lines.join
93
- end
83
+ def final_display = current_option[:label]
94
84
 
95
85
  private
96
86
 
@@ -63,17 +63,7 @@ module Clack
63
63
  lines.join
64
64
  end
65
65
 
66
- def build_final_frame
67
- lines = []
68
- lines << "#{bar}\n"
69
- lines << "#{symbol_for_state} #{@message}\n"
70
-
71
- label = @options.find { |o| o[:value] == @value }&.dig(:label).to_s
72
- display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(label)) : Colors.dim(label)
73
- lines << "#{bar} #{display}\n"
74
-
75
- lines.join
76
- end
66
+ def final_display = @options.find { |o| o[:value] == @value }&.dig(:label).to_s
77
67
 
78
68
  private
79
69
 
@@ -104,17 +104,6 @@ module Clack
104
104
  lines.join
105
105
  end
106
106
 
107
- def build_final_frame
108
- lines = []
109
- lines << "#{bar}\n"
110
- lines << "#{symbol_for_state} #{@message}\n"
111
-
112
- display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(@value)) : Colors.dim(@value)
113
- lines << "#{bar} #{display}\n"
114
-
115
- lines.join
116
- end
117
-
118
107
  private
119
108
 
120
109
  # Complete the current input using the longest common prefix of matching candidates.
data/lib/clack/symbols.rb CHANGED
@@ -26,8 +26,7 @@ module Clack
26
26
  # Explicit override
27
27
  return ENV["CLACK_UNICODE"] == "1" if ENV["CLACK_UNICODE"]
28
28
 
29
- # Default: TTY and not dumb terminal
30
- $stdout.tty? && ENV["TERM"] != "dumb" && !ENV["NO_COLOR"]
29
+ Environment.colors_supported?
31
30
  end
32
31
  end
33
32
 
@@ -36,7 +35,7 @@ module Clack
36
35
  # Unicode cancel step indicator, or ASCII fallback.
37
36
  S_STEP_CANCEL = unicode? ? "■" : "x"
38
37
  # Unicode error step indicator, or ASCII fallback.
39
- S_STEP_ERROR = unicode? ? "▲" : "x"
38
+ S_STEP_ERROR = unicode? ? "▲" : "!"
40
39
  # Unicode submit step indicator, or ASCII fallback.
41
40
  S_STEP_SUBMIT = unicode? ? "◇" : "o"
42
41
 
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.5"
5
+ VERSION = "0.4.6"
6
6
  end
data/lib/clack.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "clack/version"
4
+ require_relative "clack/environment"
4
5
  require_relative "clack/symbols"
5
6
  require_relative "clack/colors"
6
- require_relative "clack/environment"
7
7
  require_relative "clack/utils"
8
8
  require_relative "clack/core/cursor"
9
9
  require_relative "clack/core/settings"
@@ -508,15 +508,20 @@ module Clack
508
508
  end
509
509
  end
510
510
 
511
- # Terminal cleanup on exit - show cursor if it was hidden
511
+ # Terminal cleanup on exit show cursor if it was hidden.
512
+ # Uses raw write(2) for async-signal safety in trap handlers.
513
+ CURSOR_SHOW = "\e[?25h"
514
+
512
515
  at_exit do
513
- print "\e[?25h"
516
+ $stdout.print Clack::Core::Cursor.show
517
+ rescue IOError, SystemCallError
518
+ # Output unavailable
514
519
  end
515
520
 
516
- # Chain INT handler to restore cursor before passing to previous handler
521
+ # Chain INT handler to restore cursor before passing to previous handler.
517
522
  previous_int_handler = trap("INT") do
518
523
  begin
519
- print "\e[?25h"
524
+ $stdout.write_nonblock(CURSOR_SHOW)
520
525
  rescue IOError, SystemCallError
521
526
  # Output unavailable — nothing we can do
522
527
  end
@@ -532,10 +537,10 @@ previous_int_handler = trap("INT") do
532
537
  end
533
538
  end
534
539
 
535
- # Handle SIGTERM similarly to INT — restore cursor on graceful kill
540
+ # Handle SIGTERM similarly to INT — restore cursor on graceful kill.
536
541
  trap("TERM") do
537
542
  begin
538
- print "\e[?25h"
543
+ $stdout.write_nonblock(CURSOR_SHOW)
539
544
  rescue IOError, SystemCallError
540
545
  # Output unavailable
541
546
  end
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.5
4
+ version: 0.4.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Whittaker
@@ -103,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
103
  - !ruby/object:Gem::Version
104
104
  version: '0'
105
105
  requirements: []
106
- rubygems_version: 3.6.9
106
+ rubygems_version: 4.0.8
107
107
  specification_version: 4
108
108
  summary: Beautiful, minimal CLI prompts
109
109
  test_files: []