clack 0.1.0 → 0.1.2

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: 3035b53cb615be8f42827389fc118990457a06e20e9a118c9eb77a97ff5352f8
4
+ data.tar.gz: 98b12f8e74d475a50987a10c719f231050d83484989acd101c4e03bf11ad4b5e
5
5
  SHA512:
6
- metadata.gz: 2412c28b3f625313df7249154a2a7a72cd039e336f3ee2764130e7dc52a5f932358a1576bc793d8fa33b5862d60cf5dff319a5b6495546bd59cef24714c93b1f
7
- data.tar.gz: dc26d51d23d264c510c913383e448b3b8552312d1a1a78db7cba71e69ace120cc8638f69adc615fb844652065520947286a46cf75657a6a8ecdbcb499ee58bfc
6
+ metadata.gz: 770e87fe78a09c35ed8150aaee98dbc75ccadb8d23ba39a2b2ba684e00e5cf50b536e1cc0bce00aee9e9aaf22baa2dad566c75c0f64d7b80d799a70009bd2057
7
+ data.tar.gz: f77bf005240ce8533d1b1110bcd430409847fa9263682df3b7362c72b599570a77ad9eb2fad31bbc8690599348bdbd026a05733d5dad357aaa9824120382c8f5
data/CHANGELOG.md CHANGED
@@ -2,16 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.1.2]
6
+
5
7
  ### 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
8
+ - `multiline_text` prompt for multi-line input (Ctrl+D to submit)
9
+ - `help:` option on all prompts for contextual help text
10
+ - Documentation for blocking validation (database/API calls work out of the box)
11
+
12
+ ## [0.1.1]
11
13
 
12
- ### Changed
13
- - Ruby 3.2+ support
14
+ ### Fixed
15
+ - Path prompt now correctly rejects paths outside root (fixed boundary check bug)
16
+ - Password backspace properly removes Unicode grapheme clusters, not just bytes
17
+ - Terminal cleanup handles closed output streams gracefully
18
+ - SIGWINCH handler uses explicit signal check instead of rescue
19
+
20
+ ### Added
21
+ - Terminal resize support via SIGWINCH signal handling
14
22
 
15
23
  ## [0.1.0]
16
24
 
17
- Initial release.
25
+ 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
@@ -35,11 +65,13 @@ module Clack
35
65
  attr_reader :error_message
36
66
 
37
67
  # @param message [String] the prompt message to display
68
+ # @param help [String, nil] optional help text shown below the message
38
69
  # @param validate [Proc, nil] optional validation proc; returns error string or nil
39
70
  # @param input [IO] input stream (default: $stdin)
40
71
  # @param output [IO] output stream (default: $stdout)
41
- def initialize(message:, validate: nil, input: $stdin, output: $stdout)
72
+ def initialize(message:, help: nil, validate: nil, input: $stdin, output: $stdout)
42
73
  @message = message
74
+ @help = help
43
75
  @validate = validate
44
76
  @input = input
45
77
  @output = output
@@ -48,6 +80,13 @@ module Clack
48
80
  @error_message = nil
49
81
  @prev_frame = nil
50
82
  @cursor = 0
83
+ @needs_redraw = false
84
+ end
85
+
86
+ # Request a full redraw on next render cycle.
87
+ # Called by SIGWINCH handler when terminal is resized.
88
+ def request_redraw
89
+ @needs_redraw = true
51
90
  end
52
91
 
53
92
  # Run the prompt interaction loop.
@@ -57,6 +96,7 @@ module Clack
57
96
  #
58
97
  # @return [Object, Clack::CANCEL] the submitted value or CANCEL sentinel
59
98
  def run
99
+ Prompt.register(self)
60
100
  setup_terminal
61
101
  render
62
102
  @state = :active
@@ -72,6 +112,7 @@ module Clack
72
112
  finalize
73
113
  (terminal_state? && @state == :cancel) ? CANCEL : @value
74
114
  ensure
115
+ Prompt.unregister(self)
75
116
  cleanup_terminal
76
117
  end
77
118
 
@@ -121,9 +162,16 @@ module Clack
121
162
  end
122
163
 
123
164
  # Render the current frame using differential rendering.
124
- # Only redraws if the frame content has changed.
165
+ # Only redraws if the frame content has changed or redraw was requested.
125
166
  def render
126
167
  frame = build_frame
