tuile 0.15.0 → 0.17.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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +229 -80
  3. data/README.md +49 -24
  4. data/book/02-repaint.md +47 -19
  5. data/book/03-layout.md +98 -49
  6. data/book/04-event-loop.md +17 -16
  7. data/book/05-focus.md +106 -34
  8. data/book/06-theming.md +108 -38
  9. data/book/07-components.md +249 -46
  10. data/book/08-testing.md +134 -32
  11. data/book/10-locale.md +3 -3
  12. data/book/README.md +11 -10
  13. data/examples/file_commander.rb +52 -32
  14. data/examples/hello_world.rb +18 -5
  15. data/examples/sampler.rb +576 -146
  16. data/lib/tuile/buffer.rb +12 -1
  17. data/lib/tuile/canvas/backend.rb +46 -0
  18. data/lib/tuile/canvas.rb +212 -0
  19. data/lib/tuile/color.rb +38 -9
  20. data/lib/tuile/component/abstract_string_field.rb +96 -97
  21. data/lib/tuile/component/abstract_wrapping_field.rb +99 -58
  22. data/lib/tuile/component/big_decimal_field.rb +7 -6
  23. data/lib/tuile/component/button.rb +27 -19
  24. data/lib/tuile/component/checkbox.rb +21 -19
  25. data/lib/tuile/component/checkbox_group.rb +17 -18
  26. data/lib/tuile/component/combo_box.rb +69 -64
  27. data/lib/tuile/component/confirm_window.rb +34 -27
  28. data/lib/tuile/component/date_field.rb +50 -18
  29. data/lib/tuile/component/date_time_field.rb +319 -0
  30. data/lib/tuile/component/fill.rb +93 -0
  31. data/lib/tuile/component/float_field.rb +7 -6
  32. data/lib/tuile/component/form_item.rb +250 -0
  33. data/lib/tuile/component/form_layout.rb +206 -0
  34. data/lib/tuile/component/has_bad_input.rb +99 -28
  35. data/lib/tuile/component/has_caption.rb +14 -5
  36. data/lib/tuile/component/has_content.rb +8 -15
  37. data/lib/tuile/component/has_placeholder.rb +1 -1
  38. data/lib/tuile/component/has_validation.rb +40 -14
  39. data/lib/tuile/component/has_value.rb +71 -17
  40. data/lib/tuile/component/integer_field.rb +7 -6
  41. data/lib/tuile/component/label.rb +8 -15
  42. data/lib/tuile/component/layout/absolute.rb +86 -0
  43. data/lib/tuile/component/layout/box.rb +38 -60
  44. data/lib/tuile/component/layout.rb +127 -13
  45. data/lib/tuile/component/list.rb +233 -120
  46. data/lib/tuile/component/list_dropdown.rb +151 -91
  47. data/lib/tuile/component/menu_bar/cascade.rb +102 -32
  48. data/lib/tuile/component/menu_bar.rb +102 -82
  49. data/lib/tuile/component/notification.rb +76 -49
  50. data/lib/tuile/component/overlay.rb +217 -58
  51. data/lib/tuile/component/password_field.rb +1 -8
  52. data/lib/tuile/component/picker_window.rb +41 -17
  53. data/lib/tuile/component/popup.rb +15 -26
  54. data/lib/tuile/component/progress_bar.rb +17 -11
  55. data/lib/tuile/component/radio_group.rb +16 -17
  56. data/lib/tuile/component/scroller.rb +266 -0
  57. data/lib/tuile/component/select.rb +26 -43
  58. data/lib/tuile/component/slot.rb +4 -5
  59. data/lib/tuile/component/tab_sheet.rb +27 -34
  60. data/lib/tuile/component/tabs.rb +49 -34
  61. data/lib/tuile/component/text_area/wrapped_text.rb +1 -1
  62. data/lib/tuile/component/text_area.rb +32 -28
  63. data/lib/tuile/component/text_field.rb +68 -50
  64. data/lib/tuile/component/text_view.rb +157 -89
  65. data/lib/tuile/component/time_field.rb +51 -21
  66. data/lib/tuile/component/vertical_scroll_bar.rb +257 -0
  67. data/lib/tuile/component/window.rb +27 -26
  68. data/lib/tuile/component.rb +653 -323
  69. data/lib/tuile/component_background.rb +177 -0
  70. data/lib/tuile/component_util.rb +43 -0
  71. data/lib/tuile/event.rb +29 -0
  72. data/lib/tuile/event_queue.rb +18 -4
  73. data/lib/tuile/fake_event_queue.rb +1 -1
  74. data/lib/tuile/fake_screen.rb +120 -7
  75. data/lib/tuile/keys.rb +15 -6
  76. data/lib/tuile/layout_pass.rb +180 -0
  77. data/lib/tuile/listeners.rb +219 -0
  78. data/lib/tuile/mouse/router.rb +233 -0
  79. data/lib/tuile/mouse.rb +244 -0
  80. data/lib/tuile/point.rb +6 -0
  81. data/lib/tuile/rect.rb +33 -0
  82. data/lib/tuile/screen.rb +510 -138
  83. data/lib/tuile/screen_pane.rb +185 -67
  84. data/lib/tuile/strict_layout.rb +127 -0
  85. data/lib/tuile/styled_string.rb +144 -14
  86. data/lib/tuile/testing/gestures.rb +35 -0
  87. data/lib/tuile/testing.rb +316 -42
  88. data/lib/tuile/theme.rb +192 -53
  89. data/lib/tuile/theme_def.rb +4 -0
  90. data/lib/tuile/version.rb +1 -1
  91. data/lib/tuile.rb +53 -0
  92. data/sig/tuile.rbs +6084 -1507
  93. metadata +19 -17
  94. data/COMPARISON.md +0 -101
  95. data/DECISIONS.md +0 -8562
  96. data/TERMINOLOGY.md +0 -85
  97. data/ideas/arrow-key-navigation.md +0 -221
  98. data/ideas/binder.md +0 -177
  99. data/ideas/composite-field.md +0 -77
  100. data/ideas/focus-accent.md +0 -116
  101. data/ideas/form-layout.md +0 -151
  102. data/ideas/hover/probe.rb +0 -241
  103. data/ideas/hover/probe_spec.rb +0 -82
  104. data/ideas/hover.md +0 -909
  105. data/ideas/modal-backdrop.md +0 -24
  106. data/ideas/new-components.md +0 -144
  107. data/ideas/per-component-buffers.md +0 -55
  108. data/lib/tuile/mouse_event.rb +0 -68
  109. data/lib/tuile/vertical_scroll_bar.rb +0 -122
