clack 0.1.0 → 0.1.1

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: 82be7312cfdade424ca896c9ddb5c70e2bfadadc5f61c411a073d01d1daa73c5
4
- data.tar.gz: 2322b602de289654d8ba79925b804027b49a2027eb1e76fe8708d315527706a6
3
+ metadata.gz: a9749ca99273a0ed19da8274041da1b04f7355f523761169b509e3234737be7b
4
+ data.tar.gz: 8a097667af010ba054486e6a22407fefdb021752a9ce018ac2a0489f060f4140
5
5
  SHA512:
6
- metadata.gz: 2412c28b3f625313df7249154a2a7a72cd039e336f3ee2764130e7dc52a5f932358a1576bc793d8fa33b5862d60cf5dff319a5b6495546bd59cef24714c93b1f
7
- data.tar.gz: dc26d51d23d264c510c913383e448b3b8552312d1a1a78db7cba71e69ace120cc8638f69adc615fb844652065520947286a46cf75657a6a8ecdbcb499ee58bfc
6
+ metadata.gz: 6aa1f190f08a4fc285e682f6838518951ddc25647eaf034e06bdf7d1b1d06b15b0cc7cb2de3bd4c5ead11adcaea00b7cd8e2c6e8d03f28287b084f187766dd28
7
+ data.tar.gz: f6b11ea693a6ba6440a907c01b25a33ade736d074eaed61095998b873385466ee1c086ad2bb78546287e2e038a56a4cab23f509c678e3c9ecb752bca6c540907
data/CHANGELOG.md CHANGED
@@ -2,16 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ### Added
6
- - Full prompt library: text, password, confirm, select, multiselect, autocomplete, path, select_key, spinner, progress, tasks, group_multiselect
7
- - `Clack.group` for chaining prompts with shared results
8
- - `Clack.stream` for streaming output from commands and iterables
9
- - Vim-style navigation (`hjkl`) alongside arrow keys
10
- - Zero runtime dependencies
5
+ ## [0.1.1]
6
+
7
+ ### Fixed
8
+ - Path prompt now correctly rejects paths outside root (fixed boundary check bug)
9
+ - Password backspace properly removes Unicode grapheme clusters, not just bytes
10
+ - Terminal cleanup handles closed output streams gracefully
11
+ - SIGWINCH handler uses explicit signal check instead of rescue
11
12
 
12
- ### Changed
13
- - Ruby 3.2+ support
13
+ ### Added
14
+ - Terminal resize support via SIGWINCH signal handling
14
15
 
15
16
  ## [0.1.0]
16
17
 
17
- Initial release.
18
+ Initial release with full prompt library.
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Steve Whittaker
3
+ Copyright (c) 2026 Steve Whittaker
4
4
 