168
+
169
+ # Force redraw if terminal was resized
170
+ if @needs_redraw
171
+ @needs_redraw = false
172
+ @prev_frame = nil
173
+ end
174
+
127
175
  return if frame == @prev_frame
128
176
 
129
177
  if @state == :initial
@@ -176,6 +224,8 @@ module Clack
176
224
 
177
225
  def cleanup_terminal
178
226
  @output.print Cursor.show
227
+ rescue IOError, SystemCallError
228
+ # Output unavailable - terminal may need manual reset
179
229
  end
180
230
 
181
231
  def restore_cursor
@@ -198,6 +248,12 @@ module Clack
198
248
  (@state == :error) ? Colors.yellow(Symbols::S_BAR_END) : Colors.gray(Symbols::S_BAR_END)
199
249
  end
200
250
 
251
+ def help_line
252
+ return "" unless @help
253
+
254
+ "#{bar} #{Colors.dim(@help)}\n"
255
+ end
256
+
201
257
  def cursor_block
202
258
  Colors.inverse(" ")
203
259
  end
@@ -10,6 +10,7 @@ module Clack
10
10
  KEY_BACKSPACE = "\b" # ASCII 8: Backspace
11
11
  KEY_DELETE = "\u007F" # ASCII 127: Delete (often sent by backspace key)
12
12
  KEY_CTRL_C = "\u0003" # ASCII 3: Ctrl+C (interrupt)
13
+ KEY_CTRL_D = "\u0004" # ASCII 4: Ctrl+D (EOF, used for multiline submit)
13
14
  KEY_ESCAPE = "\e" # ASCII 27: Escape
14
15
  KEY_ENTER = "\r" # ASCII 13: Carriage return
15
16
  KEY_NEWLINE = "\n" # ASCII 10: Line feed
@@ -89,6 +89,7 @@ module Clack
89
89
  lines = []
90
90
  lines << "#{bar}\n"
91
91
  lines << "#{symbol_for_state} #{@message}\n"
92
+ lines << help_line
92
93
  lines << "#{active_bar} #{input_display}\n"
93
94
 
94
95
  visible_options.each_with_index do |opt, idx|
@@ -136,6 +136,7 @@ module Clack
136
136
  lines = []
137
137
  lines << "#{bar}\n"
138
138
  lines << "#{symbol_for_state} #{@message}\n"
139
+ lines << help_line
139
140
  lines << "#{active_bar} #{Colors.dim("Search:")} #{search_input_display}#{match_count}\n"
140
141
 
141
142
  visible_options.each_with_index do |opt, idx|
@@ -65,6 +65,7 @@ module Clack
65
65
  lines = []
66
66
  lines << "#{bar}\n"
67
67
  lines << "#{symbol_for_state} #{@message}\n"
68
+ lines << help_line
68
69
  lines << "#{bar} #{options_display}\n"
69
70
  lines << "#{Colors.gray(Symbols::S_BAR_END)}\n"
70
71
  lines.join
@@ -59,6 +59,7 @@ module Clack
59
59
  lines = []
60
60
  lines << "#{bar}\n"
61
61
  lines << "#{symbol_for_state} #{@message}\n"
62
+ lines << help_line
62
63
 
63
64
  prev_was_group = false