data/lib/tuile/buffer.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Tuile
4
4
  # An in-memory grid of styled cells mirroring the terminal screen. This is
5
5
  # the back buffer behind flicker-free rendering: components paint into it
6
- # (via {#set_text} / {#set_char} / {#fill}) instead of writing escape
6
+ # through a {Canvas}, whose {Canvas::Backend} it is, instead of writing escape
7
7
  # sequences straight to the terminal, and {#flush} emits the minimal escape
8
8
  # string needed to bring a terminal — one that already matches the buffer's
9
9
  # state as of the previous flush — up to date. Only cells that actually
@@ -34,6 +34,10 @@ module Tuile
34
34
  # Overwriting either half of a wide glyph blanks the orphaned half, so the
35
35
  # grid never holds a dangling continuation or a headless one.
36
36
  class Buffer
37
+ # The three primitives below are exactly {Canvas::Backend}'s, so a canvas
38
+ # paints into a buffer with no adapter between them.
39
+ include Canvas::Backend
40
+
37
41
  # One screen cell: a single grapheme cluster, the {StyledString::Style} it's
38
42
  # drawn in, and a dirty flag. Mutable by design (see {Buffer} "Dirty
39
43
  # tracking") — the grid rewrites cells in place. A continuation cell (right
@@ -317,6 +321,13 @@ module Tuile
317
321
  region_cells(rect).map { |row| row.map(&:grapheme).join }
318
322
  end
319
323
 
324
+ # {#region_text} over the whole buffer, for one {Testing.paint} returned:
325
+ #
326
+ # Testing.paint(window).text # => ["┌Caption─────┐", "│ alpha │", …]
327
+ #
328
+ # @return [Array<String>] the plain text of every row, top to bottom.
329
+ def text = region_text(Rect.new(0, 0, @width, @height))
330
+
320
331
  # @param rect [Rect]
321
332
  # @return [Array<String>] each row within `rect` rendered to ANSI, top to
