rubytui 1.2.3

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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +574 -0
  4. data/lib/rubytui/app.rb +174 -0
  5. data/lib/rubytui/backend.rb +158 -0
  6. data/lib/rubytui/backends/ansi_backend.rb +391 -0
  7. data/lib/rubytui/backends/test_backend.rb +223 -0
  8. data/lib/rubytui/buffer.rb +400 -0
  9. data/lib/rubytui/cell.rb +55 -0
  10. data/lib/rubytui/color.rb +153 -0
  11. data/lib/rubytui/color_mode.rb +203 -0
  12. data/lib/rubytui/errors.rb +13 -0
  13. data/lib/rubytui/event.rb +161 -0
  14. data/lib/rubytui/frame.rb +70 -0
  15. data/lib/rubytui/input/key.rb +93 -0
  16. data/lib/rubytui/input/parser.rb +231 -0
  17. data/lib/rubytui/input/reader.rb +119 -0
  18. data/lib/rubytui/layout/constraint.rb +83 -0
  19. data/lib/rubytui/layout/flex.rb +16 -0
  20. data/lib/rubytui/layout/layout.rb +205 -0
  21. data/lib/rubytui/modifier.rb +67 -0
  22. data/lib/rubytui/rect.rb +126 -0
  23. data/lib/rubytui/stateful_widget.rb +20 -0
  24. data/lib/rubytui/style.rb +143 -0
  25. data/lib/rubytui/symbols.rb +88 -0
  26. data/lib/rubytui/terminal.rb +218 -0
  27. data/lib/rubytui/text/line.rb +67 -0
  28. data/lib/rubytui/text/span.rb +34 -0
  29. data/lib/rubytui/text/text.rb +82 -0
  30. data/lib/rubytui/unicode.rb +162 -0
  31. data/lib/rubytui/version.rb +6 -0
  32. data/lib/rubytui/widget.rb +20 -0
  33. data/lib/rubytui/widgets/async_image.rb +248 -0
  34. data/lib/rubytui/widgets/block.rb +260 -0
  35. data/lib/rubytui/widgets/canvas.rb +248 -0
  36. data/lib/rubytui/widgets/chart.rb +224 -0
  37. data/lib/rubytui/widgets/gauge.rb +139 -0
  38. data/lib/rubytui/widgets/image.rb +330 -0
  39. data/lib/rubytui/widgets/input_field.rb +245 -0
  40. data/lib/rubytui/widgets/list.rb +186 -0
  41. data/lib/rubytui/widgets/paragraph.rb +181 -0
  42. data/lib/rubytui/widgets/popup.rb +140 -0
  43. data/lib/rubytui/widgets/scrollbar.rb +175 -0
  44. data/lib/rubytui/widgets/sparkline.rb +86 -0
  45. data/lib/rubytui/widgets/table.rb +231 -0
  46. data/lib/rubytui/widgets/tabs.rb +90 -0
  47. data/lib/rubytui.rb +389 -0
  48. metadata +89 -0