5
5
  This project is a Ruby port of Clack (https://github.com/bombshell-dev/clack)
6
6
  Originally created by Nate Moore and contributors, licensed under MIT.
data/README.md CHANGED
@@ -10,7 +10,6 @@ A faithful Ruby port of [@clack/prompts](https://github.com/bombshell-dev/clack)
10
10
 
11
11
  ## Why Clack?
12
12
 
13
- - **Zero dependencies** - Pure Ruby, stdlib only
14
13
  - **Beautiful by default** - Thoughtfully designed prompts that just look right
15
14
  - **Vim-friendly** - Navigate with `hjkl` or arrow keys
16
15
  - **Accessible** - Graceful ASCII fallbacks for limited terminals
@@ -27,6 +27,36 @@ module Clack
27
27
  # end
28
28
  #
29
29
  class Prompt
30
+ # Track active prompts for SIGWINCH notification.
31
+ # Signal handler may fire during register/unregister. We can't use
32
+ # .dup (allocates, forbidden in trap context) so we accept a benign
33
+ # race: worst case, a prompt misses one resize notification.
34
+ @active_prompts = []
35
+
36
+ class << self
37
+ attr_reader :active_prompts
38
+
39
+ # Register a prompt instance for resize notifications
40
+ def register(prompt)
41
+ @active_prompts << prompt
42
+ end
43
+
44
+ def unregister(prompt)
45
+ @active_prompts.delete(prompt)
46
+ end
47
+
48
+ # Set up SIGWINCH handler (called once on load).
49
+ # Signal handlers must avoid mutex/complex operations.
50
+ def setup_signal_handler
51
+ return if Clack::Environment.windows?
52
+ return unless Signal.list.key?("WINCH")
53
+
54
+ Signal.trap("WINCH") do
55
+ @active_prompts.each(&:request_redraw)
56
+ end
57
+ end
58
+ end
59
+
30
60
  # @return [Symbol] current state (:initial, :active, :error, :submit, :cancel)
31
61
  attr_reader :state
32
62
  # @return [Object] the current/final value
@@ -48,6 +78,13 @@ module Clack
48
78
  @error_message = nil
49
79
  @prev_frame = nil
50
80
  @cursor = 0
81
+ @needs_redraw = false
82
+ end
83
+
84
+ # Request a full redraw on next render cycle.
85
+ # Called by SIGWINCH handler when terminal is resized.
86
+ def request_redraw
87
+ @needs_redraw = true
51
88
  end
52
89
 
53
90
  # Run the prompt interaction loop.
@@ -57,6 +94,7 @@ module Clack
57
94
  #
58
95
  # @return [Object, Clack::CANCEL] the submitted value or CANCEL sentinel
59
96
  def run
97
+ Prompt.register(self)
60
98
  setup_terminal
61
99
  render
62
100
  @state = :active
@@ -72,6 +110,7 @@ module Clack
72
110
  finalize
73
111
  (terminal_state? && @state == :cancel) ? CANCEL : @value
74
112
  ensure
113
+ Prompt.unregister(self)
75
114
  cleanup_terminal
76
115
  end
77
116
 
@@ -121,9 +160,16 @@ module Clack
121
160
  end
122
161
 
123
162
  # Render the current frame using differential rendering.
124
- # Only redraws if the frame content has changed.
163
+ # Only redraws if the frame content has changed or redraw was requested.
125
164
  def render
126
165
  frame = build_frame
166
+
167
+ # Force redraw if terminal was resized
168
+ if @needs_redraw
169
+ @needs_redraw = false
170
+ @prev_frame = nil
171
+ end
172
+
127
173
  return if frame == @prev_frame
128
174
 
129
175
  if @state == :initial
@@ -176,6 +222,8 @@ module Clack
176
222
 
177
223
  def cleanup_terminal
178
224
  @output.print Cursor.show
225
+ rescue IOError, SystemCallError
226
+ # Output unavailable - terminal may need manual reset
179
227
  end
180
228
 
181
229
  def restore_cursor
@@ -30,7 +30,8 @@ module Clack
30
30
  return unless Core::Settings.printable?(key)
31
31
 
32
32
  if Core::Settings.backspace?(key)
33
- @value = @value.chop
33
+ clusters = @value.grapheme_clusters
34
+ @value = (clusters.length > 0) ? clusters[0..-2].join : ""
34
35
  else
35
36
  @value += key
36
37
  end
@@ -53,7 +54,7 @@ module Clack
53
54
  lines << "#{bar}\n"
54
55
  lines << "#{symbol_for_state} #{@message}\n"
55
56
 
56
- masked = @mask * @value.length
57
+ masked = @mask * @value.grapheme_clusters.length
57
58
  display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(masked)) : Colors.dim(masked)
58
59
  lines << "#{bar} #{display}\n"
59
60
 
@@ -63,7 +64,7 @@ module Clack
63
64
  private
64
65
 
65
66
  def masked_display
66
- masked = @mask * @value.length
67
+ masked = @mask * @value.grapheme_clusters.length
67
68
  return cursor_block if masked.empty?
68
69
 
69
70
  "#{masked}#{cursor_block}"
@@ -92,6 +92,12 @@ module Clack
92
92
  def submit_selection
93
93
  path = @value.empty? ? @root : resolve_path(@value)
94
94
 
95
+ unless path_within_root?(path)
96
+ @error_message = "Path must be within #{@root}"
97
+ @state = :error
98
+ return
99
+ end
100
+
95
101
  if @validate
96
102
  result = @validate.call(path)
97
103
  if result
@@ -138,6 +144,13 @@ module Clack
138
144
 
139
145
  def update_suggestions
140
146
  base_path = resolve_path(@value)
147
+
148
+ # Only show suggestions for paths within root
149
+ unless path_within_root?(base_path)
150
+ @suggestions = []
151
+ return
152
+ end
153
+
141
154
  search_dir = File.directory?(base_path) ? base_path : File.dirname(base_path)
142
155
  prefix = File.directory?(base_path) ? "" : File.basename(base_path).downcase
143
156
 
@@ -157,10 +170,10 @@ module Clack
157
170
 
158
171
  def format_entry(dir, entry)
159
172
  full_path = File.join(dir, entry)
160
- if full_path.start_with?(@root)
173
+ if full_path == @root || full_path.start_with?("#{@root}/")
161
174
  # Show relative path without leading ./
162
175
  path = full_path[@root.length..]
163
- path = path.sub(%r{^/}, "") # Remove leading slash
176
+ path = path.sub(%r{^/}, "")
164
177
  path = entry if path.empty?
165
178
  else
166
179
  path = full_path
@@ -172,13 +185,21 @@ module Clack
172
185
  def resolve_path(input)
173
186
  return @root if input.empty?
174
187
 
175
- if input.start_with?("/")
188
+ path = if input.start_with?("/")
176
189
  input
177
190
  elsif input.start_with?("~")
178
191
  File.expand_path(input)
179
192
  else
180
193
  File.join(@root, input)
181
194
  end
195
+
196
+ # Canonicalize to resolve .. and symlinks
197
+ File.expand_path(path)
198
+ end
199
+
200
+ def path_within_root?(path)
201
+ expanded = File.expand_path(path)
202
+ expanded == @root || expanded.start_with?("#{@root}/")
182
203
  end
183
204
 
184
205
  def visible_suggestions
@@ -35,7 +35,6 @@ module Clack
35
35
  @message = message
36
36
  @output = output
37
37
  @started = false
38
- @rendered_once = false
39
38
  @width = 40
40
39
  end
41
40
 
@@ -106,17 +105,13 @@ module Clack
106
105
  def render
107
106
  return unless @started
108
107
 
109
- # Move cursor up and clear line if not first render
110
- if @rendered_once
111
- @output.print "\e[1A\e[2K"
112
- end
113
- @rendered_once = true
114
- @output.puts "#{symbol} #{progress_bar} #{percentage}#{message_text}"
108
+ @output.print "\r\e[2K" # Return to start of line and clear it
109
+ @output.print "#{symbol} #{progress_bar} #{percentage}#{message_text}"
110
+ @output.flush
115
111
  end
116
112
 
117
113
  def render_final(state)
118
- # Move up and clear the progress line
119
- @output.print "\e[1A\e[2K" if @rendered_once
114
+ @output.print "\r\e[2K" # Clear the progress line
120
115
  sym = (state == :success) ? Colors.green(Symbols::S_STEP_SUBMIT) : Colors.red(Symbols::S_STEP_CANCEL)
121
116
  @output.puts "#{sym} #{@message}"
122
117
  end
data/lib/clack/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clack
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
data/lib/clack.rb CHANGED
@@ -574,3 +574,6 @@ previous_int_handler = trap("INT") do
574
574
  else exit(130)
575
575
  end
576
576
  end
577
+
578
+ # Set up SIGWINCH handler for terminal resize
579
+ 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.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Whittaker