322
333
  # bottom — byte-identical to what a component's per-row `set_text` over
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tuile
4
+ class Canvas
5
+ # Where a {Canvas}'s cells land: three primitives taking a fully resolved
6
+ # style. Include it to *be* one — {Tuile::Buffer} does, and needs no adapter:
7
+ #
8
+ # Canvas.new(screen.buffer)
9
+ #
10
+ # A backend is dumb by contract: no theme, no component, no background
11
+ # chain, no translation, and no default arguments — the canvas resolves all
12
+ # of that and hands down a whole style at a coordinate in the backend's own
13
+ # grid. What varies here is only *where* the cells go;
14
+ # how a write is transformed on the way there belongs to the {Canvas}, which
15
+ # is final. See `D_canvas`.
16
+ #
17
+ # **`include`, never `prepend`.** A class's own methods win over an included
18
+ # module's, which is what lets {Tuile::Buffer} carry this while defining all
19
+ # three itself; prepended, these bodies would sit *ahead* of the class and
20
+ # every write would raise.
21
+ #
22
+ # Out-of-range writes are **dropped, not raised** — the terminal-edge clip
23
+ # belongs here, which is what lets a component paint a row the terminal is
24
+ # too narrow for without checking first.
25
+ module Backend
26
+ # @param x [Integer] starting column, in this backend's own grid —
27
+ # the canvas has already applied its {Canvas#origin}.
28
+ # @param y [Integer] row, likewise.
29
+ # @param styled [StyledString] the text of one row; newlines are not handled.
30
+ # @return [void]
31
+ def set_text(x, y, styled) = raise(NotImplementedError, "#{self.class}#set_text")
32
+
33
+ # @param x [Integer] column, in this backend's own grid.
34
+ # @param y [Integer] row, likewise.
35
+ # @param grapheme [String] one grapheme cluster.
36
+ # @param style [StyledString::Style] fully resolved.
37
+ # @return [void]
38
+ def set_char(x, y, grapheme, style) = raise(NotImplementedError, "#{self.class}#set_char")
39
+
40
+ # @param rect [Rect] in this backend's own grid.
41
+ # @param style [StyledString::Style] fully resolved; only its `bg` shows.
42
+ # @return [void]
43
+ def fill(rect, style) = raise(NotImplementedError, "#{self.class}#fill")
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tuile
4
+ # The paint context a component draws through: a {Backend} that says where
5
+ # cells land, plus the state every write needs — the {#origin} that puts
6
+ # `(0, 0)` at the component's own top-left, and the background to fill in
7
+ # behind content that carries none.
8
+ #
9
+ # A component never builds one. It paints onto the canvas its
10
+ # {Component#repaint} was handed, already loaded with that component's
11
+ # {ComponentBackground#effective} and positioned at its {Component#rect}, and
12
+ # derives a second for the cells that are not its own ink:
13
+ #
14
+ # def repaint(canvas)
15
+ # canvas.set_text(0, 0, label) # my well
16
+ # canvas.with(bg_color: bg.ambient) { _1.fill(tail) } # not my ink
17
+ # end
18
+ #
19
+ # Three methods, in **paint coordinates**: `(0, 0)` is the component's own
20
+ # top-left, not the screen's, and {Component#local_rect} is its whole rect
21
+ # over here. Passing {Component#rect} instead lands the write at twice the
22
+ # offset, silently. Everything outside painting stays screen-space — `rect`,
23
+ # {Mouse::Event}, {Component#cursor_position} — which `D_canvas` argues.
24
+ #
25
+ # A `nil` {#bg_color} is the terminal default, never "inherit" — inheritance
26
+ # is resolved before a canvas is built ({Screen#canvas_for}).
27
+ #
28
+ # == Implementation details
29
+ #
30
+ # Frozen, and final by convention: {#with} yields a *derived* canvas and never
31
+ # mutates, so there are no save/restore pairs and no state to leave dangling
32
+ # for whatever paints next. Subclassing is not the extension point — {Backend}
33
+ # is, and it is the half that varies. See `D_canvas`.
34
+ #
35
+ # UI-thread-confined.
36
+ class Canvas
37
+ # @return [Backend] where the cells land.
38
+ attr_reader :backend
39
+
40
+ # @return [Color, nil] the background painted behind content that states
41
+ # none; `nil` leaves the terminal default showing.
42
+ attr_reader :bg_color
43
+
44
+ # Where this canvas's `(0, 0)` sits in {#backend} coordinates — the
45
+ # component's {Component#rect}`.top_left`, as {Screen#canvas_for} built it.
46
+ # A component never reads it: adding the offset back by hand is the one
47
+ # thing it exists to make unnecessary.
48
+ # @return [Point]
49
+ attr_reader :origin
50
+
51
+ # The region of the {#backend}'s grid this canvas may write to —
52
+ # {Screen#clip_for}'s answer moved into backend coordinates, or `nil` for a
53
+ # canvas nobody bounded, which costs one test per write and nothing else.
54
+ # Every canvas {Screen#canvas_for} builds carries one; {Screen#canvas}, the
55
+ # root, is the `nil` case.
56
+ #
57
+ # In **backend** coordinates, like {#origin} and unlike every argument the
58
+ # three paint methods take: a canvas's *state* says where it sits in the
59
+ # world, its arguments are in paint coordinates (`D_clip`).
60
+ #
61
+ # An {Rect#empty? empty} clip is not `nil`: it means *paint nothing*, and it
62
+ # is what a component that can show nothing gets — collapsed, or scrolled
63
+ # clean out of its viewport. Every write is judged against the clip on its
64
+ # own terms, so this needs no special case; it is simply the case where they
65
+ # all fail.
66
+ #
67
+ # The one cell it does not protect: {Buffer#put_char} blanks the head of a
68
+ # wide glyph whose continuation half a clipped write overwrites, one column
69
+ # outside. That is the terminal's physical truth, and better than the
70
+ # dangling half-glyph the alternative leaves.
71
+ # @return [Rect, nil]
72
+ attr_reader :clip
73
+
74
+ # @param backend [Backend]
75
+ # @param bg_color [Color, nil] already resolved — a canvas consults no
76
+ # component and no theme.
77
+ # @param origin [Point] the {#origin}; the default paints in backend
78
+ # coordinates, which is what {Screen#canvas} is.
79
+ # @param clip [Rect, nil] the {#clip}, in backend coordinates —
80
+ # {Screen#canvas_for} converts.
81
+ # @raise [Error] if `backend` does not include {Backend}, which is worth
82
+ # catching here rather than mid-paint.
83
+ def initialize(backend, bg_color: nil, origin: Point::ZERO, clip: nil)
84
+ raise Error, "#{backend.class} must include Tuile::Canvas::Backend" unless backend.is_a?(Backend)
85
+
86
+ @backend = backend
87
+ @bg_color = bg_color
88
+ @origin = origin
89
+ @clip = clip
90
+ @blank_style = bg_color ? StyledString::Style.new(bg: bg_color) : StyledString::Style::DEFAULT
91
+ freeze
92
+ end
93
+
94
+ # Yields a canvas onto the same backend with a different background, for the
95
+ # span of the block — the *only* way to change it.
96
+ #
97
+ # canvas.with(bg_color: bg.ambient) do |c|
98
+ # c.fill(right)
99
+ # c.fill(below)
100
+ # end
101
+ #
102
+ # The receiver is untouched, so nothing has to be restored afterwards and
103
+ # the block cannot leave the wrong background on for whatever paints next.
104
+ # The {#origin} and the {#clip} ride along, so a derived canvas paints in
105
+ # the same coordinates and is bounded the same way.
106
+ # @param bg_color [Color, nil] the background inside the block.
107
+ # @return [Object] the block's value.
108
+ # @raise [Error] if no block is given — a derived canvas nobody scoped is
109
+ # the dangling state this shape exists to make impossible.
110
+ def with(bg_color:)
111
+ raise Error, "Canvas#with needs a block: with(bg_color:) { |canvas| … }" unless block_given?
112
+ return yield self if bg_color == @bg_color
113
+
114
+ yield Canvas.new(@backend, bg_color:, origin: @origin, clip: @clip)
115
+ end
116
+
117
+ # Writes a {StyledString}, filling {#bg_color} behind any span that states
118
+ # no background of its own — so an inherited tint, or an invalid field's
119
+ # error well, shows through content the component did not colour.
120
+ # @param x [Integer] starting column, relative to {#origin}.
121
+ # @param y [Integer] row, relative to {#origin}.
122
+ # @param styled [StyledString] the text of one row; newlines are not handled.
123
+ # @return [void]
124
+ def set_text(x, y, styled)
125
+ col = x + @origin.x
126
+ row = y + @origin.y
127
+ return @backend.set_text(col, row, styled.under_bg(@bg_color)) if @clip.nil?
128
+ return unless clipped_row?(row)
129
+
130
+ write_clipped_text(col, row, styled)
131
+ end
132
+
133
+ # {#set_text}'s single-grapheme counterpart.
134
+ # @param x [Integer] column, relative to {#origin}.
135
+ # @param y [Integer] row, relative to {#origin}.
136
+ # @param grapheme [String] one grapheme cluster.
137
+ # @param style [StyledString::Style] its `bg`, when set, wins over {#bg_color}.
138
+ # @return [void]
139
+ def set_char(x, y, grapheme, style = StyledString::Style::DEFAULT)
140
+ style = style.merge(bg: @bg_color) if @bg_color && style.bg.nil?
141
+ col = x + @origin.x
142
+ row = y + @origin.y
143
+ return @backend.set_char(col, row, grapheme, style) if @clip.nil?
144
+ return unless clipped_row?(row)
145
+
146
+ # A zero-width cluster still lands in one cell, and that cell is what the
147
+ # clip judges.
148
+ width = [Buffer.display_width(grapheme), 1].max
149
+ from = [col, @clip.left].max
150
+ to = [col + width, @clip.left + @clip.width].min
151
+ return if to <= from
152
+
153
+ # Half of a wide glyph is unrenderable, so the columns the clip keeps are
154
+ # blanked instead — {Buffer#put_char}'s own policy at the terminal's edge.
155
+ return @backend.fill(Rect.new(from, row, to - from, 1), @blank_style) if to - from < width
156
+
157
+ @backend.set_char(col, row, grapheme, style)
158
+ end
159
+
160
+ # Blanks `area` to {#bg_color}.
161
+ #
162
+ # Only for cells nothing is about to paint over: {Buffer::Cell#set} dirties
163
+ # on a real change, so blanking a cell that is then redrawn re-emits it.
164
+ # @param area [Rect] relative to {#origin} — {Component#local_rect} for the
165
+ # whole of a component, never its {Component#rect}.
166
+ # @return [void]
167
+ def fill(area)
168
+ # The early-out {#set_text} gets from `clipped_row?`: an empty clip keeps
169
+ # no cell at all, so neither rectangle below is worth building.
170
+ return if @clip && @clip.empty?
171
+
172
+ area = area.moved_by(@origin)
173
+ @backend.fill(@clip.nil? ? area : area.intersect(@clip), @blank_style)
174
+ end
175
+
176
+ private
177
+
178
+ # @param row [Integer] a row in backend coordinates.
179
+ # @return [Boolean] whether {#clip} keeps it. Callers have already checked
180
+ # that there *is* a clip.
181
+ def clipped_row?(row) = row >= @clip.top && row < @clip.top + @clip.height
182
+
183
+ # {#set_text} for the case that has to think: cut `styled` to the clip's
184
+ # columns and write what survived where it really belongs.
185
+ #
186
+ # {StyledString#slice} **drops** a cluster the boundary falls inside rather
187
+ # than splitting one, at the start as readily as at the end — so the kept
188
+ # text can begin a column later than the cut asked for, and the write
189
+ # position is derived from it rather than assumed. The column a dropped
190
+ # cluster half-covered is blanked (`D_clip`).
191
+ # @param col [Integer] starting column, in backend coordinates.
192
+ # @param row [Integer] row, likewise; the caller has checked the clip keeps it.
193
+ # @param styled [StyledString] the text of one row, uncoloured as handed in.
194
+ # @return [void]
195
+ def write_clipped_text(col, row, styled)
196
+ right = @clip.left + @clip.width
197
+ width = styled.display_width
198
+ from = [col, @clip.left].max
199
+ to = [col + width, right].min
200
+ return if to <= from
201
+
202
+ kept = col < @clip.left ? styled.slice(@clip.left - col, width) : styled
203
+ start = col + width - kept.display_width
204
+ kept = kept.slice(0, right - start) if start + kept.display_width > right
205
+ stop = start + kept.display_width
206
+
207
+ @backend.fill(Rect.new(from, row, start - from, 1), @blank_style) if start > from
208
+ @backend.fill(Rect.new(stop, row, to - stop, 1), @blank_style) if to > stop
209
+ @backend.set_text(start, row, kept.under_bg(@bg_color)) unless kept.empty?
210
+ end
211
+ end
212
+ end
data/lib/tuile/color.rb CHANGED
@@ -122,6 +122,27 @@ module Tuile
122
122
  # @return [Symbol, Integer, Array<Integer>]