64
65
  @flat_items.each_with_index do |item, idx|
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clack
4
+ module Prompts
5
+ # Multi-line text input prompt with line navigation.
6
+ #
7
+ # Features:
8
+ # - Enter inserts newline, Ctrl+D submits
9
+ # - Arrow key navigation (up/down between lines, left/right within line)
10
+ # - Backspace merges lines when at line start
11
+ # - Validation support
12
+ #
13
+ # @example Basic usage
14
+ # message = Clack.multiline_text(message: "Enter your commit message:")
15
+ #
16
+ # @example With initial value and validation
17
+ # content = Clack.multiline_text(
18
+ # message: "Description:",
19
+ # initial_value: "feat: ",
20
+ # validate: ->(v) { "Required!" if v.strip.empty? }
21
+ # )
22
+ #
23
+ class MultilineText < Core::Prompt
24
+ # @param message [String] the prompt message
25
+ # @param initial_value [String, nil] pre-filled editable text (can contain newlines)
26
+ # @param validate [Proc, nil] validation proc returning error string or nil
27
+ # @param opts [Hash] additional options passed to {Core::Prompt}
28
+ def initialize(message:, initial_value: nil, **opts)
29
+ super(message:, **opts)
30
+ @lines = parse_initial_value(initial_value)
31
+ @line_index = @lines.length - 1
32
+ @column = current_line.grapheme_clusters.length
33
+ end
34
+
35
+ protected
36
+
37
+ def handle_key(key)
38
+ return if terminal_state?
39
+
40
+ @state = :active if @state == :error
41
+
42
+ case key
43
+ when Core::Settings::KEY_CTRL_D
44
+ submit
45
+ when Core::Settings::KEY_ESCAPE, Core::Settings::KEY_CTRL_C
46
+ @state = :cancel
47
+ when Core::Settings::KEY_ENTER, Core::Settings::KEY_NEWLINE
48
+ insert_newline
49
+ else
50
+ handle_input(key)
51
+ end
52
+ end
53
+
54
+ def handle_input(key)
55
+ case key
56
+ when "\e[A" then move_up
57
+ when "\e[B" then move_down
58
+ when "\e[C" then move_right
59
+ when "\e[D" then move_left
60
+ else handle_text_input(key)
61
+ end
62
+ end
63
+
64
+ def submit
65
+ @value = @lines.join("\n")
66
+ super
67
+ end
68
+
69
+ def build_frame
70
+ lines = []
71
+ lines << "#{bar}\n"
72
+ lines << "#{symbol_for_state} #{@message} #{Colors.dim("(Ctrl+D to submit)")}\n"
73
+ lines << help_line
74
+
75
+ @lines.each_with_index do |line, idx|
76
+ display = (idx == @line_index) ? line_with_cursor(line) : line
77
+ lines << "#{active_bar} #{display}\n"
78
+ end
79
+
80
+ lines << "#{bar_end}\n"
81
+ lines[-1] = "#{Colors.yellow(Symbols::S_BAR_END)} #{Colors.yellow(@error_message)}\n" if @state == :error
82
+
83
+ lines.join
84
+ end
85
+
86
+ def build_final_frame
87
+ lines = []
88
+ lines << "#{bar}\n"
89
+ lines << "#{symbol_for_state} #{@message}\n"
90
+
91
+ if @state == :cancel
92
+ @lines.each { |line| lines << "#{bar} #{Colors.strikethrough(Colors.dim(line))}\n" }
93
+ else
94
+ @lines.each { |line| lines << "#{bar} #{Colors.dim(line)}\n" }
95
+ end
96
+
97
+ lines.join
98
+ end
99
+
100
+ private
101
+
102
+ def parse_initial_value(value)
103
+ return [""] if value.nil? || value.empty?
104
+
105
+ value.split("\n", -1) # -1 preserves trailing empty strings
106
+ end
107
+
108
+ def current_line
109
+ @lines[@line_index] || ""
110
+ end
111
+
112
+ def line_with_cursor(line)
113
+ chars = line.grapheme_clusters
114
+ return cursor_block if chars.empty?
115
+ return "#{line}#{cursor_block}" if @column >= chars.length
116
+
117
+ before = chars[0...@column].join
118
+ current = Colors.inverse(chars[@column])
119
+ after = chars[(@column + 1)..].join
120
+ "#{before}#{current}#{after}"
121
+ end
122
+
123
+ def insert_newline
124
+ chars = current_line.grapheme_clusters
125
+ before = chars[0...@column].join
126
+ after = chars[@column..].join
127
+
128
+ @lines[@line_index] = before
129
+ @lines.insert(@line_index + 1, after)
130
+ @line_index += 1
131
+ @column = 0
132
+ end
133
+
134
+ def move_up
135
+ return if @line_index.zero?
136
+
137
+ @line_index -= 1
138
+ clamp_column
139
+ end
140
+
141
+ def move_down
142
+ return if @line_index >= @lines.length - 1
143
+
144
+ @line_index += 1
145
+ clamp_column
146
+ end
147
+
148
+ def move_left
149
+ return if @column.zero?
150
+
151
+ @column -= 1
152
+ end
153
+
154
+ def move_right
155
+ max = current_line.grapheme_clusters.length
156
+ return if @column >= max
157
+
158
+ @column += 1
159
+ end
160
+
161
+ def clamp_column
162
+ max = current_line.grapheme_clusters.length
163
+ @column = [@column, max].min
164
+ end
165
+
166
+ def handle_text_input(key)
167
+ return handle_backspace if Core::Settings.backspace?(key)
168
+ return unless Core::Settings.printable?(key)
169
+
170
+ chars = current_line.grapheme_clusters
171
+ chars.insert(@column, key)
172
+ @lines[@line_index] = chars.join
173
+ @column += 1
174
+ end
175
+
176
+ def handle_backspace
177
+ if @column.zero?
178
+ return if @line_index.zero?
179
+
180
+ merge_line_up
181
+ else
182
+ chars = current_line.grapheme_clusters
183
+ chars.delete_at(@column - 1)
184
+ @lines[@line_index] = chars.join
185
+ @column -= 1
186
+ end
187
+ end
188
+
189
+ def merge_line_up
190
+ return if @line_index.zero?
191
+
192
+ current_content = @lines.delete_at(@line_index)
193
+ @line_index -= 1
194
+ prev_length = current_line.grapheme_clusters.length
195
+ @lines[@line_index] = current_line + current_content
196
+ @column = prev_length
197
+ end
198
+ end
199
+ end
200
+ end
@@ -94,6 +94,7 @@ module Clack
94
94
  lines = []
