clack 0.4.6 → 0.6.0
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 +38 -0
- data/README.md +267 -197
- data/lib/clack/box.rb +6 -6
- data/lib/clack/core/fuzzy_matcher.rb +3 -3
- data/lib/clack/core/key_reader.rb +30 -20
- data/lib/clack/core/options_helper.rb +96 -29
- data/lib/clack/core/prompt.rb +45 -12
- data/lib/clack/core/scroll_helper.rb +10 -41
- data/lib/clack/core/selection_manager.rb +49 -0
- data/lib/clack/note.rb +4 -3
- data/lib/clack/prompts/autocomplete.rb +21 -15
- data/lib/clack/prompts/autocomplete_multiselect.rb +19 -26
- data/lib/clack/prompts/confirm.rb +8 -30
- data/lib/clack/prompts/date.rb +1 -14
- data/lib/clack/prompts/group_multiselect.rb +48 -67
- data/lib/clack/prompts/multiline_text.rb +33 -53
- data/lib/clack/prompts/multiselect.rb +27 -38
- data/lib/clack/prompts/password.rb +1 -14
- data/lib/clack/prompts/path.rb +9 -23
- data/lib/clack/prompts/progress.rb +6 -2
- data/lib/clack/prompts/range.rb +1 -14
- data/lib/clack/prompts/select.rb +18 -32
- data/lib/clack/prompts/select_key.rb +8 -8
- data/lib/clack/prompts/spinner.rb +15 -20
- data/lib/clack/prompts/tasks.rb +6 -1
- data/lib/clack/prompts/text.rb +1 -14
- data/lib/clack/testing.rb +31 -37
- data/lib/clack/utils.rb +106 -22
- data/lib/clack/version.rb +1 -1
- data/lib/clack.rb +71 -37
- metadata +3 -3
data/lib/clack/utils.rb
CHANGED
|
@@ -11,11 +11,28 @@ module Clack
|
|
|
11
11
|
text.to_s.gsub(/\e\[[0-9;]*[a-zA-Z]/, "")
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
# Get visible length of text
|
|
15
|
-
#
|
|
16
|
-
# @
|
|
14
|
+
# Get visible length (display width in columns) of text after stripping ANSI.
|
|
15
|
+
# Uses display_width to correctly measure CJK, emoji, combining chars.
|
|
16
|
+
# @param text [String]
|
|
17
|
+
# @return [Integer] display columns
|
|
17
18
|
def visible_length(text)
|
|
18
|
-
strip_ansi(text)
|
|
19
|
+
display_width(strip_ansi(text))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Calculate the terminal display width (columns) of a string.
|
|
23
|
+
# ASCII and most chars: width 1. CJK ideographs, fullwidth forms, common emoji: width 2.
|
|
24
|
+
# Zero-width joiners, combining marks, variation selectors: width 0.
|
|
25
|
+
# @param string [String]
|
|
26
|
+
# @return [Integer]
|
|
27
|
+
def display_width(string)
|
|
28
|
+
str = string.to_s
|
|
29
|
+
return 0 if str.empty?
|
|
30
|
+
|
|
31
|
+
width = 0
|
|
32
|
+
str.grapheme_clusters.each do |cluster|
|
|
33
|
+
width += grapheme_width(cluster)
|
|
34
|
+
end
|
|
35
|
+
width
|
|
19
36
|
end
|
|
20
37
|
|
|
21
38
|
# Wrap text to a specified width, preserving ANSI codes
|
|
@@ -54,7 +71,7 @@ module Clack
|
|
|
54
71
|
def truncate(text, width, ellipsis: "...")
|
|
55
72
|
return text if visible_length(text) <= width
|
|
56
73
|
|
|
57
|
-
target = width - ellipsis
|
|
74
|
+
target = width - visible_length(ellipsis)
|
|
58
75
|
return ellipsis if target <= 0
|
|
59
76
|
|
|
60
77
|
# Handle ANSI codes: we need to truncate visible chars while preserving codes
|
|
@@ -98,38 +115,105 @@ module Clack
|
|
|
98
115
|
|
|
99
116
|
def break_long_word(word, width)
|
|
100
117
|
lines = []
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
clusters = strip_ansi(word).grapheme_clusters
|
|
119
|
+
|
|
120
|
+
chunk = +""
|
|
121
|
+
chunk_width = 0
|
|
122
|
+
clusters.each do |gc|
|
|
123
|
+
gw = grapheme_width(gc)
|
|
124
|
+
if chunk_width + gw > width && !chunk.empty?
|
|
125
|
+
lines << chunk
|
|
126
|
+
chunk = +""
|
|
127
|
+
chunk_width = 0
|
|
128
|
+
end
|
|
129
|
+
chunk << gc
|
|
130
|
+
chunk_width += gw
|
|
131
|
+
# Force include first grapheme even if its width exceeds limit
|
|
132
|
+
if gw > width && chunk_width == gw
|
|
133
|
+
lines << chunk
|
|
134
|
+
chunk = +""
|
|
135
|
+
chunk_width = 0
|
|
136
|
+
end
|
|
107
137
|
end
|
|
138
|
+
lines << chunk unless chunk.empty?
|
|
108
139
|
|
|
109
140
|
lines
|
|
110
141
|
end
|
|
111
142
|
|
|
112
143
|
def truncate_visible(text, target_len)
|
|
113
|
-
result = ""
|
|
114
|
-
|
|
144
|
+
result = +""
|
|
145
|
+
visible_width = 0
|
|
115
146
|
position = 0
|
|
147
|
+
ansi_re = /\A\e\[[0-9;]*[a-zA-Z]/
|
|
116
148
|
|
|
117
|
-
while position < text.length &&
|
|
118
|
-
if text[position] == "\e" && (match = text[position..].match(
|
|
119
|
-
|
|
120
|
-
result += match[0]
|
|
149
|
+
while position < text.length && visible_width < target_len
|
|
150
|
+
if text[position] == "\e" && (match = text[position..].match(ansi_re))
|
|
151
|
+
result << match[0]
|
|
121
152
|
position += match[0].length
|
|
122
153
|
else
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
154
|
+
# Extract the grapheme cluster starting at this position
|
|
155
|
+
gc = text[position..].grapheme_clusters.first
|
|
156
|
+
break unless gc
|
|
157
|
+
|
|
158
|
+
gw = grapheme_width(gc)
|
|
159
|
+
break if visible_width + gw > target_len
|
|
160
|
+
result << gc
|
|
161
|
+
visible_width += gw
|
|
162
|
+
position += gc.length
|
|
126
163
|
end
|
|
127
164
|
end
|
|
128
165
|
|
|
129
|
-
|
|
130
|
-
result += "\e[0m" if result.include?("\e[") && !result.end_with?("\e[0m")
|
|
166
|
+
result << "\e[0m" if result.include?("\e[") && !result.end_with?("\e[0m")
|
|
131
167
|
result
|
|
132
168
|
end
|
|
169
|
+
|
|
170
|
+
# Width of a grapheme cluster: the max char_width among its codepoints.
|
|
171
|
+
# Handles ZWJ emoji sequences, combining marks, and flag sequences correctly.
|
|
172
|
+
def grapheme_width(cluster)
|
|
173
|
+
max_w = 0
|
|
174
|
+
cluster.each_char do |char|
|
|
175
|
+
w = char_width(char)
|
|
176
|
+
max_w = w if w > max_w
|
|
177
|
+
end
|
|
178
|
+
max_w
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def char_width(char)
|
|
182
|
+
code = char.ord
|
|
183
|
+
return 0 if zero_width_code?(code)
|
|
184
|
+
return 2 if wide_char_code?(code)
|
|
185
|
+
1
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def zero_width_code?(code)
|
|
189
|
+
return true if (0x0300..0x036F).cover?(code)
|
|
190
|
+
return true if (0x1AB0..0x1AFF).cover?(code)
|
|
191
|
+
return true if (0x20D0..0x20FF).cover?(code)
|
|
192
|
+
return true if [0x200B, 0x200C, 0x200D, 0xFEFF].include?(code)
|
|
193
|
+
return true if (0xFE00..0xFE0F).cover?(code)
|
|
194
|
+
false
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def wide_char_code?(code)
|
|
198
|
+
# CJK Unified + extensions + compatibility
|
|
199
|
+
return true if (0x4E00..0x9FFF).cover?(code)
|
|
200
|
+
return true if (0x3400..0x4DBF).cover?(code)
|
|
201
|
+
return true if (0xF900..0xFAFF).cover?(code)
|
|
202
|
+
# Korean Hangul syllables
|
|
203
|
+
return true if (0xAC00..0xD7AF).cover?(code)
|
|
204
|
+
# Japanese kana
|
|
205
|
+
return true if (0x3040..0x309F).cover?(code)
|
|
206
|
+
return true if (0x30A0..0x30FF).cover?(code)
|
|
207
|
+
# Fullwidth and wide punctuation
|
|
208
|
+
return true if (0x3000..0x303F).cover?(code)
|
|
209
|
+
return true if (0xFF01..0xFF5E).cover?(code)
|
|
210
|
+
return true if (0xFFE0..0xFFE6).cover?(code)
|
|
211
|
+
# Common emoji / symbols blocks that render as wide
|
|
212
|
+
return true if (0x1F000..0x1F9FF).cover?(code)
|
|
213
|
+
return true if (0x2600..0x26FF).cover?(code)
|
|
214
|
+
return true if (0x2700..0x27BF).cover?(code)
|
|
215
|
+
false
|
|
216
|
+
end
|
|
133
217
|
end
|
|
134
218
|
end
|
|
135
219
|
end
|
data/lib/clack/version.rb
CHANGED
data/lib/clack.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative "clack/core/options_helper"
|
|
|
13
13
|
require_relative "clack/core/text_input_helper"
|
|
14
14
|
require_relative "clack/core/scroll_helper"
|
|
15
15
|
require_relative "clack/core/fuzzy_matcher"
|
|
16
|
+
require_relative "clack/core/selection_manager"
|
|
16
17
|
require_relative "clack/core/ci_mode"
|
|
17
18
|
require_relative "clack/prompts/text"
|
|
18
19
|
require_relative "clack/prompts/multiline_text"
|
|
@@ -505,47 +506,80 @@ module Clack
|
|
|
505
506
|
load demo_path
|
|
506
507
|
run_demo
|
|
507
508
|
end
|
|
508
|
-
end
|
|
509
|
-
end
|
|
510
509
|
|
|
511
|
-
#
|
|
512
|
-
#
|
|
513
|
-
|
|
510
|
+
# Install signal handlers for clean terminal cleanup.
|
|
511
|
+
# Call this once in your CLI entry point. Handles INT, TERM, and SIGWINCH.
|
|
512
|
+
# Without calling this, Ctrl+C may leave the cursor hidden.
|
|
513
|
+
#
|
|
514
|
+
# @return [void]
|
|
515
|
+
def setup!
|
|
516
|
+
return if @setup_done
|
|
514
517
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
end
|
|
518
|
+
@setup_done = true
|
|
519
|
+
install_signal_handlers
|
|
520
|
+
install_at_exit
|
|
521
|
+
Core::Prompt.setup_signal_handler
|
|
522
|
+
end
|
|
520
523
|
|
|
521
|
-
#
|
|
522
|
-
|
|
523
|
-
begin
|
|
524
|
-
$stdout.write_nonblock(CURSOR_SHOW)
|
|
525
|
-
rescue IOError, SystemCallError
|
|
526
|
-
# Output unavailable — nothing we can do
|
|
527
|
-
end
|
|
528
|
-
case previous_int_handler
|
|
529
|
-
when Proc
|
|
530
|
-
begin
|
|
531
|
-
previous_int_handler.call
|
|
532
|
-
rescue
|
|
533
|
-
exit(130)
|
|
534
|
-
end
|
|
535
|
-
when "DEFAULT", "SYSTEM_DEFAULT" then exit(130)
|
|
536
|
-
else exit(130)
|
|
537
|
-
end
|
|
538
|
-
end
|
|
524
|
+
# @return [Boolean] whether setup! has been called
|
|
525
|
+
def setup? = !!@setup_done
|
|
539
526
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
527
|
+
private
|
|
528
|
+
|
|
529
|
+
CURSOR_SHOW = "\e[?25h"
|
|
530
|
+
private_constant :CURSOR_SHOW
|
|
531
|
+
|
|
532
|
+
def install_at_exit
|
|
533
|
+
at_exit do
|
|
534
|
+
$stdout.print Core::Cursor.show
|
|
535
|
+
rescue IOError, SystemCallError
|
|
536
|
+
# Output unavailable
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def install_signal_handlers
|
|
541
|
+
# Chain INT handler to restore cursor before passing to previous handler.
|
|
542
|
+
previous_int_handler = trap("INT") do
|
|
543
|
+
begin
|
|
544
|
+
$stdout.write_nonblock(CURSOR_SHOW)
|
|
545
|
+
rescue IOError, SystemCallError
|
|
546
|
+
# Output unavailable
|
|
547
|
+
end
|
|
548
|
+
case previous_int_handler
|
|
549
|
+
when Proc
|
|
550
|
+
begin
|
|
551
|
+
previous_int_handler.call
|
|
552
|
+
rescue
|
|
553
|
+
exit(130)
|
|
554
|
+
end
|
|
555
|
+
when "DEFAULT", "SYSTEM_DEFAULT" then exit(130)
|
|
556
|
+
else exit(130)
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# Chain TERM handler to restore cursor, then delegate to previous handler.
|
|
561
|
+
previous_term_handler = trap("TERM") do
|
|
562
|
+
begin
|
|
563
|
+
$stdout.write_nonblock(CURSOR_SHOW)
|
|
564
|
+
rescue IOError, SystemCallError
|
|
565
|
+
# Output unavailable
|
|
566
|
+
end
|
|
567
|
+
case previous_term_handler
|
|
568
|
+
when Proc
|
|
569
|
+
begin
|
|
570
|
+
previous_term_handler.call
|
|
571
|
+
rescue
|
|
572
|
+
exit(143)
|
|
573
|
+
end
|
|
574
|
+
when "DEFAULT", "SYSTEM_DEFAULT" then exit(143)
|
|
575
|
+
else exit(143)
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
end
|
|
546
579
|
end
|
|
547
|
-
exit(143)
|
|
548
580
|
end
|
|
549
581
|
|
|
550
|
-
#
|
|
551
|
-
|
|
582
|
+
# Auto-setup for backwards compatibility.
|
|
583
|
+
# Libraries that require 'clack' get signal handlers installed automatically.
|
|
584
|
+
# To opt out, set CLACK_NO_AUTO_SETUP=1 before requiring clack.
|
|
585
|
+
Clack.setup! unless ENV["CLACK_NO_AUTO_SETUP"]
|
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
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Steve Whittaker
|
|
@@ -50,6 +50,7 @@ files:
|
|
|
50
50
|
- lib/clack/core/options_helper.rb
|
|
51
51
|
- lib/clack/core/prompt.rb
|
|
52
52
|
- lib/clack/core/scroll_helper.rb
|
|
53
|
+
- lib/clack/core/selection_manager.rb
|
|
53
54
|
- lib/clack/core/settings.rb
|
|
54
55
|
- lib/clack/core/text_input_helper.rb
|
|
55
56
|
- lib/clack/environment.rb
|
|
@@ -84,7 +85,6 @@ homepage: https://github.com/swhitt/clackrb
|
|
|
84
85
|
licenses:
|
|
85
86
|
- MIT
|
|
86
87
|
metadata:
|
|
87
|
-
homepage_uri: https://github.com/swhitt/clackrb
|
|
88
88
|
source_code_uri: https://github.com/swhitt/clackrb
|
|
89
89
|
changelog_uri: https://github.com/swhitt/clackrb/blob/main/CHANGELOG.md
|
|
90
90
|
bug_tracker_uri: https://github.com/swhitt/clackrb/issues
|
|
@@ -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:
|
|
106
|
+
rubygems_version: 3.6.9
|
|
107
107
|
specification_version: 4
|
|
108
108
|
summary: Beautiful, minimal CLI prompts
|
|
109
109
|
test_files: []
|