123
123
  attr_reader :value
124
124
 
125
+ # This color's red, green and blue, for an app doing color math — a
126
+ # contrast check, a tint derived from {Screen#background_color}:
127
+ #
128
+ # Color.hex("#ff6400").rgb # => [255, 100, 0]
129
+ # Color::DEEP_SKY_BLUE1.rgb # => [0, 175, 255], palette cell 39
130
+ # Color::MAGENTA.rgb # => nil, the terminal's scheme decides
131
+ # Color.palette(5).rgb # => nil, likewise
132
+ #
133
+ # `nil` for the 16 named colors, in either spelling, because the scheme
134
+ # remaps them and any answer would be a guess. Indices 16..255 answer
135
+ # xterm's cube and grey ramp: a terminal *may* redefine those too (OSC 4),
136
+ # but in practice none does.
137
+ #
138
+ # @return [Array<Integer>, nil] frozen, each channel 0..255.
139
+ def rgb
140
+ case @value
141
+ when Array then @value
142
+ when Integer then PALETTE_RGB[@value]
143
+ end
144
+ end
145
+
125
146
  # SGR parameter codes for emitting this color as either a foreground
126
147
  # (`target: :fg`) or background (`target: :bg`). Returned as an array so
127
148
  # callers can splice them into a multi-attribute SGR (e.g. bold + color).