95
95
  lines << "#{bar}\n"
96
96
  lines << "#{symbol_for_state} #{@message}\n"
97
+ lines << help_line
97
98
 
98
99
  visible_options.each_with_index do |opt, idx|
99
100
  actual_idx = @scroll_offset + idx
@@ -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
@@ -40,6 +41,7 @@ module Clack
40
41
  lines = []
41
42
  lines << "#{bar}\n"
42
43
  lines << "#{symbol_for_state} #{@message}\n"
44
+ lines << help_line
43
45
  lines << "#{active_bar} #{masked_display}\n"
44
46
  lines << "#{bar_end}\n"
45
47
 
@@ -53,7 +55,7 @@ module Clack
53
55
  lines << "#{bar}\n"
54
56
  lines << "#{symbol_for_state} #{@message}\n"
55
57
 
56
- masked = @mask * @value.length
58
+ masked = @mask * @value.grapheme_clusters.length
57
59
  display = (@state == :cancel) ? Colors.strikethrough(Colors.dim(masked)) : Colors.dim(masked)
58
60
  lines << "#{bar} #{display}\n"
59
61
 
@@ -63,7 +65,7 @@ module Clack
63
65
  private
64
66
 
65
67
  def masked_display
66
- masked = @mask * @value.length
68
+ masked = @mask * @value.grapheme_clusters.length
67
69
  return cursor_block if masked.empty?
68
70
 
69
71
  "#{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
@@ -109,6 +115,7 @@ module Clack
109
115
  lines = []
110
116
  lines << "#{bar}\n"
111
117
  lines << "#{symbol_for_state} #{@message}\n"
118
+ lines << help_line
112
119
  lines << "#{active_bar} #{input_display}\n"
113
120
 
114
121
  visible_suggestions.each_with_index do |path, idx|
@@ -138,6 +145,13 @@ module Clack
138
145
 
139
146
  def update_suggestions
140
147
  base_path = resolve_path(@value)
148
+
149
+ # Only show suggestions for paths within root
150
+ unless path_within_root?(base_path)
151
+ @suggestions = []
152
+ return
153
+ end
154
+
141
155
  search_dir = File.directory?(base_path) ? base_path : File.dirname(base_path)
142
156
  prefix = File.directory?(base_path) ? "" : File.basename(base_path).downcase
143
157
 
@@ -157,10 +171,10 @@ module Clack
157
171
 
158
172
  def format_entry(dir, entry)
159
173
  full_path = File.join(dir, entry)
160
- if full_path.start_with?(@root)
174
+ if full_path == @root || full_path.start_with?("#{@root}/")
161
175
  # Show relative path without leading ./
162
176
  path = full_path[@root.length..]
163
- path = path.sub(%r{^/}, "") # Remove leading slash
177
+ path = path.sub(%r{^/}, "")
164
178
  path = entry if path.empty?
165
179
  else
166
180
  path = full_path
@@ -172,13 +186,21 @@ module Clack
172
186
  def resolve_path(input)
173
187
  return @root if input.empty?
174
188
 
175
- if input.start_with?("/")
189
+ path = if input.start_with?("/")
176
190
  input
177
191
  elsif input.start_with?("~")
178
192
  File.expand_path(input)
179
193
  else
180
194
  File.join(@root, input)
181
195
  end
196
+
197
+ # Canonicalize to resolve .. and symlinks
198
+ File.expand_path(path)
199
+ end
200
+
201
+ def path_within_root?(path)
202
+ expanded = File.expand_path(path)
203
+ expanded == @root || expanded.start_with?("#{@root}/")
182
204
  end
