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 +4 -4
- data/CHANGELOG.md +16 -8
- data/LICENSE +1 -1
- data/README.md +0 -1
- data/lib/clack/core/prompt.rb +58 -2
- data/lib/clack/core/settings.rb +1 -0
- data/lib/clack/prompts/autocomplete.rb +1 -0
- data/lib/clack/prompts/autocomplete_multiselect.rb +1 -0
- data/lib/clack/prompts/confirm.rb +1 -0
- data/lib/clack/prompts/group_multiselect.rb +1 -0
- data/lib/clack/prompts/multiline_text.rb +200 -0
- data/lib/clack/prompts/multiselect.rb +1 -0
- data/lib/clack/prompts/password.rb +5 -3
- data/lib/clack/prompts/path.rb +25 -3
- data/lib/clack/prompts/progress.rb +4 -9
- data/lib/clack/prompts/select.rb +1 -0
- data/lib/clack/prompts/select_key.rb +1 -0
- data/lib/clack/prompts/text.rb +1 -0
- data/lib/clack/validators.rb +11 -0
- data/lib/clack/version.rb +1 -1
- data/lib/clack.rb +17 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3035b53cb615be8f42827389fc118990457a06e20e9a118c9eb77a97ff5352f8
|
|
4
|
+
data.tar.gz: 98b12f8e74d475a50987a10c719f231050d83484989acd101c4e03bf11ad4b5e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
7
|
-
- `
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
###
|
|
13
|
-
-
|
|
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
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
|
data/lib/clack/core/prompt.rb
CHANGED
|
@@ -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
|
data/lib/clack/core/settings.rb
CHANGED
|
@@ -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
|
|
@@ -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|
|
|
@@ -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
|
|
@@ -30,7 +30,8 @@ module Clack
|
|
|
30
30
|
return unless Core::Settings.printable?(key)
|
|
31
31
|
|
|
32
32
|
if Core::Settings.backspace?(key)
|
|
33
|
-
|
|
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}"
|
data/lib/clack/prompts/path.rb
CHANGED
|
@@ -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{^/}, "")
|
|
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
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
#
|
|
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/prompts/select.rb
CHANGED
data/lib/clack/prompts/text.rb
CHANGED
data/lib/clack/validators.rb
CHANGED
|
@@ -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
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.
|
|
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
|