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/style.rb
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "color"
|
|
4
|
+
|
|
5
|
+
module Rich
|
|
6
|
+
# Style attributes represented as bit flags
|
|
7
|
+
module StyleAttribute
|
|
8
|
+
BOLD = 1 << 0 # 1
|
|
9
|
+
DIM = 1 << 1 # 2
|
|
10
|
+
ITALIC = 1 << 2 # 4
|
|
11
|
+
UNDERLINE = 1 << 3 # 8
|
|
12
|
+
BLINK = 1 << 4 # 16
|
|
13
|
+
BLINK2 = 1 << 5 # 32 (rapid blink)
|
|
14
|
+
REVERSE = 1 << 6 # 64
|
|
15
|
+
CONCEAL = 1 << 7 # 128
|
|
16
|
+
STRIKE = 1 << 8 # 256
|
|
17
|
+
UNDERLINE2 = 1 << 9 # 512 (double underline)
|
|
18
|
+
FRAME = 1 << 10 # 1024
|
|
19
|
+
ENCIRCLE = 1 << 11 # 2048
|
|
20
|
+
OVERLINE = 1 << 12 # 4096
|
|
21
|
+
|
|
22
|
+
ALL = {
|
|
23
|
+
bold: BOLD,
|
|
24
|
+
dim: DIM,
|
|
25
|
+
italic: ITALIC,
|
|
26
|
+
underline: UNDERLINE,
|
|
27
|
+
blink: BLINK,
|
|
28
|
+
blink2: BLINK2,
|
|
29
|
+
reverse: REVERSE,
|
|
30
|
+
conceal: CONCEAL,
|
|
31
|
+
strike: STRIKE,
|
|
32
|
+
underline2: UNDERLINE2,
|
|
33
|
+
frame: FRAME,
|
|
34
|
+
encircle: ENCIRCLE,
|
|
35
|
+
overline: OVERLINE
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
NAMES = ALL.keys.freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Represents a terminal style with colors and text attributes.
|
|
42
|
+
# Styles are immutable and can be combined using the + operator.
|
|
43
|
+
class Style
|
|
44
|
+
# ANSI reset and attribute codes
|
|
45
|
+
ANSI_CODES = {
|
|
46
|
+
bold: "1",
|
|
47
|
+
dim: "2",
|
|
48
|
+
italic: "3",
|
|
49
|
+
underline: "4",
|
|
50
|
+
blink: "5",
|
|
51
|
+
blink2: "6",
|
|
52
|
+
reverse: "7",
|
|
53
|
+
conceal: "8",
|
|
54
|
+
strike: "9",
|
|
55
|
+
underline2: "21",
|
|
56
|
+
frame: "51",
|
|
57
|
+
encircle: "52",
|
|
58
|
+
overline: "53"
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
# Regex for parsing style definitions
|
|
62
|
+
STYLE_REGEX = /
|
|
63
|
+
(?<not>not\s+)?
|
|
64
|
+
(?<attr>bold|dim|italic|underline2?|blink2?|reverse|conceal|strike|frame|encircle|overline)|
|
|
65
|
+
(?<link>link\s+(?<url>\S+))|
|
|
66
|
+
(?<on>on\s+)?(?<color>\S+)
|
|
67
|
+
/x
|
|
68
|
+
|
|
69
|
+
# @return [Color, nil] Foreground color
|
|
70
|
+
attr_reader :color
|
|
71
|
+
|
|
72
|
+
# @return [Color, nil] Background color
|
|
73
|
+
attr_reader :bgcolor
|
|
74
|
+
|
|
75
|
+
# @return [Integer] Attributes that are explicitly set
|
|
76
|
+
attr_reader :set_attributes
|
|
77
|
+
|
|
78
|
+
# @return [Integer] Attribute values (0 = off, 1 = on)
|
|
79
|
+
attr_reader :attributes
|
|
80
|
+
|
|
81
|
+
# @return [String, nil] Hyperlink URL
|
|
82
|
+
attr_reader :link
|
|
83
|
+
|
|
84
|
+
# @return [Hash, nil] Meta information
|
|
85
|
+
attr_reader :meta
|
|
86
|
+
|
|
87
|
+
# Cache for parsed styles
|
|
88
|
+
@parse_cache = {}
|
|
89
|
+
@parse_cache_mutex = Mutex.new
|
|
90
|
+
|
|
91
|
+
# Create a new style
|
|
92
|
+
# @param color [Color, String, nil] Foreground color
|
|
93
|
+
# @param bgcolor [Color, String, nil] Background color
|
|
94
|
+
# @param bold [Boolean, nil] Bold attribute
|
|
95
|
+
# @param dim [Boolean, nil] Dim attribute
|
|
96
|
+
# @param italic [Boolean, nil] Italic attribute
|
|
97
|
+
# @param underline [Boolean, nil] Underline attribute
|
|
98
|
+
# @param blink [Boolean, nil] Blink attribute
|
|
99
|
+
# @param blink2 [Boolean, nil] Rapid blink attribute
|
|
100
|
+
# @param reverse [Boolean, nil] Reverse video attribute
|
|
101
|
+
# @param conceal [Boolean, nil] Conceal attribute
|
|
102
|
+
# @param strike [Boolean, nil] Strikethrough attribute
|
|
103
|
+
# @param underline2 [Boolean, nil] Double underline attribute
|
|
104
|
+
# @param frame [Boolean, nil] Frame attribute
|
|
105
|
+
# @param encircle [Boolean, nil] Encircle attribute
|
|
106
|
+
# @param overline [Boolean, nil] Overline attribute
|
|
107
|
+
# @param link [String, nil] Hyperlink URL
|
|
108
|
+
# @param meta [Hash, nil] Meta information
|
|
109
|
+
def initialize(
|
|
110
|
+
color: nil,
|
|
111
|
+
bgcolor: nil,
|
|
112
|
+
bold: nil,
|
|
113
|
+
dim: nil,
|
|
114
|
+
italic: nil,
|
|
115
|
+
underline: nil,
|
|
116
|
+
blink: nil,
|
|
117
|
+
blink2: nil,
|
|
118
|
+
reverse: nil,
|
|
119
|
+
conceal: nil,
|
|
120
|
+
strike: nil,
|
|
121
|
+
underline2: nil,
|
|
122
|
+
frame: nil,
|
|
123
|
+
encircle: nil,
|
|
124
|
+
overline: nil,
|
|
125
|
+
link: nil,
|
|
126
|
+
meta: nil
|
|
127
|
+
)
|
|
128
|
+
@color = parse_color(color)
|
|
129
|
+
@bgcolor = parse_color(bgcolor)
|
|
130
|
+
@link = link&.freeze
|
|
131
|
+
@meta = meta&.freeze
|
|
132
|
+
|
|
133
|
+
# Build attribute masks
|
|
134
|
+
@set_attributes = 0
|
|
135
|
+
@attributes = 0
|
|
136
|
+
|
|
137
|
+
attrs = {
|
|
138
|
+
bold: bold, dim: dim, italic: italic, underline: underline,
|
|
139
|
+
blink: blink, blink2: blink2, reverse: reverse, conceal: conceal,
|
|
140
|
+
strike: strike, underline2: underline2, frame: frame,
|
|
141
|
+
encircle: encircle, overline: overline
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
attrs.each do |name, value|
|
|
145
|
+
next if value.nil?
|
|
146
|
+
|
|
147
|
+
bit = StyleAttribute::ALL[name]
|
|
148
|
+
@set_attributes |= bit
|
|
149
|
+
@attributes |= bit if value
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
freeze
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Check if any attributes, colors, or link are set
|
|
156
|
+
# @return [Boolean]
|
|
157
|
+
def blank?
|
|
158
|
+
@color.nil? && @bgcolor.nil? && @set_attributes == 0 && @link.nil?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @return [Boolean] False if blank
|
|
162
|
+
def present?
|
|
163
|
+
!blank?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Get a specific attribute value
|
|
167
|
+
# @param name [Symbol] Attribute name
|
|
168
|
+
# @return [Boolean, nil] Attribute value or nil if not set
|
|
169
|
+
def [](name)
|
|
170
|
+
bit = StyleAttribute::ALL[name]
|
|
171
|
+
return nil unless bit
|
|
172
|
+
|
|
173
|
+
return nil if (@set_attributes & bit) == 0
|
|
174
|
+
|
|
175
|
+
(@attributes & bit) != 0
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Attribute accessor methods
|
|
179
|
+
StyleAttribute::NAMES.each do |attr_name|
|
|
180
|
+
define_method(attr_name) { self[attr_name] }
|
|
181
|
+
define_method("#{attr_name}?") { self[attr_name] == true }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Generate ANSI escape codes for this style
|
|
185
|
+
# @param color_system [Symbol] Target color system
|
|
186
|
+
# @return [String] ANSI escape sequence
|
|
187
|
+
def render(color_system: ColorSystem::TRUECOLOR)
|
|
188
|
+
codes = []
|
|
189
|
+
|
|
190
|
+
# Add attribute codes
|
|
191
|
+
StyleAttribute::NAMES.each do |name|
|
|
192
|
+
value = self[name]
|
|
193
|
+
next if value.nil?
|
|
194
|
+
|
|
195
|
+
if value
|
|
196
|
+
codes << ANSI_CODES[name]
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Add color codes
|
|
201
|
+
if @color
|
|
202
|
+
target_color = @color.downgrade(color_system)
|
|
203
|
+
codes.concat(target_color.ansi_codes(foreground: true))
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if @bgcolor
|
|
207
|
+
target_color = @bgcolor.downgrade(color_system)
|
|
208
|
+
codes.concat(target_color.ansi_codes(foreground: false))
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
return "" if codes.empty?
|
|
212
|
+
|
|
213
|
+
"\e[#{codes.join(';')}m"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Generate the style definition string
|
|
217
|
+
# @return [String]
|
|
218
|
+
def to_s
|
|
219
|
+
parts = []
|
|
220
|
+
|
|
221
|
+
StyleAttribute::NAMES.each do |name|
|
|
222
|
+
value = self[name]
|
|
223
|
+
next if value.nil?
|
|
224
|
+
|
|
225
|
+
parts << (value ? name.to_s : "not #{name}")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
parts << @color.name if @color
|
|
229
|
+
parts << "on #{@bgcolor.name}" if @bgcolor
|
|
230
|
+
parts << "link #{@link}" if @link
|
|
231
|
+
|
|
232
|
+
parts.join(" ")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def inspect
|
|
236
|
+
attrs = []
|
|
237
|
+
attrs << "color=#{@color.name}" if @color
|
|
238
|
+
attrs << "bgcolor=#{@bgcolor.name}" if @bgcolor
|
|
239
|
+
|
|
240
|
+
StyleAttribute::NAMES.each do |name|
|
|
241
|
+
value = self[name]
|
|
242
|
+
attrs << "#{name}=#{value}" unless value.nil?
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
attrs << "link=#{@link.inspect}" if @link
|
|
246
|
+
|
|
247
|
+
"#<Rich::Style #{attrs.join(' ')}>"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Combine two styles (right-hand style takes precedence)
|
|
251
|
+
# @param other [Style] Style to combine with
|
|
252
|
+
# @return [Style] Combined style
|
|
253
|
+
def +(other)
|
|
254
|
+
return self if other.nil? || other.blank?
|
|
255
|
+
return other if blank?
|
|
256
|
+
|
|
257
|
+
new_color = other.color || @color
|
|
258
|
+
new_bgcolor = other.bgcolor || @bgcolor
|
|
259
|
+
new_link = other.link || @link
|
|
260
|
+
new_meta = @meta || other.meta ? (@meta || {}).merge(other.meta || {}) : nil
|
|
261
|
+
|
|
262
|
+
# Merge attributes
|
|
263
|
+
new_set = @set_attributes | other.set_attributes
|
|
264
|
+
new_attrs = (@attributes & ~other.set_attributes) | (other.attributes & other.set_attributes)
|
|
265
|
+
|
|
266
|
+
Style.combine(
|
|
267
|
+
color: new_color,
|
|
268
|
+
bgcolor: new_bgcolor,
|
|
269
|
+
link: new_link,
|
|
270
|
+
meta: new_meta,
|
|
271
|
+
set_attributes: new_set,
|
|
272
|
+
attributes: new_attrs
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Get style with no colors
|
|
277
|
+
# @return [Style]
|
|
278
|
+
def without_color
|
|
279
|
+
Style.combine(
|
|
280
|
+
color: nil,
|
|
281
|
+
bgcolor: nil,
|
|
282
|
+
link: @link,
|
|
283
|
+
meta: @meta,
|
|
284
|
+
set_attributes: @set_attributes,
|
|
285
|
+
attributes: @attributes
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Get background-only style
|
|
290
|
+
# @return [Style]
|
|
291
|
+
def background_style
|
|
292
|
+
Style.new(bgcolor: @bgcolor)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def ==(other)
|
|
296
|
+
return false unless other.is_a?(Style)
|
|
297
|
+
|
|
298
|
+
@color == other.color &&
|
|
299
|
+
@bgcolor == other.bgcolor &&
|
|
300
|
+
@set_attributes == other.set_attributes &&
|
|
301
|
+
@attributes == other.attributes &&
|
|
302
|
+
@link == other.link
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
alias eql? ==
|
|
306
|
+
|
|
307
|
+
def hash
|
|
308
|
+
[@color, @bgcolor, @set_attributes, @attributes, @link].hash
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
class << self
|
|
312
|
+
# Parse a style definition string
|
|
313
|
+
# @param style [String, Style, nil] Style definition
|
|
314
|
+
# @return [Style]
|
|
315
|
+
def parse(style)
|
|
316
|
+
return null if style.nil? || (style.is_a?(String) && style.empty?)
|
|
317
|
+
return style if style.is_a?(Style)
|
|
318
|
+
|
|
319
|
+
style = style.to_s
|
|
320
|
+
|
|
321
|
+
@parse_cache_mutex.synchronize do
|
|
322
|
+
return @parse_cache[style] if @parse_cache.key?(style)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
result = parse_uncached(style)
|
|
326
|
+
|
|
327
|
+
@parse_cache_mutex.synchronize do
|
|
328
|
+
@parse_cache[style] = result
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
result
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Create a null (empty) style
|
|
335
|
+
# @return [Style]
|
|
336
|
+
def null
|
|
337
|
+
@null ||= new
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Create a combined style with explicit attributes (internal use)
|
|
341
|
+
# @param color [Color, nil] Foreground color
|
|
342
|
+
# @param bgcolor [Color, nil] Background color
|
|
343
|
+
# @param link [String, nil] Hyperlink
|
|
344
|
+
# @param meta [Hash, nil] Meta info
|
|
345
|
+
# @param set_attributes [Integer] Set attributes bitmask
|
|
346
|
+
# @param attributes [Integer] Attribute values bitmask
|
|
347
|
+
# @return [Style]
|
|
348
|
+
def combine(color:, bgcolor:, link:, meta:, set_attributes:, attributes:)
|
|
349
|
+
style = allocate
|
|
350
|
+
style.instance_variable_set(:@color, color)
|
|
351
|
+
style.instance_variable_set(:@bgcolor, bgcolor)
|
|
352
|
+
style.instance_variable_set(:@link, link&.freeze)
|
|
353
|
+
style.instance_variable_set(:@meta, meta&.freeze)
|
|
354
|
+
style.instance_variable_set(:@set_attributes, set_attributes)
|
|
355
|
+
style.instance_variable_set(:@attributes, attributes)
|
|
356
|
+
style.freeze
|
|
357
|
+
style
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Create a style from just colors
|
|
361
|
+
# @param color [Color, String, nil] Foreground color
|
|
362
|
+
# @param bgcolor [Color, String, nil] Background color
|
|
363
|
+
# @return [Style]
|
|
364
|
+
def from_color(color: nil, bgcolor: nil)
|
|
365
|
+
new(color: color, bgcolor: bgcolor)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Create a style with meta information
|
|
369
|
+
# @param meta [Hash] Meta data
|
|
370
|
+
# @return [Style]
|
|
371
|
+
def from_meta(meta)
|
|
372
|
+
new(meta: meta)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Normalize a style definition
|
|
376
|
+
# @param style [String] Style definition
|
|
377
|
+
# @return [String] Normalized style definition
|
|
378
|
+
def normalize(style)
|
|
379
|
+
parse(style).to_s
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
private
|
|
383
|
+
|
|
384
|
+
def parse_uncached(style_str)
|
|
385
|
+
attrs = {}
|
|
386
|
+
color = nil
|
|
387
|
+
bgcolor = nil
|
|
388
|
+
link = nil
|
|
389
|
+
|
|
390
|
+
style_str.scan(STYLE_REGEX) do
|
|
391
|
+
match = Regexp.last_match
|
|
392
|
+
|
|
393
|
+
if match[:attr]
|
|
394
|
+
attr_name = match[:attr].to_sym
|
|
395
|
+
attrs[attr_name] = match[:not].nil?
|
|
396
|
+
elsif match[:link]
|
|
397
|
+
link = match[:url]
|
|
398
|
+
elsif match[:color]
|
|
399
|
+
color_name = match[:color]
|
|
400
|
+
begin
|
|
401
|
+
parsed_color = Color.parse(color_name)
|
|
402
|
+
if match[:on]
|
|
403
|
+
bgcolor = parsed_color
|
|
404
|
+
else
|
|
405
|
+
color = parsed_color
|
|
406
|
+
end
|
|
407
|
+
rescue ColorParseError
|
|
408
|
+
# Ignore invalid colors
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
new(
|
|
414
|
+
color: color,
|
|
415
|
+
bgcolor: bgcolor,
|
|
416
|
+
link: link,
|
|
417
|
+
**attrs
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
private
|
|
423
|
+
|
|
424
|
+
def parse_color(color)
|
|
425
|
+
return nil if color.nil?
|
|
426
|
+
return color if color.is_a?(Color)
|
|
427
|
+
|
|
428
|
+
Color.parse(color)
|
|
429
|
+
rescue ColorParseError
|
|
430
|
+
nil
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|