r2ui 0.1.0 → 0.2.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 +64 -0
- data/README.md +79 -0
- data/lib/r2ui/app.rb +311 -34
- data/lib/r2ui/canvas.rb +135 -14
- data/lib/r2ui/cli/builder.rb +72 -0
- data/lib/r2ui/cli/context.rb +45 -0
- data/lib/r2ui/cli/definition.rb +174 -0
- data/lib/r2ui/cli/ext/ask.rb +133 -0
- data/lib/r2ui/cli/ext/box.rb +112 -0
- data/lib/r2ui/cli/ext/choose.rb +162 -0
- data/lib/r2ui/cli/ext/choose_many.rb +241 -0
- data/lib/r2ui/cli/ext/color_option.rb +64 -0
- data/lib/r2ui/cli/ext/completion.rb +520 -0
- data/lib/r2ui/cli/ext/config.rb +179 -0
- data/lib/r2ui/cli/ext/confirm.rb +110 -0
- data/lib/r2ui/cli/ext/dashboard.rb +105 -0
- data/lib/r2ui/cli/ext/diff.rb +195 -0
- data/lib/r2ui/cli/ext/editor.rb +94 -0
- data/lib/r2ui/cli/ext/env.rb +108 -0
- data/lib/r2ui/cli/ext/examples.rb +62 -0
- data/lib/r2ui/cli/ext/filter.rb +269 -0
- data/lib/r2ui/cli/ext/format.rb +104 -0
- data/lib/r2ui/cli/ext/list.rb +117 -0
- data/lib/r2ui/cli/ext/log.rb +89 -0
- data/lib/r2ui/cli/ext/pager.rb +97 -0
- data/lib/r2ui/cli/ext/pairs.rb +62 -0
- data/lib/r2ui/cli/ext/parallel.rb +274 -0
- data/lib/r2ui/cli/ext/password.rb +158 -0
- data/lib/r2ui/cli/ext/progress.rb +330 -0
- data/lib/r2ui/cli/ext/reference.rb +129 -0
- data/lib/r2ui/cli/ext/spinner.rb +165 -0
- data/lib/r2ui/cli/ext/summary.rb +63 -0
- data/lib/r2ui/cli/ext/table.rb +141 -0
- data/lib/r2ui/cli/ext/tasks.rb +216 -0
- data/lib/r2ui/cli/ext/trace.rb +64 -0
- data/lib/r2ui/cli/ext/tree.rb +114 -0
- data/lib/r2ui/cli/ext/version.rb +67 -0
- data/lib/r2ui/cli/extension.rb +129 -0
- data/lib/r2ui/cli/help.rb +115 -0
- data/lib/r2ui/cli/helpers.rb +20 -0
- data/lib/r2ui/cli/live.rb +150 -0
- data/lib/r2ui/cli/parser.rb +165 -0
- data/lib/r2ui/cli/prompt.rb +133 -0
- data/lib/r2ui/cli/runner.rb +86 -0
- data/lib/r2ui/cli/shell.rb +185 -0
- data/lib/r2ui/cli/testing.rb +49 -0
- data/lib/r2ui/cli.rb +129 -0
- data/lib/r2ui/compat/bubbletea/LICENSE-bubbletea.txt +21 -0
- data/lib/r2ui/compat/bubbletea/ansi.rb +424 -0
- data/lib/r2ui/compat/bubbletea/commands.rb +143 -0
- data/lib/r2ui/compat/bubbletea/input.rb +269 -0
- data/lib/r2ui/compat/bubbletea/input_reader.rb +66 -0
- data/lib/r2ui/compat/bubbletea/messages.rb +251 -0
- data/lib/r2ui/compat/bubbletea/model.rb +65 -0
- data/lib/r2ui/compat/bubbletea/native.rb +254 -0
- data/lib/r2ui/compat/bubbletea/renderer.rb +192 -0
- data/lib/r2ui/compat/bubbletea/runner.rb +425 -0
- data/lib/r2ui/compat/bubbletea/terminal.rb +230 -0
- data/lib/r2ui/compat/bubbletea/version.rb +8 -0
- data/lib/r2ui/compat/bubbletea/width_table.rb +228 -0
- data/lib/r2ui/compat/bubbletea.rb +16 -0
- data/lib/r2ui/compat/lipgloss/border.rb +17 -0
- data/lib/r2ui/compat/lipgloss/color.rb +78 -0
- data/lib/r2ui/compat/lipgloss/color_blend.rb +166 -0
- data/lib/r2ui/compat/lipgloss/colorful.rb +585 -0
- data/lib/r2ui/compat/lipgloss/layout.rb +461 -0
- data/lib/r2ui/compat/lipgloss/list.rb +85 -0
- data/lib/r2ui/compat/lipgloss/position.rb +30 -0
- data/lib/r2ui/compat/lipgloss/style/border.rb +134 -0
- data/lib/r2ui/compat/lipgloss/style.rb +1062 -0
- data/lib/r2ui/compat/lipgloss/table/render.rb +518 -0
- data/lib/r2ui/compat/lipgloss/table.rb +185 -0
- data/lib/r2ui/compat/lipgloss/termenv.rb +768 -0
- data/lib/r2ui/compat/lipgloss/text/unicode_tables.rb +428 -0
- data/lib/r2ui/compat/lipgloss/text.rb +1555 -0
- data/lib/r2ui/compat/lipgloss/tree/go_tree.rb +243 -0
- data/lib/r2ui/compat/lipgloss/tree.rb +105 -0
- data/lib/r2ui/compat/lipgloss/version.rb +15 -0
- data/lib/r2ui/compat/lipgloss.rb +64 -0
- data/lib/r2ui/compat/load_path/bubbletea.rb +4 -0
- data/lib/r2ui/compat/load_path/lipgloss.rb +4 -0
- data/lib/r2ui/component.rb +67 -0
- data/lib/r2ui/context.rb +87 -0
- data/lib/r2ui/drop_in.rb +16 -0
- data/lib/r2ui/dsl/column.rb +11 -3
- data/lib/r2ui/dsl/dashboard.rb +45 -9
- data/lib/r2ui/dsl/resource.rb +12 -0
- data/lib/r2ui/ext/after.rb +54 -0
- data/lib/r2ui/ext/async.rb +81 -0
- data/lib/r2ui/ext/batch_sequence.rb +54 -0
- data/lib/r2ui/ext/countdown.rb +52 -0
- data/lib/r2ui/ext/data_table.rb +71 -0
- data/lib/r2ui/ext/every.rb +66 -0
- data/lib/r2ui/ext/exec.rb +116 -0
- data/lib/r2ui/ext/file_picker.rb +78 -0
- data/lib/r2ui/ext/focus_report.rb +69 -0
- data/lib/r2ui/ext/help.rb +62 -0
- data/lib/r2ui/ext/keys.rb +64 -0
- data/lib/r2ui/ext/list.rb +145 -0
- data/lib/r2ui/ext/messages.rb +87 -0
- data/lib/r2ui/ext/mouse.rb +134 -0
- data/lib/r2ui/ext/paginator.rb +86 -0
- data/lib/r2ui/ext/paste.rb +86 -0
- data/lib/r2ui/ext/println.rb +21 -0
- data/lib/r2ui/ext/progress.rb +85 -0
- data/lib/r2ui/ext/refresh.rb +56 -0
- data/lib/r2ui/ext/resize.rb +62 -0
- data/lib/r2ui/ext/screen.rb +47 -0
- data/lib/r2ui/ext/screen_mode.rb +63 -0
- data/lib/r2ui/ext/spinner.rb +59 -0
- data/lib/r2ui/ext/state.rb +45 -0
- data/lib/r2ui/ext/stopwatch.rb +67 -0
- data/lib/r2ui/ext/suspend.rb +56 -0
- data/lib/r2ui/ext/text_area.rb +73 -0
- data/lib/r2ui/ext/text_input.rb +83 -0
- data/lib/r2ui/ext/theme.rb +112 -0
- data/lib/r2ui/ext/view.rb +37 -0
- data/lib/r2ui/ext/viewport.rb +92 -0
- data/lib/r2ui/ext/window_title.rb +51 -0
- data/lib/r2ui/extension.rb +199 -0
- data/lib/r2ui/history.rb +4 -1
- data/lib/r2ui/keys.rb +35 -8
- data/lib/r2ui/motion.rb +169 -0
- data/lib/r2ui/panel_state.rb +46 -2
- data/lib/r2ui/renderer.rb +87 -22
- data/lib/r2ui/version.rb +1 -1
- data/lib/r2ui/widgets/box.rb +6 -3
- data/lib/r2ui/widgets/glyphs.rb +149 -0
- data/lib/r2ui/widgets/table.rb +109 -25
- data/lib/r2ui.rb +12 -1
- metadata +119 -4
- data/lib/r2ui/terminal.rb +0 -53
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Terminal colors and SGR styling: a port of muesli/termenv v0.16.0 (profiles, colors, Style, and the
|
|
4
|
+
# Unix color-profile / background detection) plus lipgloss v1.1.0 color.go and renderer.go.
|
|
5
|
+
#
|
|
6
|
+
# API used by Lipgloss::Style (mirrors termenv / lipgloss Go names):
|
|
7
|
+
#
|
|
8
|
+
# R2UI::Compat::Gloss::Termenv
|
|
9
|
+
# Profile::TRUE_COLOR / ANSI256 / ANSI / ASCII termenv.Profile values (frozen Profile objects)
|
|
10
|
+
# Profile.resolve(:true_color | :ansi256 | :ansi | :ascii | Profile) -> Profile
|
|
11
|
+
# Profile#color(str) -> termenv color or nil (Profile.Color: "#hex", "0".."255", "" -> nil)
|
|
12
|
+
# Profile#convert(c) -> color or nil (Profile.Convert: degrade to the profile)
|
|
13
|
+
# Profile#string -> Style (Profile.String(): a Style for this profile)
|
|
14
|
+
# NoColor.new / ANSIColor.new(n) / ANSI256Color.new(n) / RGBColor.new("#hex")
|
|
15
|
+
# #sequence(bg = false) -> "31", "38;5;123", "48;2;1;2;3", "" (Color.Sequence)
|
|
16
|
+
# Termenv.convert_to_rgb(c) -> Colorful::Color (ConvertToRGB)
|
|
17
|
+
# Style.new(profile = Profile::TRUE_COLOR) (termenv.Style; Style{} is TrueColor in Go)
|
|
18
|
+
# #bold #faint #italic #underline #overline #blink #reverse #cross_out -> new Style
|
|
19
|
+
# #foreground(color_or_nil) #background(color_or_nil) -> new Style (nil: no-op)
|
|
20
|
+
# #styled(str) -> "\e[<seq>m" + str + "\e[0m", or str for ASCII / no styles / empty sequence
|
|
21
|
+
# Output.new(io: STDOUT, env: ENV) termenv.Output: color_profile, env_color_profile,
|
|
22
|
+
# env_no_color?, tty?, background_color,
|
|
23
|
+
# has_dark_background?
|
|
24
|
+
#
|
|
25
|
+
# R2UI::Compat::Gloss::Renderer lipgloss.Renderer; the default one is global
|
|
26
|
+
# Renderer.default the renderer Lipgloss uses
|
|
27
|
+
# Renderer.color_profile -> Termenv::Profile detected lazily, once (EnvColorProfile on stdout)
|
|
28
|
+
# Renderer.has_dark_background? -> bool detected lazily, once (OSC 11 query on a TTY)
|
|
29
|
+
# Renderer.terminal_color(value, adaptive: true, complete: true, strict: true)
|
|
30
|
+
# Ruby color value -> TerminalColor, validated like
|
|
31
|
+
# the C extension (raises the same TypeErrors)
|
|
32
|
+
# Renderer.color(value) -> termenv color or nil terminal_color(value).color(Renderer.default)
|
|
33
|
+
# Test hooks (r2ui only): Renderer.color_profile = :true_color | :ansi256 | :ansi | :ascii | nil,
|
|
34
|
+
# Renderer.has_dark_background = true | false | nil, Renderer.reset! (nil re-enables detection)
|
|
35
|
+
#
|
|
36
|
+
# R2UI::Compat::Gloss::TerminalColor lipgloss TerminalColor implementations
|
|
37
|
+
# Color.new(str), NoColor.new, AdaptiveColor.new(light, dark),
|
|
38
|
+
# CompleteColor.new(true_color, ansi256, ansi), CompleteAdaptiveColor.new(light_cc, dark_cc)
|
|
39
|
+
# #color(renderer = Renderer.default) -> termenv color or nil; #rgba -> [r, g, b, a] (16-bit)
|
|
40
|
+
#
|
|
41
|
+
# Typical render (lipgloss style.go):
|
|
42
|
+
# r = Renderer.default
|
|
43
|
+
# te = r.color_profile.string.bold.foreground(fg.color(r)).background(bg.color(r))
|
|
44
|
+
# te.styled("text")
|
|
45
|
+
module R2UI
|
|
46
|
+
module Compat
|
|
47
|
+
module Gloss
|
|
48
|
+
module Termenv
|
|
49
|
+
ESC = "\e"
|
|
50
|
+
BEL = "\a"
|
|
51
|
+
CSI = "#{ESC}[".freeze
|
|
52
|
+
OSC = "#{ESC}]".freeze
|
|
53
|
+
ST = "#{ESC}\\".freeze
|
|
54
|
+
|
|
55
|
+
RESET_SEQ = "0"
|
|
56
|
+
BOLD_SEQ = "1"
|
|
57
|
+
FAINT_SEQ = "2"
|
|
58
|
+
ITALIC_SEQ = "3"
|
|
59
|
+
UNDERLINE_SEQ = "4"
|
|
60
|
+
BLINK_SEQ = "5"
|
|
61
|
+
REVERSE_SEQ = "7"
|
|
62
|
+
CROSS_OUT_SEQ = "9"
|
|
63
|
+
OVERLINE_SEQ = "53"
|
|
64
|
+
|
|
65
|
+
FOREGROUND = "38"
|
|
66
|
+
BACKGROUND = "48"
|
|
67
|
+
|
|
68
|
+
# termenv ansiHex: the 16 ANSI colors, the 6x6x6 cube and the 24 grays.
|
|
69
|
+
ANSI_HEX = begin
|
|
70
|
+
base = %w[#000000 #800000 #008000 #808000 #000080 #800080 #008080 #c0c0c0
|
|
71
|
+
#808080 #ff0000 #00ff00 #ffff00 #0000ff #ff00ff #00ffff #ffffff]
|
|
72
|
+
levels = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
|
|
73
|
+
cube = (0...216).map { |i| format("#%02x%02x%02x", levels[i / 36], levels[(i / 6) % 6], levels[i % 6]) }
|
|
74
|
+
grays = (0...24).map { |i| format("#%02x%02x%02x", 8 + 10 * i, 8 + 10 * i, 8 + 10 * i) }
|
|
75
|
+
(base + cube + grays).map(&:freeze).freeze
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class Error < StandardError; end
|
|
79
|
+
|
|
80
|
+
# termenv.NoColor: no sequence at all (but still appended to a Style as "").
|
|
81
|
+
class NoColor
|
|
82
|
+
def sequence(_bg = false)
|
|
83
|
+
""
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def to_s
|
|
87
|
+
""
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def ==(other)
|
|
91
|
+
other.is_a?(NoColor)
|
|
92
|
+
end
|
|
93
|
+
alias eql? ==
|
|
94
|
+
|
|
95
|
+
def hash
|
|
96
|
+
NoColor.hash
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# termenv.ANSIColor (0-15; other values still produce a sequence, as in Go).
|
|
101
|
+
ANSIColor = Struct.new(:value) do
|
|
102
|
+
def sequence(bg = false)
|
|
103
|
+
col = value
|
|
104
|
+
bg_mod = ->(c) { bg ? c + 10 : c }
|
|
105
|
+
return (bg_mod.call(col) + 30).to_s if col < 8
|
|
106
|
+
|
|
107
|
+
(bg_mod.call(col - 8) + 90).to_s
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def to_s
|
|
111
|
+
ANSI_HEX.fetch(value, "")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# termenv.ANSI256Color (16-255).
|
|
116
|
+
ANSI256Color = Struct.new(:value) do
|
|
117
|
+
def sequence(bg = false)
|
|
118
|
+
"#{bg ? BACKGROUND : FOREGROUND};5;#{value}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def to_s
|
|
122
|
+
ANSI_HEX.fetch(value, "")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# termenv.RGBColor: a "#rrggbb" (or "#rgb") string.
|
|
127
|
+
RGBColor = Struct.new(:value) do
|
|
128
|
+
def sequence(bg = false)
|
|
129
|
+
f = Colorful.hex(value)
|
|
130
|
+
return "" if f.nil?
|
|
131
|
+
|
|
132
|
+
channels = [f.r, f.g, f.b].map { |v| Colorful.go_uint8(v * 255) }
|
|
133
|
+
"#{bg ? BACKGROUND : FOREGROUND};2;#{channels.join(';')}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def to_s
|
|
137
|
+
value
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
module_function
|
|
142
|
+
|
|
143
|
+
# termenv.ConvertToRGB. Unknown colors (and Go's out-of-range palette indexes, which panic in
|
|
144
|
+
# Go) come back as black, like colorful.Hex("") does.
|
|
145
|
+
def convert_to_rgb(color)
|
|
146
|
+
hex = case color
|
|
147
|
+
when RGBColor then color.value
|
|
148
|
+
when ANSIColor, ANSI256Color
|
|
149
|
+
color.value.between?(0, 255) ? ANSI_HEX[color.value] : ""
|
|
150
|
+
else ""
|
|
151
|
+
end
|
|
152
|
+
Colorful.hex(hex) || Colorful::Color.new(0.0, 0.0, 0.0)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def ansi256_to_ansi_color(color)
|
|
156
|
+
h = Colorful.hex(ANSI_HEX.fetch(color.value)) # Go panics on an out-of-range index
|
|
157
|
+
best = 0
|
|
158
|
+
md = Float::MAX
|
|
159
|
+
16.times do |i|
|
|
160
|
+
d = h.distance_hsluv(Colorful.hex(ANSI_HEX[i]))
|
|
161
|
+
if d < md
|
|
162
|
+
md = d
|
|
163
|
+
best = i
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
ANSIColor.new(best)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def hex_to_ansi256_color(c)
|
|
170
|
+
v2ci = lambda do |v|
|
|
171
|
+
if v < 48 then 0
|
|
172
|
+
elsif v < 115 then 1
|
|
173
|
+
else ((v - 35) / 40).to_i
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
r = v2ci.call(c.r * 255.0)
|
|
177
|
+
g = v2ci.call(c.g * 255.0)
|
|
178
|
+
b = v2ci.call(c.b * 255.0)
|
|
179
|
+
ci = 36 * r + 6 * g + b
|
|
180
|
+
i2cv = [0, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
|
|
181
|
+
cr = i2cv[r]
|
|
182
|
+
cg = i2cv[g]
|
|
183
|
+
cb = i2cv[b]
|
|
184
|
+
|
|
185
|
+
average = (r + g + b) / 3
|
|
186
|
+
gray_idx = if average > 238
|
|
187
|
+
23
|
|
188
|
+
else
|
|
189
|
+
(average - 3).fdiv(10).truncate # Go integer division truncates toward zero
|
|
190
|
+
end
|
|
191
|
+
gv = 8 + 10 * gray_idx
|
|
192
|
+
|
|
193
|
+
c2 = Colorful::Color.new(cr / 255.0, cg / 255.0, cb / 255.0)
|
|
194
|
+
g2 = Colorful::Color.new(gv / 255.0, gv / 255.0, gv / 255.0)
|
|
195
|
+
return ANSI256Color.new(16 + ci) if c.distance_hsluv(c2) <= c.distance_hsluv(g2)
|
|
196
|
+
|
|
197
|
+
ANSI256Color.new(232 + gray_idx)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# strconv.Atoi: optional sign, decimal digits, must fit in int64.
|
|
201
|
+
def atoi(str)
|
|
202
|
+
return nil unless str.match?(/\A[+-]?[0-9]+\z/)
|
|
203
|
+
|
|
204
|
+
i = str.to_i
|
|
205
|
+
i.between?(-2**63, 2**63 - 1) ? i : nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# termenv.Profile.
|
|
209
|
+
class Profile
|
|
210
|
+
attr_reader :value, :name
|
|
211
|
+
|
|
212
|
+
CACHE_LIMIT = 4096
|
|
213
|
+
|
|
214
|
+
def initialize(value, name, sym)
|
|
215
|
+
@value = value
|
|
216
|
+
@name = name
|
|
217
|
+
@sym = sym
|
|
218
|
+
@cache = {} # Profile#color results; degrading to ANSI costs ~2ms per color
|
|
219
|
+
freeze
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def to_sym
|
|
223
|
+
@sym
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def inspect
|
|
227
|
+
"#<Termenv::Profile #{@name}>"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
TRUE_COLOR = new(0, "TrueColor", :true_color)
|
|
231
|
+
ANSI256 = new(1, "ANSI256", :ansi256)
|
|
232
|
+
ANSI = new(2, "ANSI", :ansi)
|
|
233
|
+
ASCII = new(3, "Ascii", :ascii)
|
|
234
|
+
ALL = [TRUE_COLOR, ANSI256, ANSI, ASCII].freeze
|
|
235
|
+
|
|
236
|
+
def self.resolve(profile)
|
|
237
|
+
return profile if profile.is_a?(Profile)
|
|
238
|
+
|
|
239
|
+
ALL.find { |p| p.to_sym == profile } ||
|
|
240
|
+
raise(ArgumentError, "unknown color profile #{profile.inspect} (use :true_color, :ansi256, :ansi or :ascii)")
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Profile.String: a new Style for this profile.
|
|
244
|
+
def string
|
|
245
|
+
Style.new(self)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Profile.Convert. Returns nil for an unparsable RGB color, as Go does.
|
|
249
|
+
def convert(color)
|
|
250
|
+
return NoColor.new if equal?(ASCII)
|
|
251
|
+
|
|
252
|
+
case color
|
|
253
|
+
when ANSIColor then color
|
|
254
|
+
when ANSI256Color
|
|
255
|
+
equal?(ANSI) ? Termenv.ansi256_to_ansi_color(color) : color
|
|
256
|
+
when RGBColor
|
|
257
|
+
h = Colorful.hex(color.value)
|
|
258
|
+
return nil if h.nil?
|
|
259
|
+
return color if equal?(TRUE_COLOR)
|
|
260
|
+
|
|
261
|
+
ac = Termenv.hex_to_ansi256_color(h)
|
|
262
|
+
equal?(ANSI) ? Termenv.ansi256_to_ansi_color(ac) : ac
|
|
263
|
+
else color
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Profile.Color: "#..." is RGB, an integer below 16 is ANSI, any other integer is ANSI256,
|
|
268
|
+
# anything else (including "") is nil.
|
|
269
|
+
def color(str)
|
|
270
|
+
str = str.to_s
|
|
271
|
+
return nil if str.empty?
|
|
272
|
+
return @cache[str] if @cache.key?(str)
|
|
273
|
+
|
|
274
|
+
c = if str.start_with?("#")
|
|
275
|
+
RGBColor.new(str.dup.freeze)
|
|
276
|
+
else
|
|
277
|
+
i = Termenv.atoi(str)
|
|
278
|
+
i.nil? ? nil : (i < 16 ? ANSIColor.new(i) : ANSI256Color.new(i))
|
|
279
|
+
end
|
|
280
|
+
c = convert(c)&.freeze unless c.nil?
|
|
281
|
+
@cache.clear if @cache.size >= CACHE_LIMIT
|
|
282
|
+
@cache[str.dup.freeze] = c
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# termenv.Style: an immutable list of SGR parameters for one profile.
|
|
287
|
+
class Style
|
|
288
|
+
attr_reader :profile, :styles
|
|
289
|
+
|
|
290
|
+
def initialize(profile = Profile::TRUE_COLOR, styles = [])
|
|
291
|
+
@profile = Profile.resolve(profile)
|
|
292
|
+
@styles = styles.dup.freeze
|
|
293
|
+
freeze
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def styled(str)
|
|
297
|
+
return str if @profile.equal?(Profile::ASCII)
|
|
298
|
+
return str if @styles.empty?
|
|
299
|
+
|
|
300
|
+
seq = @styles.join(";")
|
|
301
|
+
return str if seq.empty?
|
|
302
|
+
|
|
303
|
+
"#{CSI}#{seq}m#{str}#{CSI}#{RESET_SEQ}m"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def foreground(color)
|
|
307
|
+
color.nil? ? self : with(color.sequence(false))
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def background(color)
|
|
311
|
+
color.nil? ? self : with(color.sequence(true))
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def bold = with(BOLD_SEQ)
|
|
315
|
+
def faint = with(FAINT_SEQ)
|
|
316
|
+
def italic = with(ITALIC_SEQ)
|
|
317
|
+
def underline = with(UNDERLINE_SEQ)
|
|
318
|
+
def overline = with(OVERLINE_SEQ)
|
|
319
|
+
def blink = with(BLINK_SEQ)
|
|
320
|
+
def reverse = with(REVERSE_SEQ)
|
|
321
|
+
def cross_out = with(CROSS_OUT_SEQ)
|
|
322
|
+
|
|
323
|
+
private
|
|
324
|
+
|
|
325
|
+
def with(seq)
|
|
326
|
+
Style.new(@profile, @styles + [seq])
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# termenv.Output for a file (stdout by default): color profile and background detection as
|
|
331
|
+
# termenv_unix.go does it.
|
|
332
|
+
class Output
|
|
333
|
+
OSC_TIMEOUT = 5.0 # seconds per byte, as termenv's OSCTimeout
|
|
334
|
+
|
|
335
|
+
# TIOCGPGRP, for termenv's isForeground check.
|
|
336
|
+
TIOCGPGRP = case RUBY_PLATFORM
|
|
337
|
+
when /darwin|bsd/ then 0x40047477
|
|
338
|
+
when /linux/ then 0x540F
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
attr_reader :io, :env
|
|
342
|
+
|
|
343
|
+
# io: the terminal output (termenv uses os.Stdout).
|
|
344
|
+
# env: anything with #[] (ENV by default).
|
|
345
|
+
# assume_tty: true forces the TTY checks to pass (termenv WithTTY), nil asks the io.
|
|
346
|
+
# osc_timeout: seconds to wait for each byte of a terminal reply.
|
|
347
|
+
def initialize(io: STDOUT, env: ENV, assume_tty: nil, osc_timeout: OSC_TIMEOUT)
|
|
348
|
+
@io = io
|
|
349
|
+
@env = env
|
|
350
|
+
@assume_tty = assume_tty
|
|
351
|
+
@osc_timeout = osc_timeout
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def getenv(key)
|
|
355
|
+
@env[key].to_s
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def tty?
|
|
359
|
+
return true if @assume_tty
|
|
360
|
+
return false unless getenv("CI").empty?
|
|
361
|
+
|
|
362
|
+
@io.respond_to?(:tty?) && @io.tty?
|
|
363
|
+
rescue IOError
|
|
364
|
+
false
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def env_no_color?
|
|
368
|
+
!getenv("NO_COLOR").empty? || (getenv("CLICOLOR") == "0" && !cli_color_forced?)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def env_color_profile
|
|
372
|
+
return Profile::ASCII if env_no_color?
|
|
373
|
+
|
|
374
|
+
p = color_profile
|
|
375
|
+
return Profile::ANSI if cli_color_forced? && p.equal?(Profile::ASCII)
|
|
376
|
+
|
|
377
|
+
p
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def cli_color_forced?
|
|
381
|
+
forced = getenv("CLICOLOR_FORCE")
|
|
382
|
+
!forced.empty? && forced != "0"
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def color_profile
|
|
386
|
+
return Profile::ASCII unless tty?
|
|
387
|
+
return Profile::TRUE_COLOR if getenv("GOOGLE_CLOUD_SHELL") == "true"
|
|
388
|
+
|
|
389
|
+
term = getenv("TERM")
|
|
390
|
+
case getenv("COLORTERM").downcase
|
|
391
|
+
when "24bit", "truecolor"
|
|
392
|
+
return Profile::ANSI256 if term.start_with?("screen") && getenv("TERM_PROGRAM") != "tmux"
|
|
393
|
+
|
|
394
|
+
return Profile::TRUE_COLOR
|
|
395
|
+
when "yes", "true"
|
|
396
|
+
return Profile::ANSI256
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
case term
|
|
400
|
+
when "alacritty", "contour", "rio", "wezterm", "xterm-ghostty", "xterm-kitty"
|
|
401
|
+
return Profile::TRUE_COLOR
|
|
402
|
+
when "linux", "xterm"
|
|
403
|
+
return Profile::ANSI
|
|
404
|
+
end
|
|
405
|
+
return Profile::ANSI256 if term.include?("256color")
|
|
406
|
+
return Profile::ANSI if term.include?("color") || term.include?("ansi")
|
|
407
|
+
|
|
408
|
+
Profile::ASCII
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Output.BackgroundColor (uncached; lipgloss's Renderer caches the dark/light answer).
|
|
412
|
+
def background_color
|
|
413
|
+
return NoColor.new unless tty?
|
|
414
|
+
|
|
415
|
+
begin
|
|
416
|
+
s = term_status_report(11)
|
|
417
|
+
c = xterm_color(s)
|
|
418
|
+
return c if c
|
|
419
|
+
rescue Error, SystemCallError, IOError
|
|
420
|
+
nil
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
colorfgbg = getenv("COLORFGBG")
|
|
424
|
+
if colorfgbg.include?(";")
|
|
425
|
+
i = Termenv.atoi(colorfgbg.split(";", -1).last)
|
|
426
|
+
return ANSIColor.new(i) if i
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
ANSIColor.new(0)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def has_dark_background?
|
|
433
|
+
_h, _s, l = Termenv.convert_to_rgb(background_color).hsl
|
|
434
|
+
l < 0.5
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# termenv xTermColor: "\e]11;rgb:rrrr/gggg/bbbb" + BEL / ESC / ST -> RGBColor, or nil.
|
|
438
|
+
def xterm_color(str)
|
|
439
|
+
return nil if str.bytesize < 24 || str.bytesize > 25
|
|
440
|
+
|
|
441
|
+
if str.end_with?(BEL) then str = str.delete_suffix(BEL)
|
|
442
|
+
elsif str.end_with?(ESC) then str = str.delete_suffix(ESC)
|
|
443
|
+
elsif str.end_with?(ST) then str = str.delete_suffix(ST)
|
|
444
|
+
else return nil
|
|
445
|
+
end
|
|
446
|
+
str = str.byteslice(4..)
|
|
447
|
+
return nil unless str.start_with?(";rgb:")
|
|
448
|
+
|
|
449
|
+
parts = str.delete_prefix(";rgb:").split("/", -1)
|
|
450
|
+
return nil if parts.size < 3 || parts[0, 3].any? { |p| p.bytesize < 2 } # Go would panic
|
|
451
|
+
|
|
452
|
+
RGBColor.new("##{parts[0].byteslice(0, 2)}#{parts[1].byteslice(0, 2)}#{parts[2].byteslice(0, 2)}")
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# termStatusReport: query the terminal with OSC <sequence>;? followed by a cursor position
|
|
456
|
+
# request (which every terminal answers), with echo and canonical mode off. The terminal
|
|
457
|
+
# mode is restored on every exit path.
|
|
458
|
+
def term_status_report(sequence)
|
|
459
|
+
term = getenv("TERM")
|
|
460
|
+
raise Error, "unable to retrieve status report" if term.start_with?("screen", "tmux", "dumb")
|
|
461
|
+
|
|
462
|
+
tty = tty_io
|
|
463
|
+
raise Error, "unable to retrieve status report" if tty.nil?
|
|
464
|
+
raise Error, "unable to retrieve status report" unless foreground?(tty)
|
|
465
|
+
|
|
466
|
+
require "io/console"
|
|
467
|
+
saved = tty.console_mode
|
|
468
|
+
begin
|
|
469
|
+
mode = saved.dup
|
|
470
|
+
mode.raw!(intr: true)
|
|
471
|
+
tty.console_mode = mode
|
|
472
|
+
|
|
473
|
+
tty.syswrite("#{OSC}#{sequence};?#{ST}")
|
|
474
|
+
tty.syswrite("#{CSI}6n")
|
|
475
|
+
|
|
476
|
+
res, is_osc = read_next_response(tty)
|
|
477
|
+
raise Error, "unable to retrieve status report" unless is_osc
|
|
478
|
+
|
|
479
|
+
read_next_response(tty) # the cursor position reply, discarded
|
|
480
|
+
res
|
|
481
|
+
ensure
|
|
482
|
+
tty.console_mode = saved
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
private
|
|
487
|
+
|
|
488
|
+
# A read/write handle on the output's file descriptor (Go reads replies from os.Stdout).
|
|
489
|
+
def tty_io
|
|
490
|
+
return @io if @io.respond_to?(:sysread) && !@io.equal?(STDOUT) && !@io.equal?($stdout)
|
|
491
|
+
return nil unless @io.respond_to?(:fileno)
|
|
492
|
+
|
|
493
|
+
@tty_io ||= IO.new(@io.fileno, "r+", autoclose: false)
|
|
494
|
+
rescue SystemCallError, IOError, ArgumentError
|
|
495
|
+
nil
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def foreground?(tty)
|
|
499
|
+
return false if TIOCGPGRP.nil?
|
|
500
|
+
|
|
501
|
+
buf = [0].pack("l")
|
|
502
|
+
tty.ioctl(TIOCGPGRP, buf)
|
|
503
|
+
buf.unpack1("l") == Process.getpgrp
|
|
504
|
+
rescue SystemCallError, IOError, NotImplementedError
|
|
505
|
+
false
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def read_next_byte(tty)
|
|
509
|
+
raise Error, "timeout" if IO.select([tty], nil, nil, @osc_timeout).nil?
|
|
510
|
+
|
|
511
|
+
tty.sysread(1)
|
|
512
|
+
rescue EOFError
|
|
513
|
+
raise Error, "EOF"
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# readNextResponse: an OSC reply (ends in BEL or ESC) or a cursor position reply (ends in R).
|
|
517
|
+
def read_next_response(tty)
|
|
518
|
+
start = read_next_byte(tty)
|
|
519
|
+
start = read_next_byte(tty) while start != ESC
|
|
520
|
+
response = +start
|
|
521
|
+
|
|
522
|
+
tpe = read_next_byte(tty)
|
|
523
|
+
response << tpe
|
|
524
|
+
osc = case tpe
|
|
525
|
+
when "[" then false
|
|
526
|
+
when "]" then true
|
|
527
|
+
else raise Error, "unable to retrieve status report"
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
loop do
|
|
531
|
+
b = read_next_byte(tty)
|
|
532
|
+
response << b
|
|
533
|
+
if osc
|
|
534
|
+
return [response.force_encoding(Encoding::BINARY), true] if b == BEL || response.end_with?(ESC)
|
|
535
|
+
elsif b == "R"
|
|
536
|
+
return [response.force_encoding(Encoding::BINARY), false]
|
|
537
|
+
end
|
|
538
|
+
break if response.bytesize > 25
|
|
539
|
+
end
|
|
540
|
+
raise Error, "unable to retrieve status report"
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# lipgloss TerminalColor implementations (color.go). Each resolves against a Renderer at render
|
|
546
|
+
# time, like Go's TerminalColor.color(*Renderer).
|
|
547
|
+
module TerminalColor
|
|
548
|
+
# lipgloss.NoColor
|
|
549
|
+
class NoColor
|
|
550
|
+
def color(_renderer = Renderer.default)
|
|
551
|
+
Termenv::NoColor.new
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def rgba
|
|
555
|
+
[0x0, 0x0, 0x0, 0xFFFF]
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def ==(other)
|
|
559
|
+
other.is_a?(NoColor)
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
module RGBA
|
|
564
|
+
def rgba(renderer = Renderer.default)
|
|
565
|
+
c = Termenv.convert_to_rgb(color(renderer))
|
|
566
|
+
conv = ->(v) { [[(v * 65_535.0 + 0.5).to_i, 0].max, 0xFFFF_FFFF].min }
|
|
567
|
+
[conv.call(c.r), conv.call(c.g), conv.call(c.b), 0xFFFF]
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# lipgloss.Color: hex ("#ff8000", "#f80") or ANSI ("0".."255"); "" means no color.
|
|
572
|
+
Color = Struct.new(:value) do
|
|
573
|
+
include RGBA
|
|
574
|
+
|
|
575
|
+
def color(renderer = Renderer.default)
|
|
576
|
+
renderer.color_profile.color(value)
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
# lipgloss.AdaptiveColor
|
|
581
|
+
AdaptiveColor = Struct.new(:light, :dark) do
|
|
582
|
+
include RGBA
|
|
583
|
+
|
|
584
|
+
def color(renderer = Renderer.default)
|
|
585
|
+
Color.new(renderer.has_dark_background? ? dark : light).color(renderer)
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# lipgloss.CompleteColor: exact values per profile, no degradation.
|
|
590
|
+
CompleteColor = Struct.new(:true_color, :ansi256, :ansi) do
|
|
591
|
+
include RGBA
|
|
592
|
+
|
|
593
|
+
def color(renderer = Renderer.default)
|
|
594
|
+
p = renderer.color_profile
|
|
595
|
+
case p
|
|
596
|
+
when Termenv::Profile::TRUE_COLOR then p.color(true_color)
|
|
597
|
+
when Termenv::Profile::ANSI256 then p.color(ansi256)
|
|
598
|
+
when Termenv::Profile::ANSI then p.color(ansi)
|
|
599
|
+
else Termenv::NoColor.new
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# lipgloss.CompleteAdaptiveColor
|
|
605
|
+
CompleteAdaptiveColor = Struct.new(:light, :dark) do
|
|
606
|
+
include RGBA
|
|
607
|
+
|
|
608
|
+
def color(renderer = Renderer.default)
|
|
609
|
+
(renderer.has_dark_background? ? dark : light).color(renderer)
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# lipgloss.Renderer: caches the color profile and the dark-background answer, each detected
|
|
615
|
+
# once on first use (Go's sync.Once), unless set explicitly.
|
|
616
|
+
class Renderer
|
|
617
|
+
attr_reader :output
|
|
618
|
+
|
|
619
|
+
def initialize(output = Termenv::Output.new)
|
|
620
|
+
@output = output
|
|
621
|
+
@mutex = Mutex.new
|
|
622
|
+
reset!
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def color_profile
|
|
626
|
+
@mutex.synchronize do
|
|
627
|
+
@color_profile ||= @explicit_profile || @output.env_color_profile
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# nil re-enables detection (r2ui test hook; Go's SetColorProfile has no reset).
|
|
632
|
+
def color_profile=(profile)
|
|
633
|
+
@mutex.synchronize do
|
|
634
|
+
@explicit_profile = profile.nil? ? nil : Termenv::Profile.resolve(profile)
|
|
635
|
+
@color_profile = @explicit_profile
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
# Detection queries the terminal (up to seconds), so it runs outside the lock: other threads
|
|
640
|
+
# asking for the profile or background aren't blocked. The first answer stored wins; one
|
|
641
|
+
# started before a setter or reset! is used by its caller but not stored.
|
|
642
|
+
def has_dark_background?
|
|
643
|
+
generation = @mutex.synchronize do
|
|
644
|
+
return @explicit_dark unless @explicit_dark.nil?
|
|
645
|
+
return @dark unless @dark.nil?
|
|
646
|
+
|
|
647
|
+
@generation
|
|
648
|
+
end
|
|
649
|
+
detected = @output.has_dark_background?
|
|
650
|
+
@mutex.synchronize do
|
|
651
|
+
return @explicit_dark unless @explicit_dark.nil?
|
|
652
|
+
|
|
653
|
+
@dark = detected if @dark.nil? && @generation == generation
|
|
654
|
+
@dark.nil? ? detected : @dark
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# nil re-enables detection (r2ui test hook).
|
|
659
|
+
def has_dark_background=(value)
|
|
660
|
+
@mutex.synchronize do
|
|
661
|
+
@explicit_dark = value.nil? ? nil : (value ? true : false)
|
|
662
|
+
@dark = nil
|
|
663
|
+
@generation += 1
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def reset!
|
|
668
|
+
@mutex.synchronize do
|
|
669
|
+
@explicit_profile = nil
|
|
670
|
+
@color_profile = nil
|
|
671
|
+
@explicit_dark = nil
|
|
672
|
+
@dark = nil
|
|
673
|
+
@generation = (@generation || 0) + 1
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
class << self
|
|
678
|
+
def default
|
|
679
|
+
@default ||= new
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
attr_writer :default
|
|
683
|
+
|
|
684
|
+
def color_profile = default.color_profile
|
|
685
|
+
def color_profile=(profile)
|
|
686
|
+
default.color_profile = profile
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def has_dark_background? = default.has_dark_background?
|
|
690
|
+
def has_dark_background=(value)
|
|
691
|
+
default.has_dark_background = value
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def reset! = default.reset!
|
|
695
|
+
|
|
696
|
+
# The Ruby color value -> TerminalColor conversion the C extension does (style.c
|
|
697
|
+
# style_foreground / style_background):
|
|
698
|
+
# * responds to #light and #dark: if both answers respond to #true_color, #ansi256 and
|
|
699
|
+
# #ansi it is a CompleteAdaptiveColor, otherwise an AdaptiveColor of two strings;
|
|
700
|
+
# * responds to #true_color, #ansi256 and #ansi: CompleteColor;
|
|
701
|
+
# * otherwise it must be a String (Check_Type).
|
|
702
|
+
# Strings inside composite colors go through StringValueCStr (implicit #to_str, no NUL).
|
|
703
|
+
# Other setters accept less: border colors are adaptive: true, complete: false; margin
|
|
704
|
+
# background is adaptive: false, complete: false; place's whitespace foreground is
|
|
705
|
+
# complete: false, strict: false (StringValueCStr instead of Check_Type).
|
|
706
|
+
def terminal_color(value, adaptive: true, complete: true, strict: true)
|
|
707
|
+
if adaptive && adaptive_color?(value)
|
|
708
|
+
light = value.light
|
|
709
|
+
dark = value.dark
|
|
710
|
+
if complete && complete_color?(light) && complete_color?(dark)
|
|
711
|
+
return TerminalColor::CompleteAdaptiveColor.new(complete_from(light), complete_from(dark))
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
return TerminalColor::AdaptiveColor.new(c_string(light), c_string(dark))
|
|
715
|
+
end
|
|
716
|
+
return complete_from(value) if complete && complete_color?(value)
|
|
717
|
+
|
|
718
|
+
check_string(value) if strict
|
|
719
|
+
TerminalColor::Color.new(c_string(value))
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def color(value, renderer = default)
|
|
723
|
+
terminal_color(value).color(renderer)
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def adaptive_color?(value)
|
|
727
|
+
value.respond_to?(:light) && value.respond_to?(:dark)
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
def complete_color?(value)
|
|
731
|
+
value.respond_to?(:true_color) && value.respond_to?(:ansi256) && value.respond_to?(:ansi)
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
private
|
|
735
|
+
|
|
736
|
+
def complete_from(value)
|
|
737
|
+
true_color = value.true_color
|
|
738
|
+
ansi256 = value.ansi256
|
|
739
|
+
ansi = value.ansi
|
|
740
|
+
TerminalColor::CompleteColor.new(c_string(true_color), c_string(ansi256), c_string(ansi))
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def check_string(value)
|
|
744
|
+
return if value.is_a?(String)
|
|
745
|
+
|
|
746
|
+
raise TypeError, "wrong argument type #{type_name(value)} (expected String)"
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
# StringValueCStr: implicit #to_str conversion, and no NUL bytes.
|
|
750
|
+
def c_string(value)
|
|
751
|
+
str = String.try_convert(value)
|
|
752
|
+
raise TypeError, "no implicit conversion of #{type_name(value)} into String" if str.nil?
|
|
753
|
+
raise ArgumentError, "string contains null byte" if str.include?("\0")
|
|
754
|
+
|
|
755
|
+
str.dup.freeze
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
def type_name(value)
|
|
759
|
+
case value
|
|
760
|
+
when nil, true, false then value.inspect
|
|
761
|
+
else value.class.to_s
|
|
762
|
+
end
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
end
|