183
205
 
184
206
  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
@@ -69,6 +69,7 @@ module Clack
69
69
  lines = []
70
70
  lines << "#{bar}\n"
71
71
  lines << "#{symbol_for_state} #{@message}\n"
72
+ lines << help_line
72
73
 
73
74
  visible_options.each_with_index do |opt, idx|
74
75
  actual_idx = @scroll_offset + idx
@@ -55,6 +55,7 @@ module Clack
55
55
  lines = []
56
56
  lines << "#{bar}\n"
57
57
  lines << "#{symbol_for_state} #{@message}\n"
58
+ lines << help_line
58
59
 
59
60
  @options.each do |opt|
60
61
  lines << "#{bar} #{option_display(opt)}\n"
@@ -70,6 +70,7 @@ module Clack
70
70
  lines = []
71
71
  lines << "#{bar}\n"
72
72
  lines << "#{symbol_for_state} #{@message}\n"
73
+ lines << help_line
73
74
  lines << "#{active_bar} #{input_display}\n"
74
75
  lines << "#{bar_end}\n" if @state == :active || @state == :initial
75
76
 
@@ -4,11 +4,22 @@ module Clack
4
4
  # Built-in validators for common validation patterns.
5
5
  # Use these with the `validate:` option on prompts.
6
6
  #
7
+ # Validation procs can perform any operation including slow I/O (database
8
+ # lookups, API calls, etc.) - they simply block until complete.
9
+ #
7
10
  # @example Using built-in validators
8
11
  # Clack.text(message: "Name?", validate: Clack::Validators.required)
9
12
  # Clack.text(message: "Email?", validate: Clack::Validators.format(/@/, "Must be an email"))
10
13
  # Clack.password(message: "Password?", validate: Clack::Validators.min_length(8))
11
14
  #
15
+ # @example Database validation (blocking I/O)
16
+ # Clack.text(
17
+ # message: "Email?",
18
+ # validate: ->(email) {
19
+ # "Already taken" if User.exists?(email: email)
20
+ # }
21
+ # )
22
+ #
12
23
  # @example Combining validators
13
24
  # Clack.text(
14
25
  # message: "Username?",
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.2"
5
5
  end
data/lib/clack.rb CHANGED
@@ -12,6 +12,7 @@ require_relative "clack/core/prompt"
12
12
  require_relative "clack/core/options_helper"
13
13
  require_relative "clack/core/text_input_helper"
14
14
  require_relative "clack/prompts/text"
15
+ require_relative "clack/prompts/multiline_text"
15
16
  require_relative "clack/prompts/password"
16
17
  require_relative "clack/prompts/confirm"
17
18
  require_relative "clack/prompts/select"
@@ -128,6 +129,19 @@ module Clack
128
129
  Prompts::Text.new(message:, **opts).run
129
130
  end
130
131
 
132
+ # Prompt for multi-line text input.
133
+ #
134
+ # Enter inserts a newline, Ctrl+D submits. Useful for commit messages,
135
+ # notes, or any multi-line content.
136
+ #
137
+ # @param message [String] the prompt message
138
+ # @param initial_value [String, nil] pre-filled editable text (can contain newlines)
139
+ # @param validate [Proc, nil] validation function returning error message or nil
140
+ # @return [String, CANCEL] user input (lines joined with \n) or CANCEL if cancelled
141
+ def multiline_text(message:, **opts)
142
+ Prompts::MultilineText.new(message:, **opts).run
143
+ end
144
+
131
145
  # Prompt for password input (masked display).
132
146
  #
133
147
  # @param message [String] the prompt message
@@ -574,3 +588,6 @@ previous_int_handler = trap("INT") do
574
588
  else exit(130)
575
589
  end
576
590
  end
591
+
592
+ # Set up SIGWINCH handler for terminal resize
593
+ 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.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Whittaker
@@ -39,6 +39,7 @@ files:
39
39
  - lib/clack/prompts/autocomplete_multiselect.rb
40
40
  - lib/clack/prompts/confirm.rb
41
41
  - lib/clack/prompts/group_multiselect.rb
42
+ - lib/clack/prompts/multiline_text.rb
42
43
  - lib/clack/prompts/multiselect.rb
43
44
  - lib/clack/prompts/password.rb
44
45
  - lib/clack/prompts/path.rb