@@ -214,17 +235,12 @@ module Tuile
214
235
 
215
236
  private
216
237
 
217
- # This color's RGB — the palette cell's own coordinates when the value is
218
- # an index. Only ever asked of a non-Symbol value; a named color has no
219
- # RGB of its own, since the terminal's scheme decides what it looks like.
238
+ # {#rgb}, except an index 0..15 answers xterm's default for it — the guess
239
+ # `:ansi16` quantization needs something to match against, and {#rgb}
240
+ # must not hand out. Only ever asked of a non-Symbol value.
220
241
  # @return [Array<Integer>] red, green and blue, each 0..255.
221
242
  def rgb_triple
222
- return @value if @value.is_a?(Array)
223
- return ANSI16_RGB[@value] if @value < 16
224
- return [8 + (10 * (@value - 232))] * 3 if @value >= 232
225
-
226
- cube = @value - 16
227
- [CUBE_LEVELS[cube / 36], CUBE_LEVELS[(cube / 6) % 6], CUBE_LEVELS[cube % 6]]
243
+ rgb || ANSI16_RGB[@value]
228
244
  end
229
245
 
230
246
  # Written flat — destructured rather than splatted, `x * x` rather than
@@ -293,6 +309,19 @@ module Tuile
293
309
  CUBE_INDEX = Array.new(256) { |c| (0...6).min_by { |i| (CUBE_LEVELS[i] - c).abs } }.freeze
294
310
  private_constant :CUBE_INDEX
295
311
 
312
+ # Palette index 0..255 → its RGB, frozen, or nil for 0..15 — what {#rgb}
313
+ # answers for an index, precomputed so it allocates nothing.
314
+ # @return [Array<Array<Integer>, nil>]
315
+ PALETTE_RGB = Array.new(256) do |index|
316
+ if index < 16 then nil
317
+ elsif index >= 232 then ([8 + (10 * (index - 232))] * 3).freeze
318
+ else
319
+ cube = index - 16
320
+ [CUBE_LEVELS[cube / 36], CUBE_LEVELS[(cube / 6) % 6], CUBE_LEVELS[cube % 6]].freeze
321
+ end
322
+ end.freeze
323
+ private_constant :PALETTE_RGB
324
+
296
325
  # xterm's default RGB for each of the 16 named colors, in {COLOR_SYMBOLS}
297
326
  # order — what `:ansi16` quantization matches against. A terminal scheme
298
327
  # may redefine these; see {#quantize}.