@@ -0,0 +1,400 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyTUI
4
+ # 2D grid of styled {Cell}s used for rendering.
5
+ #
6
+ # Buffer is the core rendering target. Widgets write into a Buffer via
7
+ # {#set_string} and {#set_style}. The {Terminal} then diffs two buffers
8
+ # to send only changed cells to the backend.
9
+ #
10
+ # For one-shot CLI output, use {#to_ansi} or {#print} to convert the
11
+ # buffer contents to an ANSI-styled string without needing a Terminal.
12
+ #
13
+ # @example Create and render text
14
+ # buf = Buffer.new(80, 24)
15
+ # buf.set_string(0, 0, "Hello!", Style.new.fg(Color::GREEN).bold)
16
+ # puts buf.to_ansi
17
+ class Buffer
18
+ # @return [Rect] the rectangular bounds of this buffer
19
+ attr_reader :area
20
+ # @return [Hash{Array => Hash{Symbol => Object}}]
21
+ # image placements keyed by [x, y, content_hash] (or [x, y] when no content
22
+ # hash is given), registered via {#set_image} by {Widgets::Image#render}
23
+ attr_reader :image_placements
24
+
25
+ # Create a new Buffer.
26
+ #
27
+ # @overload initialize(width, height)
28
+ # Shorthand that creates a buffer at origin (0, 0).
29
+ # @param width [Integer] buffer width in columns
30
+ # @param height [Integer] buffer height in rows
31
+ #
32
+ # @overload initialize(area)
33
+ # @param area [Rect] the rectangular bounds
34
+ def initialize(area_or_width, height = nil)
35
+ @area = if height
36
+ Rect.new(0, 0, area_or_width, height)
37
+ else
38
+ area_or_width
39
+ end
40
+ @cells = Array.new(@area.area) { Cell.new }
41
+ @image_placements = {}
42
+ end
43
+
44
+ # Get the cell at position (x, y).
45
+ #
46
+ # @param x [Integer] column position
47
+ # @param y [Integer] row position
48
+ # @return [Cell, nil] the cell, or nil if out of bounds
49
+ def [](x, y)
50
+ idx = index(x, y)
51
+ idx ? @cells[idx] : nil
52
+ end
53
+
54
+ # Set the cell at position (x, y).
55
+ #
56
+ # @param x [Integer] column position
57
+ # @param y [Integer] row position
58
+ # @param cell [Cell] the cell to place
59
+ # @return [void]
60
+ def []=(x, y, cell)
61
+ idx = index(x, y)
62
+ @cells[idx] = cell if idx
63
+ end
64
+
65
+ # @!group Deprecated Unicode utilities — use the Unicode module instead
66
+
67
+ # @deprecated Use {Unicode.string_width} instead.
68
+ # @param string [String] the string to measure
69
+ # @return [Integer] display width in terminal columns
70
+ def self.string_width(string) = Unicode.string_width(string)
71
+ # @deprecated Use {Unicode.cluster_width} instead.
72
+ # @param cluster [String] a grapheme cluster (one or more codepoints)
73
+ # @return [Integer] 0, 1, or 2
74
+ def self.cluster_width(cluster) = Unicode.cluster_width(cluster)
75
+ # @deprecated Use {Unicode.truncate_to_width} instead.
76
+ # @param string [String] the string to truncate
77
+ # @param max_width [Integer] maximum display width in columns
78
+ # @return [String] truncated string
79
+ def self.truncate_to_width(string, max_width) = Unicode.truncate_to_width(string, max_width)
80
+ # @deprecated Use {Unicode.char_width} instead.
81
+ # @param ch [String] a single character
82
+ # @return [Integer] 0 for combining marks, 2 for wide characters, 1 otherwise
83
+ def self.char_width(ch) = Unicode.char_width(ch)
84
+ # @deprecated Use {Unicode.combining_mark?} instead.
85
+ # @param cp [Integer] Unicode codepoint
86
+ # @return [Boolean] true if codepoint is a combining mark (zero-width)
87
+ def self.combining_mark?(cp) = Unicode.combining_mark?(cp)
88
+ # @deprecated Use {Unicode.wide_char?} instead.
89
+ # @param cp [Integer] Unicode codepoint
90
+ # @return [Boolean] true if codepoint is a wide character (2-column)
91
+ def self.wide_char?(cp) = Unicode.wide_char?(cp)
92
+
93
+ # @!endgroup
94
+
95
+ # Write a styled string starting at (x, y).
96
+ #
97
+ # Iterates by grapheme cluster so that combining sequences (Devanagari
98
+ # matras/virama, emoji ZWJ, Latin diacritics) are stored as a single
99
+ # cell symbol. Wide clusters (2-column) mark the next cell as a spacer.
100
+ # Clips at buffer boundaries.
101
+ #
102
+ # @param x [Integer] starting column
103
+ # @param y [Integer] row
104
+ # @param string [String] text to write
105
+ # @param style [Style] style to apply (default: {Style::DEFAULT})
106
+ # @return [Integer] number of terminal columns consumed
107
+ def set_string(x, y, string, style = Style::DEFAULT)
108
+ return 0 unless y >= @area.y && y < @area.bottom
109
+
110
+ count = 0
111
+ col = x
112
+
113
+ string.grapheme_clusters.each do |gc|
114
+ break if col >= @area.right
115
+
116
+ w = Unicode.cluster_width(gc)
117
+
118
+ if col >= @area.x
119
+ idx = index(col, y)
120
+ if idx
121
+ @cells[idx].symbol = gc
122
+ @cells[idx].style = style
123
+ count += w
124
+
125
+ # For wide clusters, mark the next cell as a spacer
126
+ if w == 2 && col + 1 < @area.right
127
+ next_idx = index(col + 1, y)
128
+ if next_idx
129
+ @cells[next_idx].symbol = ""
130
+ @cells[next_idx].style = style
131
+ end
132
+ end
133
+ end
134
+ end
135
+ col += w
136
+ end
137
+ count
138
+ end
139
+
140
+ # Apply a style to all cells within the given area.
141
+ #
142
+ # Uses {Style#patch} so the applied style merges with existing cell styles.
143
+ #
144
+ # @param area [Rect] the area to style
145
+ # @param style [Style] the style to apply
146
+ # @return [void]
147
+ def set_style(area, style)
148
+ each_cell_in(area) do |idx|
149
+ @cells[idx].style = @cells[idx].style.patch(style)
150
+ end
151
+ end
152
+
153
+ # Diff this buffer against another, returning changed cells.
154
+ #
155
+ # Used by {Terminal#draw} for minimal screen updates.
156
+ #
157
+ # @param other [Buffer] the previous buffer to diff against
158
+ # @return [Array<Array(Integer, Integer, Cell)>] changed cells as [x, y, cell]
159
+ def diff(other)
160
+ changes = []
161
+ @cells.each_with_index do |cell, i|
162
+ next if cell == other.cells[i]
163
+
164
+ x = @area.x + (i % @area.width)
165
+ y = @area.y + (i / @area.width)
166
+ changes << [x, y, cell]
167
+ end
168
+ changes
169
+ end
170
+
171
+ # Reset all cells to empty (space character, default style).
172
+ #
173
+ # @return [void]
174
+ def reset!
175
+ reset_cells!
176
+ @image_placements.clear
177
+ end
178
+
179
+ # Reset cells only, keeping image placements — used by {Terminal} for
180
+ # full repaints, where placements must still diff against the previous
181
+ # frame to detect deletions.
182
+ #
183
+ # @return [void]
184
+ def reset_cells!
185
+ @cells.each(&:reset!)
186
+ end
187
+
188
+ # Register an image placement for Kitty graphics protocol rendering.
189
+ #
190
+ # Called by {Widgets::Image#render}. The placement is stored and later
191
+ # emitted by the backend during {Terminal#draw}.
192
+ #
193
+ # @param x [Integer] column position
194
+ # @param y [Integer] row position
195
+ # @param cols [Integer] display width in columns
196
+ # @param rows [Integer] display height in rows
197
+ # @param data [String] raw image bytes (PNG or RGBA)
198
+ # @param pixel_width [Integer] image width in pixels
199
+ # @param pixel_height [Integer] image height in pixels
200
+ # @param format [Symbol] :png or :rgba
201
+ # @param content_hash [String, nil] hash of image content for change detection
202
+ # @param src_x [Integer, nil] source rectangle X offset in pixels
203
+ # @param src_y [Integer, nil] source rectangle Y offset in pixels
204
+ # @param src_w [Integer, nil] source rectangle width in pixels
205
+ # @param src_h [Integer, nil] source rectangle height in pixels
206
+ # @return [void]
207
+ def set_image(x:, y:, cols:, rows:, data:, pixel_width:, pixel_height:, format:,
208
+ content_hash: nil, src_x: nil, src_y: nil, src_w: nil, src_h: nil)
209
+ # Key by position + content hash to detect when image changes at same position
210
+ key = content_hash ? [x, y, content_hash] : [x, y]
211
+
212
+ placement = {
213
+ x: x, y: y, cols: cols, rows: rows,
214
+ data: data, pixel_width: pixel_width, pixel_height: pixel_height,
215
+ format: format, content_hash: content_hash
216
+ }
217
+
218
+ # Add source rectangle if specified
219
+ if src_x && src_y && src_w && src_h
220
+ placement[:src_x] = src_x
221
+ placement[:src_y] = src_y
222
+ placement[:src_w] = src_w
223
+ placement[:src_h] = src_h
224
+ end
225
+
226
+ @image_placements[key] = placement
227
+ end
228
+
229
+ # Merge another buffer on top of this one (for overlays).
230
+ #
231
+ # Non-empty cells from the other buffer overwrite cells in this buffer.
232
+ #
233
+ # @param other [Buffer] the overlay buffer
234
+ # @return [void]
235
+ def merge(other)
236
+ other.each_cell_with_position do |x, y, cell|
237
+ next if cell.empty?
238
+
239
+ idx = index(x, y)
240
+ @cells[idx] = cell if idx
241
+ end
242
+ end
243
+
244
+ # Iterate over all cells with their absolute positions.
245
+ #
246
+ # @yield [x, y, cell] for each cell in the buffer
247
+ # @yieldparam x [Integer] column position
248
+ # @yieldparam y [Integer] row position
249
+ # @yieldparam cell [Cell]
250
+ # @return [void]
251
+ def each_cell_with_position
252
+ @cells.each_with_index do |cell, i|
253
+ x = @area.x + (i % @area.width)
254
+ y = @area.y + (i / @area.width)
255
+ yield x, y, cell
256
+ end
257
+ end
258
+
259
+ # Convert buffer contents to an ANSI-styled string for one-shot printing.
260
+ #
261
+ # Each row becomes a line separated by +\n+. Styles are emitted as
262
+ # ANSI SGR escape codes with reset at end of each styled row.
263
+ #
264
+ # @return [String] ANSI-escaped string ready for terminal output
265
+ #
266
+ # @example
267
+ # buf = Buffer.new(40, 5)
268
+ # buf.set_string(0, 0, "Hello", Style.new.fg(Color::GREEN))
269
+ # puts buf.to_ansi
270
+ def to_ansi
271
+ csi = "\e["
272
+ result = String.new(capacity: @area.area * 2)
273
+ prev_style = nil
274
+
275
+ @area.height.times do |row|
276
+ # Build the row content and track the last non-empty column
277
+ row_buf = String.new(capacity: @area.width * 2)
278
+ last_styled_col = -1
279
+ col = 0
280
+ display_col = 0 # track actual terminal column for cursor correction
281
+
282
+ while col < @area.width
283
+ cell = @cells[row * @area.width + col]
284
+
285
+ # Skip spacer cells from wide characters
286
+ if cell.symbol.empty?
287
+ col += 1
288
+ next
289
+ end
290
+
291
+ # Apply style changes
292
+ style = cell.style
293
+ unless style == prev_style
294
+ codes = style_to_sgr_codes(style, prev_style)
295
+ row_buf << "#{csi}#{codes.join(";")}m" unless codes.empty?
296
+ prev_style = style
297
+ end
298
+
299
+ row_buf << cell.symbol
300
+
301
+ # Track last non-default cell for trimming
302
+ unless cell.empty?
303
+ last_styled_col = col
304
+ end
305
+
306
+ w = Unicode.string_width(cell.symbol)
307
+ display_col += w
308
+ col += [w, 1].max
309
+
310
+ # After wide characters, emit cursor column to correct any
311
+ # terminal rendering drift (terminals may disagree about emoji width)
312
+ if w >= 2 && col < @area.width
313
+ row_buf << "#{csi}#{display_col + 1}G"
314
+ end
315
+ end
316
+
317
+ # Reset at end of row if we had any styling
318
+ if prev_style && prev_style != Style::DEFAULT
319
+ row_buf << "#{csi}0m"
320
+ prev_style = nil
321
+ end
322
+
323
+ result << row_buf
324
+ result << "\n" unless row == @area.height - 1
325
+ end
326
+
327
+ result
328
+ end
329
+
330
+ # Print the buffer's ANSI output to a stream.
331
+ #
332
+ # Convenience wrapper around {#to_ansi}.
333
+ #
334
+ # @param output [IO] output stream (default: $stdout)
335
+ # @return [void]
336
+ def print(output = $stdout)
337
+ output.write(to_ansi)
338
+ end
339
+
340
+ protected
341
+
342
+ attr_reader :cells
343
+
344
+ private
345
+
346
+ def index(x, y)
347
+ return nil unless x >= @area.x && x < @area.right &&
348
+ y >= @area.y && y < @area.bottom
349
+
350
+ (y - @area.y) * @area.width + (x - @area.x)
351
+ end
352
+
353
+ def char_width(ch) = Unicode.char_width(ch)
354
+
355
+ # Generate SGR codes to transition from prev_style to style
356
+ def style_to_sgr_codes(style, prev_style)
357
+ codes = []
358
+ prev = prev_style || Style::DEFAULT
359
+
360
+ # Check if we need a full reset
361
+ removed_mods = prev.modifiers & ~style.modifiers
362
+ if removed_mods != 0 || (prev.fg && !style.fg) || (prev.bg && !style.bg)
363
+ codes << "0"
364
+ prev = Style::DEFAULT
365
+ end
366
+
367
+ # Foreground
368
+ if style.fg != prev.fg
369
+ codes << (style.fg ? style.fg.to_fg_ansi : "39")
370
+ end
371
+
372
+ # Background
373
+ if style.bg != prev.bg
374
+ codes << (style.bg ? style.bg.to_bg_ansi : "49")
375
+ end
376
+
377
+ # Modifiers added
378
+ added = style.modifiers & ~prev.modifiers
379
+ Modifier::SGR_ON.each do |flag, code|
380
+ codes << code if Modifier.contains?(added, flag)
381
+ end
382
+
383
+ codes
384
+ end
385
+
386
+ def each_cell_in(area)
387
+ y_start = [area.y, @area.y].max
388
+ y_end = [area.bottom, @area.bottom].min
389
+ x_start = [area.x, @area.x].max
390
+ x_end = [area.right, @area.right].min
391
+
392
+ (y_start...y_end).each do |y|
393
+ (x_start...x_end).each do |x|
394
+ idx = index(x, y)
395
+ yield idx if idx
396
+ end
397
+ end
398
+ end
399
+ end
400
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyTUI
4
+ # The atomic unit of the rendering grid: a single character with a style.
5
+ #
6
+ # Each cell in a {Buffer} holds one character (or empty string for wide-char
7
+ # spacers) and an associated {Style}.
8
+ class Cell
9
+ # @return [String] the character displayed in this cell
10
+ attr_accessor :symbol
11
+ # @return [Style] the style applied to this cell
12
+ attr_accessor :style
13
+
14
+ # Default empty character (space).
15
+ EMPTY_CHAR = " "
16
+
17
+ # @param symbol [String] the character (default: space)
18
+ # @param style [Style] the style (default: {Style::DEFAULT})
19
+ def initialize(symbol = EMPTY_CHAR, style = Style::DEFAULT)
20
+ @symbol = symbol
21
+ @style = style
22
+ end
23
+
24
+ # Reset this cell to empty (space character, default style).
25
+ #
26
+ # @return [self]
27
+ def reset!
28
+ @symbol = EMPTY_CHAR
29
+ @style = Style::DEFAULT
30
+ self
31
+ end
32
+
33
+ # Set this cell's character and style.
34
+ #
35
+ # @param symbol [String] the character
36
+ # @param style [Style] the style (default: {Style::DEFAULT})
37
+ # @return [self]
38
+ def set(symbol, style = Style::DEFAULT)
39
+ @symbol = symbol
40
+ @style = style
41
+ self
42
+ end
43
+
44
+ # Compare by symbol and style.
45
+ #
46
+ # @param other [Object] object to compare against
47
+ # @return [Boolean] true if +other+ is a Cell with an equal symbol and style
48
+ def ==(other)
49
+ other.is_a?(Cell) && @symbol == other.symbol && @style == other.style
50
+ end
51
+
52
+ # @return [Boolean] true if this cell has default content (space + default style)
53
+ def empty? = @symbol == EMPTY_CHAR && @style == Style::DEFAULT
54
+ end
55
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyTUI
4
+ # Terminal color supporting named (16), indexed (256), and RGB truecolor.
5
+ #
6
+ # Use the predefined constants for the standard 16 colors, or the factory
7
+ # methods for indexed and RGB colors.
8
+ #
9
+ # @example Named colors
10
+ # Style.new.fg(Color::RED).bg(Color::BLACK)
11
+ #
12
+ # @example 256-color palette
13
+ # Style.new.fg(Color.indexed(208)) # orange
14
+ #
15
+ # @example RGB truecolor
16
+ # Style.new.fg(Color.rgb(255, 128, 0))
17
+ class Color
18
+ # @return [Symbol] color kind — +:named+, +:indexed+, +:rgb+, or +:reset+
19
+ attr_reader :kind
20
+ # @return [Integer, Array<Integer>, nil] color value (index, [r,g,b], or named index)
21
+ attr_reader :value
22
+
23
+ # @param kind [Symbol] color kind
24
+ # @param value [Integer, Array<Integer>, nil] color value
25
+ def initialize(kind, value = nil)
26
+ @kind = kind
27
+ @value = value
28
+ end
29
+
30
+ # Value equality: same kind and value.
31
+ #
32
+ # @param other [Object] object to compare
33
+ # @return [Boolean] true if +other+ is a Color with equal {#kind} and {#value}
34
+ def ==(other)
35
+ other.is_a?(Color) && @kind == other.kind && @value == other.value
36
+ end
37
+ alias_method :eql?, :==
38
+
39
+ # @return [Integer] hash derived from {#kind} and {#value}
40
+ def hash = [@kind, @value].hash
41
+
42
+ class << self
43
+ # Create a color from the 256-color indexed palette.
44
+ #
45
+ # @param n [Integer] color index (0-255)
46
+ # @return [Color]
47
+ # @raise [ArgumentError] if n is outside 0-255
48
+ def indexed(n)
49
+ raise ArgumentError, "Color index must be 0-255, got #{n}" unless (0..255).cover?(n)
50
+
51
+ new(:indexed, n).freeze
52
+ end
53
+
54
+ # Create an RGB truecolor.
55
+ #
56
+ # @param r [Integer] red component (0-255)
57
+ # @param g [Integer] green component (0-255)
58
+ # @param b [Integer] blue component (0-255)
59
+ # @return [Color]
60
+ def rgb(r, g, b)
61
+ new(:rgb, [r, g, b].freeze).freeze
62
+ end
63
+ end
64
+
65
+ # Generate ANSI foreground escape code fragment (without ESC[ prefix or m suffix).
66
+ #
67
+ # @return [String] SGR code fragment (e.g., "31", "38;5;208", "38;2;255;128;0")
68
+ def to_fg_ansi
69
+ case @kind
70
+ when :reset then "39"
71
+ when :named then (@value < 8 ? "#{30 + @value}" : "#{90 + @value - 8}")
72
+ when :indexed then "38;5;#{@value}"
73
+ when :rgb then "38;2;#{@value[0]};#{@value[1]};#{@value[2]}"
74
+ end
75
+ end
76
+
77
+ # Generate ANSI background escape code fragment.
78
+ #
79
+ # @return [String] SGR code fragment (e.g., "41", "48;5;208", "48;2;255;128;0")
80
+ def to_bg_ansi
81
+ case @kind
82
+ when :reset then "49"
83
+ when :named then (@value < 8 ? "#{40 + @value}" : "#{100 + @value - 8}")
84
+ when :indexed then "48;5;#{@value}"
85
+ when :rgb then "48;2;#{@value[0]};#{@value[1]};#{@value[2]}"
86
+ end
87
+ end
88
+
89
+ # @!group Standard Named Colors (16-color palette)
90
+
91
+ # Terminal default color (SGR 39 / 49).
92
+ RESET = new(:reset).freeze
93
+ # Black (SGR 30 / 40).
94
+ BLACK = new(:named, 0).freeze
95
+ # Red (SGR 31 / 41).
96
+ RED = new(:named, 1).freeze
97
+ # Green (SGR 32 / 42).
98
+ GREEN = new(:named, 2).freeze
99
+ # Yellow (SGR 33 / 43).
100
+ YELLOW = new(:named, 3).freeze
101
+ # Blue (SGR 34 / 44).
102
+ BLUE = new(:named, 4).freeze
103
+ # Magenta (SGR 35 / 45).
104
+ MAGENTA = new(:named, 5).freeze
105
+ # Cyan (SGR 36 / 46).
106
+ CYAN = new(:named, 6).freeze
107
+ # White (SGR 37 / 47).
108
+ WHITE = new(:named, 7).freeze
109
+ # Bright black, usually rendered as gray (SGR 90 / 100).
110
+ BRIGHT_BLACK = new(:named, 8).freeze
111
+ # Bright red (SGR 91 / 101).
112
+ BRIGHT_RED = new(:named, 9).freeze
113
+ # Bright green (SGR 92 / 102).
114
+ BRIGHT_GREEN = new(:named, 10).freeze
115
+ # Bright yellow (SGR 93 / 103).
116
+ BRIGHT_YELLOW = new(:named, 11).freeze
117
+ # Bright blue (SGR 94 / 104).
118
+ BRIGHT_BLUE = new(:named, 12).freeze
119
+ # Bright magenta (SGR 95 / 105).
120
+ BRIGHT_MAGENTA = new(:named, 13).freeze
121
+ # Bright cyan (SGR 96 / 106).
122
+ BRIGHT_CYAN = new(:named, 14).freeze
123
+ # Bright white (SGR 97 / 107).
124
+ BRIGHT_WHITE = new(:named, 15).freeze
125
+
126
+ # @!endgroup
127
+
128
+ # @!group Color Aliases
129
+
130
+ # Alias for {BRIGHT_BLACK}.
131
+ GRAY = BRIGHT_BLACK
132
+ # Alias for {BRIGHT_BLACK}.
133
+ GREY = BRIGHT_BLACK
134
+ # Alias for {BRIGHT_BLACK}.
135
+ DARK_GRAY = BRIGHT_BLACK
136
+ # Alias for {BRIGHT_BLACK}.
137
+ DARK_GREY = BRIGHT_BLACK
138
+ # Alias for {BRIGHT_RED}.
139
+ LIGHT_RED = BRIGHT_RED
140
+ # Alias for {BRIGHT_GREEN}.
141
+ LIGHT_GREEN = BRIGHT_GREEN
142
+ # Alias for {BRIGHT_YELLOW}.
143
+ LIGHT_YELLOW = BRIGHT_YELLOW
144
+ # Alias for {BRIGHT_BLUE}.
145
+ LIGHT_BLUE = BRIGHT_BLUE
146
+ # Alias for {BRIGHT_MAGENTA}.
147
+ LIGHT_MAGENTA = BRIGHT_MAGENTA
148
+ # Alias for {BRIGHT_CYAN}.
149
+ LIGHT_CYAN = BRIGHT_CYAN
150
+
151
+ # @!endgroup
152
+ end
153
+ end