rich-ruby 1.0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +546 -0
- data/examples/demo.rb +106 -0
- data/examples/showcase.rb +420 -0
- data/examples/smoke_test.rb +41 -0
- data/examples/stress_test.rb +604 -0
- data/examples/syntax_markdown_demo.rb +166 -0
- data/examples/verify.rb +215 -0
- data/examples/visual_demo.rb +145 -0
- data/lib/rich/_palettes.rb +148 -0
- data/lib/rich/box.rb +342 -0
- data/lib/rich/cells.rb +512 -0
- data/lib/rich/color.rb +628 -0
- data/lib/rich/color_triplet.rb +220 -0
- data/lib/rich/console.rb +549 -0
- data/lib/rich/control.rb +332 -0
- data/lib/rich/json.rb +254 -0
- data/lib/rich/layout.rb +314 -0
- data/lib/rich/markdown.rb +509 -0
- data/lib/rich/markup.rb +175 -0
- data/lib/rich/panel.rb +311 -0
- data/lib/rich/progress.rb +430 -0
- data/lib/rich/segment.rb +387 -0
- data/lib/rich/style.rb +433 -0
- data/lib/rich/syntax.rb +1145 -0
- data/lib/rich/table.rb +525 -0
- data/lib/rich/terminal_theme.rb +126 -0
- data/lib/rich/text.rb +433 -0
- data/lib/rich/tree.rb +220 -0
- data/lib/rich/version.rb +5 -0
- data/lib/rich/win32_console.rb +582 -0
- data/lib/rich.rb +108 -0
- metadata +106 -0
data/lib/rich/color.rb
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "color_triplet"
|
|
4
|
+
require_relative "_palettes"
|
|
5
|
+
|
|
6
|
+
module Rich
|
|
7
|
+
# Color systems supported by terminals
|
|
8
|
+
module ColorSystem
|
|
9
|
+
STANDARD = :standard # 16 colors (4-bit)
|
|
10
|
+
EIGHT_BIT = :eight_bit # 256 colors (8-bit)
|
|
11
|
+
TRUECOLOR = :truecolor # 16 million colors (24-bit)
|
|
12
|
+
WINDOWS = :windows # Windows Console legacy colors
|
|
13
|
+
|
|
14
|
+
ALL = [STANDARD, EIGHT_BIT, TRUECOLOR, WINDOWS].freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# @param system [Symbol] Color system
|
|
18
|
+
# @return [Boolean] Whether the color system is valid
|
|
19
|
+
def valid?(system)
|
|
20
|
+
ALL.include?(system)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Types of color values
|
|
26
|
+
module ColorType
|
|
27
|
+
DEFAULT = :default # Terminal default color
|
|
28
|
+
STANDARD = :standard # 16 ANSI colors (0-15)
|
|
29
|
+
EIGHT_BIT = :eight_bit # 256 colors (0-255)
|
|
30
|
+
TRUECOLOR = :truecolor # RGB color
|
|
31
|
+
WINDOWS = :windows # Windows console color
|
|
32
|
+
|
|
33
|
+
ALL = [DEFAULT, STANDARD, EIGHT_BIT, TRUECOLOR, WINDOWS].freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# ANSI color name to number mapping
|
|
37
|
+
ANSI_COLOR_NAMES = {
|
|
38
|
+
"black" => 0,
|
|
39
|
+
"red" => 1,
|
|
40
|
+
"green" => 2,
|
|
41
|
+
"yellow" => 3,
|
|
42
|
+
"blue" => 4,
|
|
43
|
+
"magenta" => 5,
|
|
44
|
+
"cyan" => 6,
|
|
45
|
+
"white" => 7,
|
|
46
|
+
"bright_black" => 8,
|
|
47
|
+
"bright_red" => 9,
|
|
48
|
+
"bright_green" => 10,
|
|
49
|
+
"bright_yellow" => 11,
|
|
50
|
+
"bright_blue" => 12,
|
|
51
|
+
"bright_magenta" => 13,
|
|
52
|
+
"bright_cyan" => 14,
|
|
53
|
+
"bright_white" => 15,
|
|
54
|
+
"grey0" => 16,
|
|
55
|
+
"gray0" => 16,
|
|
56
|
+
"navy_blue" => 17,
|
|
57
|
+
"dark_blue" => 18,
|
|
58
|
+
"blue3" => 20,
|
|
59
|
+
"blue1" => 21,
|
|
60
|
+
"dark_green" => 22,
|
|
61
|
+
"deep_sky_blue4" => 25,
|
|
62
|
+
"dodger_blue3" => 26,
|
|
63
|
+
"dodger_blue2" => 27,
|
|
64
|
+
"green4" => 28,
|
|
65
|
+
"spring_green4" => 29,
|
|
66
|
+
"turquoise4" => 30,
|
|
67
|
+
"deep_sky_blue3" => 32,
|
|
68
|
+
"dodger_blue1" => 33,
|
|
69
|
+
"green3" => 40,
|
|
70
|
+
"spring_green3" => 41,
|
|
71
|
+
"dark_cyan" => 36,
|
|
72
|
+
"light_sea_green" => 37,
|
|
73
|
+
"deep_sky_blue2" => 38,
|
|
74
|
+
"deep_sky_blue1" => 39,
|
|
75
|
+
"spring_green2" => 47,
|
|
76
|
+
"cyan3" => 43,
|
|
77
|
+
"dark_turquoise" => 44,
|
|
78
|
+
"turquoise2" => 45,
|
|
79
|
+
"green1" => 46,
|
|
80
|
+
"spring_green1" => 48,
|
|
81
|
+
"medium_spring_green" => 49,
|
|
82
|
+
"cyan2" => 50,
|
|
83
|
+
"cyan1" => 51,
|
|
84
|
+
"dark_red" => 88,
|
|
85
|
+
"deep_pink4" => 125,
|
|
86
|
+
"purple4" => 55,
|
|
87
|
+
"purple3" => 56,
|
|
88
|
+
"blue_violet" => 57,
|
|
89
|
+
"orange4" => 94,
|
|
90
|
+
"grey37" => 59,
|
|
91
|
+
"gray37" => 59,
|
|
92
|
+
"medium_purple4" => 60,
|
|
93
|
+
"slate_blue3" => 62,
|
|
94
|
+
"royal_blue1" => 63,
|
|
95
|
+
"chartreuse4" => 64,
|
|
96
|
+
"dark_sea_green4" => 71,
|
|
97
|
+
"pale_turquoise4" => 66,
|
|
98
|
+
"steel_blue" => 67,
|
|
99
|
+
"steel_blue3" => 68,
|
|
100
|
+
"cornflower_blue" => 69,
|
|
101
|
+
"chartreuse3" => 76,
|
|
102
|
+
"cadet_blue" => 73,
|
|
103
|
+
"sky_blue3" => 74,
|
|
104
|
+
"steel_blue1" => 81,
|
|
105
|
+
"pale_green3" => 114,
|
|
106
|
+
"sea_green3" => 78,
|
|
107
|
+
"aquamarine3" => 79,
|
|
108
|
+
"medium_turquoise" => 80,
|
|
109
|
+
"chartreuse2" => 112,
|
|
110
|
+
"sea_green2" => 83,
|
|
111
|
+
"sea_green1" => 85,
|
|
112
|
+
"aquamarine1" => 122,
|
|
113
|
+
"dark_slate_gray2" => 87,
|
|
114
|
+
"dark_magenta" => 91,
|
|
115
|
+
"dark_violet" => 128,
|
|
116
|
+
"purple" => 129,
|
|
117
|
+
"light_pink4" => 95,
|
|
118
|
+
"plum4" => 96,
|
|
119
|
+
"medium_purple3" => 98,
|
|
120
|
+
"slate_blue1" => 99,
|
|
121
|
+
"yellow4" => 106,
|
|
122
|
+
"wheat4" => 101,
|
|
123
|
+
"grey53" => 102,
|
|
124
|
+
"gray53" => 102,
|
|
125
|
+
"light_slate_grey" => 103,
|
|
126
|
+
"light_slate_gray" => 103,
|
|
127
|
+
"medium_purple" => 104,
|
|
128
|
+
"light_slate_blue" => 105,
|
|
129
|
+
"dark_olive_green3" => 149,
|
|
130
|
+
"dark_sea_green" => 108,
|
|
131
|
+
"light_sky_blue3" => 110,
|
|
132
|
+
"sky_blue2" => 111,
|
|
133
|
+
"dark_sea_green3" => 150,
|
|
134
|
+
"dark_slate_gray3" => 116,
|
|
135
|
+
"sky_blue1" => 117,
|
|
136
|
+
"chartreuse1" => 118,
|
|
137
|
+
"light_green" => 120,
|
|
138
|
+
"pale_green1" => 156,
|
|
139
|
+
"dark_slate_gray1" => 123,
|
|
140
|
+
"red3" => 160,
|
|
141
|
+
"medium_violet_red" => 126,
|
|
142
|
+
"magenta3" => 164,
|
|
143
|
+
"dark_orange3" => 166,
|
|
144
|
+
"indian_red" => 167,
|
|
145
|
+
"hot_pink3" => 168,
|
|
146
|
+
"medium_orchid3" => 133,
|
|
147
|
+
"medium_orchid" => 134,
|
|
148
|
+
"medium_purple2" => 140,
|
|
149
|
+
"dark_goldenrod" => 136,
|
|
150
|
+
"light_salmon3" => 173,
|
|
151
|
+
"rosy_brown" => 138,
|
|
152
|
+
"grey63" => 139,
|
|
153
|
+
"gray63" => 139,
|
|
154
|
+
"medium_purple1" => 141,
|
|
155
|
+
"gold3" => 178,
|
|
156
|
+
"dark_khaki" => 143,
|
|
157
|
+
"navajo_white3" => 144,
|
|
158
|
+
"grey69" => 145,
|
|
159
|
+
"gray69" => 145,
|
|
160
|
+
"light_steel_blue3" => 146,
|
|
161
|
+
"light_steel_blue" => 147,
|
|
162
|
+
"yellow3" => 184,
|
|
163
|
+
"dark_sea_green2" => 157,
|
|
164
|
+
"light_cyan3" => 152,
|
|
165
|
+
"light_sky_blue1" => 153,
|
|
166
|
+
"green_yellow" => 154,
|
|
167
|
+
"dark_olive_green2" => 155,
|
|
168
|
+
"dark_sea_green1" => 193,
|
|
169
|
+
"pale_turquoise1" => 159,
|
|
170
|
+
"deep_pink3" => 162,
|
|
171
|
+
"magenta2" => 200,
|
|
172
|
+
"hot_pink2" => 169,
|
|
173
|
+
"orchid" => 170,
|
|
174
|
+
"medium_orchid1" => 207,
|
|
175
|
+
"orange3" => 172,
|
|
176
|
+
"light_pink3" => 174,
|
|
177
|
+
"pink3" => 175,
|
|
178
|
+
"plum3" => 176,
|
|
179
|
+
"violet" => 177,
|
|
180
|
+
"light_goldenrod3" => 179,
|
|
181
|
+
"tan" => 180,
|
|
182
|
+
"misty_rose3" => 181,
|
|
183
|
+
"thistle3" => 182,
|
|
184
|
+
"plum2" => 183,
|
|
185
|
+
"khaki3" => 185,
|
|
186
|
+
"light_goldenrod2" => 222,
|
|
187
|
+
"light_yellow3" => 187,
|
|
188
|
+
"grey84" => 188,
|
|
189
|
+
"gray84" => 188,
|
|
190
|
+
"light_steel_blue1" => 189,
|
|
191
|
+
"yellow2" => 190,
|
|
192
|
+
"dark_olive_green1" => 192,
|
|
193
|
+
"honeydew2" => 194,
|
|
194
|
+
"light_cyan1" => 195,
|
|
195
|
+
"red1" => 196,
|
|
196
|
+
"deep_pink2" => 197,
|
|
197
|
+
"deep_pink1" => 199,
|
|
198
|
+
"magenta1" => 201,
|
|
199
|
+
"orange_red1" => 202,
|
|
200
|
+
"indian_red1" => 204,
|
|
201
|
+
"hot_pink" => 206,
|
|
202
|
+
"dark_orange" => 208,
|
|
203
|
+
"salmon1" => 209,
|
|
204
|
+
"light_coral" => 210,
|
|
205
|
+
"pale_violet_red1" => 211,
|
|
206
|
+
"orchid2" => 212,
|
|
207
|
+
"orchid1" => 213,
|
|
208
|
+
"orange1" => 214,
|
|
209
|
+
"sandy_brown" => 215,
|
|
210
|
+
"light_salmon1" => 216,
|
|
211
|
+
"light_pink1" => 217,
|
|
212
|
+
"pink1" => 218,
|
|
213
|
+
"plum1" => 219,
|
|
214
|
+
"gold1" => 220,
|
|
215
|
+
"navajo_white1" => 223,
|
|
216
|
+
"misty_rose1" => 224,
|
|
217
|
+
"thistle1" => 225,
|
|
218
|
+
"yellow1" => 226,
|
|
219
|
+
"light_goldenrod1" => 227,
|
|
220
|
+
"khaki1" => 228,
|
|
221
|
+
"wheat1" => 229,
|
|
222
|
+
"cornsilk1" => 230,
|
|
223
|
+
"grey100" => 231,
|
|
224
|
+
"gray100" => 231,
|
|
225
|
+
"grey3" => 232,
|
|
226
|
+
"gray3" => 232,
|
|
227
|
+
"grey7" => 233,
|
|
228
|
+
"gray7" => 233,
|
|
229
|
+
"grey11" => 234,
|
|
230
|
+
"gray11" => 234,
|
|
231
|
+
"grey15" => 235,
|
|
232
|
+
"gray15" => 235,
|
|
233
|
+
"grey19" => 236,
|
|
234
|
+
"gray19" => 236,
|
|
235
|
+
"grey23" => 237,
|
|
236
|
+
"gray23" => 237,
|
|
237
|
+
"grey27" => 238,
|
|
238
|
+
"gray27" => 238,
|
|
239
|
+
"grey30" => 239,
|
|
240
|
+
"gray30" => 239,
|
|
241
|
+
"grey35" => 240,
|
|
242
|
+
"gray35" => 240,
|
|
243
|
+
"grey39" => 241,
|
|
244
|
+
"gray39" => 241,
|
|
245
|
+
"grey42" => 242,
|
|
246
|
+
"gray42" => 242,
|
|
247
|
+
"grey46" => 243,
|
|
248
|
+
"gray46" => 243,
|
|
249
|
+
"grey50" => 244,
|
|
250
|
+
"gray50" => 244,
|
|
251
|
+
"grey54" => 245,
|
|
252
|
+
"gray54" => 245,
|
|
253
|
+
"grey58" => 246,
|
|
254
|
+
"gray58" => 246,
|
|
255
|
+
"grey62" => 247,
|
|
256
|
+
"gray62" => 247,
|
|
257
|
+
"grey66" => 248,
|
|
258
|
+
"gray66" => 248,
|
|
259
|
+
"grey70" => 249,
|
|
260
|
+
"gray70" => 249,
|
|
261
|
+
"grey74" => 250,
|
|
262
|
+
"gray74" => 250,
|
|
263
|
+
"grey78" => 251,
|
|
264
|
+
"gray78" => 251,
|
|
265
|
+
"grey82" => 252,
|
|
266
|
+
"gray82" => 252,
|
|
267
|
+
"grey85" => 253,
|
|
268
|
+
"gray85" => 253,
|
|
269
|
+
"grey89" => 254,
|
|
270
|
+
"gray89" => 254,
|
|
271
|
+
"grey93" => 255,
|
|
272
|
+
"gray93" => 255
|
|
273
|
+
}.freeze
|
|
274
|
+
|
|
275
|
+
# Reverse mapping from number to canonical name
|
|
276
|
+
COLOR_NUMBER_TO_NAME = ANSI_COLOR_NAMES.each_with_object({}) do |(name, number), hash|
|
|
277
|
+
hash[number] ||= name
|
|
278
|
+
end.freeze
|
|
279
|
+
|
|
280
|
+
# Error raised when a color definition cannot be parsed
|
|
281
|
+
class ColorParseError < StandardError
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Represents a terminal color.
|
|
285
|
+
# Supports default colors, standard ANSI colors (0-15), 8-bit colors (0-255),
|
|
286
|
+
# and true color (24-bit RGB).
|
|
287
|
+
class Color
|
|
288
|
+
# Regex for parsing color definitions
|
|
289
|
+
COLOR_REGEX = /\A
|
|
290
|
+
(?:\#(?<hex>[0-9a-fA-F]{6}))|
|
|
291
|
+
(?:color\((?<color8>\d{1,3})\))|
|
|
292
|
+
(?:rgb\((?<rgb>[\d\s,]+)\))
|
|
293
|
+
\z/x
|
|
294
|
+
|
|
295
|
+
# @return [String] Original color name or definition
|
|
296
|
+
attr_reader :name
|
|
297
|
+
|
|
298
|
+
# @return [Symbol] Color type (see ColorType)
|
|
299
|
+
attr_reader :type
|
|
300
|
+
|
|
301
|
+
# @return [Integer, nil] Color number for standard/8-bit colors
|
|
302
|
+
attr_reader :number
|
|
303
|
+
|
|
304
|
+
# @return [ColorTriplet, nil] RGB triplet for truecolor
|
|
305
|
+
attr_reader :triplet
|
|
306
|
+
|
|
307
|
+
# Cache for parsed colors
|
|
308
|
+
@parse_cache = {}
|
|
309
|
+
@parse_cache_mutex = Mutex.new
|
|
310
|
+
|
|
311
|
+
# Cache for downgraded colors
|
|
312
|
+
@downgrade_cache = {}
|
|
313
|
+
@downgrade_cache_mutex = Mutex.new
|
|
314
|
+
|
|
315
|
+
# Create a new color
|
|
316
|
+
# @param name [String] Color name or definition
|
|
317
|
+
# @param type [Symbol] Color type (see ColorType)
|
|
318
|
+
# @param number [Integer, nil] Color number
|
|
319
|
+
# @param triplet [ColorTriplet, nil] RGB triplet
|
|
320
|
+
def initialize(name, type:, number: nil, triplet: nil)
|
|
321
|
+
@name = name.freeze
|
|
322
|
+
@type = type
|
|
323
|
+
@number = number
|
|
324
|
+
@triplet = triplet
|
|
325
|
+
freeze
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# @return [Symbol] Native color system for this color
|
|
329
|
+
def system
|
|
330
|
+
case @type
|
|
331
|
+
when ColorType::DEFAULT
|
|
332
|
+
ColorSystem::STANDARD
|
|
333
|
+
when ColorType::STANDARD
|
|
334
|
+
ColorSystem::STANDARD
|
|
335
|
+
when ColorType::EIGHT_BIT
|
|
336
|
+
ColorSystem::EIGHT_BIT
|
|
337
|
+
when ColorType::TRUECOLOR
|
|
338
|
+
ColorSystem::TRUECOLOR
|
|
339
|
+
when ColorType::WINDOWS
|
|
340
|
+
ColorSystem::WINDOWS
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# @return [Boolean] True if color is system-defined (may vary by terminal)
|
|
345
|
+
def system_defined?
|
|
346
|
+
[ColorType::DEFAULT, ColorType::STANDARD].include?(@type)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# @return [Boolean] True if this is the default color
|
|
350
|
+
def default?
|
|
351
|
+
@type == ColorType::DEFAULT
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Get the RGB triplet for this color
|
|
355
|
+
# @param theme [TerminalTheme, nil] Terminal theme for system colors
|
|
356
|
+
# @param foreground [Boolean] True for foreground, false for background
|
|
357
|
+
# @return [ColorTriplet] RGB color value
|
|
358
|
+
def get_truecolor(theme: nil, foreground: true)
|
|
359
|
+
case @type
|
|
360
|
+
when ColorType::TRUECOLOR
|
|
361
|
+
@triplet
|
|
362
|
+
when ColorType::EIGHT_BIT
|
|
363
|
+
Palettes.get_eight_bit(@number)
|
|
364
|
+
when ColorType::STANDARD
|
|
365
|
+
if theme
|
|
366
|
+
theme.ansi_colors[@number]
|
|
367
|
+
else
|
|
368
|
+
Palettes.get_standard(@number)
|
|
369
|
+
end
|
|
370
|
+
when ColorType::WINDOWS
|
|
371
|
+
Palettes.get_windows(@number)
|
|
372
|
+
when ColorType::DEFAULT
|
|
373
|
+
if theme
|
|
374
|
+
foreground ? theme.foreground : theme.background
|
|
375
|
+
else
|
|
376
|
+
foreground ? ColorTriplet.new(255, 255, 255) : ColorTriplet.new(0, 0, 0)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Get ANSI escape codes for this color
|
|
382
|
+
# @param foreground [Boolean] True for foreground, false for background
|
|
383
|
+
# @return [Array<String>] ANSI code components
|
|
384
|
+
def ansi_codes(foreground: true)
|
|
385
|
+
case @type
|
|
386
|
+
when ColorType::DEFAULT
|
|
387
|
+
[foreground ? "39" : "49"]
|
|
388
|
+
when ColorType::STANDARD
|
|
389
|
+
base = foreground ? 30 : 40
|
|
390
|
+
offset = @number < 8 ? @number : (@number - 8 + 60)
|
|
391
|
+
[(base + (@number < 8 ? @number : @number - 8 + 60)).to_s]
|
|
392
|
+
when ColorType::EIGHT_BIT
|
|
393
|
+
[foreground ? "38" : "48", "5", @number.to_s]
|
|
394
|
+
when ColorType::TRUECOLOR
|
|
395
|
+
[foreground ? "38" : "48", "2", @triplet.red.to_s, @triplet.green.to_s, @triplet.blue.to_s]
|
|
396
|
+
when ColorType::WINDOWS
|
|
397
|
+
base = @number < 8 ? (foreground ? 30 : 40) : (foreground ? 90 : 100)
|
|
398
|
+
[(base + @number % 8).to_s]
|
|
399
|
+
else
|
|
400
|
+
[]
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Downgrade color to a simpler color system
|
|
405
|
+
# @param target_system [Symbol] Target color system
|
|
406
|
+
# @return [Color] Downgraded color (may be self if no downgrade needed)
|
|
407
|
+
def downgrade(target_system)
|
|
408
|
+
return self if @type == ColorType::DEFAULT
|
|
409
|
+
return self if target_system == system
|
|
410
|
+
|
|
411
|
+
cache_key = [@type, @number, @triplet&.to_a, target_system]
|
|
412
|
+
|
|
413
|
+
self.class.instance_variable_get(:@downgrade_cache_mutex).synchronize do
|
|
414
|
+
cache = self.class.instance_variable_get(:@downgrade_cache)
|
|
415
|
+
return cache[cache_key] if cache.key?(cache_key)
|
|
416
|
+
|
|
417
|
+
result = compute_downgrade(target_system)
|
|
418
|
+
cache[cache_key] = result
|
|
419
|
+
result
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Check equality with another color
|
|
424
|
+
def ==(other)
|
|
425
|
+
return false unless other.is_a?(Color)
|
|
426
|
+
|
|
427
|
+
@type == other.type && @number == other.number && @triplet == other.triplet
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
alias eql? ==
|
|
431
|
+
|
|
432
|
+
def hash
|
|
433
|
+
[@type, @number, @triplet].hash
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def to_s
|
|
437
|
+
@name
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def inspect
|
|
441
|
+
case @type
|
|
442
|
+
when ColorType::DEFAULT
|
|
443
|
+
"#<Rich::Color default>"
|
|
444
|
+
when ColorType::TRUECOLOR
|
|
445
|
+
"#<Rich::Color #{@name} (#{@triplet.hex})>"
|
|
446
|
+
else
|
|
447
|
+
"#<Rich::Color #{@name} (#{@type}:#{@number})>"
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
class << self
|
|
452
|
+
# Parse a color definition string
|
|
453
|
+
# @param color [String] Color definition
|
|
454
|
+
# @return [Color] Parsed color
|
|
455
|
+
# @raise [ColorParseError] If color cannot be parsed
|
|
456
|
+
def parse(color)
|
|
457
|
+
return color if color.is_a?(Color)
|
|
458
|
+
|
|
459
|
+
@parse_cache_mutex.synchronize do
|
|
460
|
+
return @parse_cache[color] if @parse_cache.key?(color)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
result = parse_uncached(color)
|
|
464
|
+
|
|
465
|
+
@parse_cache_mutex.synchronize do
|
|
466
|
+
@parse_cache[color] = result
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
result
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Create a default color
|
|
473
|
+
# @return [Color]
|
|
474
|
+
def default
|
|
475
|
+
@default ||= new("default", type: ColorType::DEFAULT)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Create a color from ANSI number
|
|
479
|
+
# @param number [Integer] Color number (0-255)
|
|
480
|
+
# @return [Color]
|
|
481
|
+
def from_ansi(number)
|
|
482
|
+
number = number.clamp(0, 255)
|
|
483
|
+
type = number < 16 ? ColorType::STANDARD : ColorType::EIGHT_BIT
|
|
484
|
+
name = COLOR_NUMBER_TO_NAME[number] || "color(#{number})"
|
|
485
|
+
new(name, type: type, number: number)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Create a color from RGB triplet
|
|
489
|
+
# @param triplet [ColorTriplet] RGB triplet
|
|
490
|
+
# @return [Color]
|
|
491
|
+
def from_triplet(triplet)
|
|
492
|
+
new(triplet.hex, type: ColorType::TRUECOLOR, triplet: triplet)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Create a color from RGB values
|
|
496
|
+
# @param red [Integer] Red (0-255)
|
|
497
|
+
# @param green [Integer] Green (0-255)
|
|
498
|
+
# @param blue [Integer] Blue (0-255)
|
|
499
|
+
# @return [Color]
|
|
500
|
+
def from_rgb(red, green, blue)
|
|
501
|
+
from_triplet(ColorTriplet.new(red, green, blue))
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
private
|
|
505
|
+
|
|
506
|
+
def parse_uncached(color)
|
|
507
|
+
original = color
|
|
508
|
+
color = color.to_s.strip.downcase
|
|
509
|
+
|
|
510
|
+
return default if color == "default"
|
|
511
|
+
|
|
512
|
+
# Check named colors
|
|
513
|
+
if ANSI_COLOR_NAMES.key?(color)
|
|
514
|
+
number = ANSI_COLOR_NAMES[color]
|
|
515
|
+
type = number < 16 ? ColorType::STANDARD : ColorType::EIGHT_BIT
|
|
516
|
+
return new(color, type: type, number: number)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Try regex patterns
|
|
520
|
+
match = COLOR_REGEX.match(color)
|
|
521
|
+
raise ColorParseError, "'#{original}' is not a valid color" unless match
|
|
522
|
+
|
|
523
|
+
if match[:hex]
|
|
524
|
+
triplet = ColorTriplet.from_hex(match[:hex])
|
|
525
|
+
return new(color, type: ColorType::TRUECOLOR, triplet: triplet)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
if match[:color8]
|
|
529
|
+
number = match[:color8].to_i
|
|
530
|
+
raise ColorParseError, "Color number must be <= 255 in '#{original}'" if number > 255
|
|
531
|
+
|
|
532
|
+
type = number < 16 ? ColorType::STANDARD : ColorType::EIGHT_BIT
|
|
533
|
+
return new(color, type: type, number: number)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
if match[:rgb]
|
|
537
|
+
parts = match[:rgb].split(",").map(&:strip).map(&:to_i)
|
|
538
|
+
raise ColorParseError, "Expected 3 RGB components in '#{original}'" unless parts.length == 3
|
|
539
|
+
raise ColorParseError, "Color components must be <= 255 in '#{original}'" if parts.any? { |p| p > 255 }
|
|
540
|
+
|
|
541
|
+
triplet = ColorTriplet.new(*parts)
|
|
542
|
+
return new(color, type: ColorType::TRUECOLOR, triplet: triplet)
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
raise ColorParseError, "'#{original}' is not a valid color"
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
private
|
|
550
|
+
|
|
551
|
+
def compute_downgrade(target_system)
|
|
552
|
+
triplet = get_truecolor
|
|
553
|
+
|
|
554
|
+
case target_system
|
|
555
|
+
when ColorSystem::EIGHT_BIT
|
|
556
|
+
return self if @type == ColorType::EIGHT_BIT
|
|
557
|
+
|
|
558
|
+
# Convert to grayscale if low saturation
|
|
559
|
+
r, g, b = triplet.normalized
|
|
560
|
+
_h, l, s = rgb_to_hls(r, g, b)
|
|
561
|
+
|
|
562
|
+
if s < 0.15
|
|
563
|
+
gray = (l * 25.0).round
|
|
564
|
+
color_number = if gray == 0
|
|
565
|
+
16
|
|
566
|
+
elsif gray == 25
|
|
567
|
+
231
|
|
568
|
+
else
|
|
569
|
+
231 + gray
|
|
570
|
+
end
|
|
571
|
+
return Color.new(@name, type: ColorType::EIGHT_BIT, number: color_number)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# Map to 6x6x6 color cube
|
|
575
|
+
six_red = triplet.red < 95 ? triplet.red / 95.0 : 1 + (triplet.red - 95) / 40.0
|
|
576
|
+
six_green = triplet.green < 95 ? triplet.green / 95.0 : 1 + (triplet.green - 95) / 40.0
|
|
577
|
+
six_blue = triplet.blue < 95 ? triplet.blue / 95.0 : 1 + (triplet.blue - 95) / 40.0
|
|
578
|
+
|
|
579
|
+
color_number = 16 + 36 * six_red.round + 6 * six_green.round + six_blue.round
|
|
580
|
+
Color.new(@name, type: ColorType::EIGHT_BIT, number: color_number.to_i)
|
|
581
|
+
|
|
582
|
+
when ColorSystem::STANDARD
|
|
583
|
+
number = Palettes.match_standard(triplet)
|
|
584
|
+
Color.new(@name, type: ColorType::STANDARD, number: number)
|
|
585
|
+
|
|
586
|
+
when ColorSystem::WINDOWS
|
|
587
|
+
if @type == ColorType::EIGHT_BIT && @number < 16
|
|
588
|
+
return Color.new(@name, type: ColorType::WINDOWS, number: @number)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
number = Palettes.match_windows(triplet)
|
|
592
|
+
Color.new(@name, type: ColorType::WINDOWS, number: number)
|
|
593
|
+
|
|
594
|
+
else
|
|
595
|
+
self
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def rgb_to_hls(r, g, b)
|
|
600
|
+
max_c = [r, g, b].max
|
|
601
|
+
min_c = [r, g, b].min
|
|
602
|
+
l = (max_c + min_c) / 2.0
|
|
603
|
+
|
|
604
|
+
return [0.0, l, 0.0] if max_c == min_c
|
|
605
|
+
|
|
606
|
+
s = if l <= 0.5
|
|
607
|
+
(max_c - min_c) / (max_c + min_c)
|
|
608
|
+
else
|
|
609
|
+
(max_c - min_c) / (2.0 - max_c - min_c)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
rc = (max_c - r) / (max_c - min_c)
|
|
613
|
+
gc = (max_c - g) / (max_c - min_c)
|
|
614
|
+
bc = (max_c - b) / (max_c - min_c)
|
|
615
|
+
|
|
616
|
+
h = if r == max_c
|
|
617
|
+
bc - gc
|
|
618
|
+
elsif g == max_c
|
|
619
|
+
2.0 + rc - bc
|
|
620
|
+
else
|
|
621
|
+
4.0 + gc - rc
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
h = (h / 6.0) % 1.0
|
|
625
|
+
[h, l, s]
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
end
|