fix(terminal): quantize colors against full xterm palette
- commit
- f223378bc39565c61f7eb8a796ad4750b1530121
- author
- brice · · 2026-08-18 18:36 UTC
- parent
- 385a8c0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +178 -22
M internal/usecase/daemon/terminal_color.go +38 -4
| @@ -28,9 +28,43 @@ func adaptFrameColors(frame renderer.Frame, trueColor bool) renderer.Frame { | |||
| 28 | 28 | return adapted | |
| 29 | 29 | } | |
| 30 | 30 | ||
| 31 | + | var ( | |
| 32 | + | xterm256ColorLevels = [...]uint8{0, 95, 135, 175, 215, 255} | |
| 33 | + | xterm256GrayLevels = [...]uint8{8, 18, 28, 38, 48, 58, 68, 78, 88, 98, 108, 118, 128, 138, 148, 158, 168, 178, 188, 198, 208, 218, 228, 238} | |
| 34 | + | ) | |
| 35 | + | ||
| 31 | 36 | func xterm256Color(rgb renderer.RGB) int { | |
| 32 | - | red := (int(rgb.R)*5 + 127) / 255 | |
| 33 | - | green := (int(rgb.G)*5 + 127) / 255 | |
| 34 | - | blue := (int(rgb.B)*5 + 127) / 255 | |
| 35 | - | return 16 + 36*red + 6*green + blue | |
| 37 | + | bestIndex := 16 | |
| 38 | + | bestDistance := 3*255*255 + 1 | |
| 39 | + | ||
| 40 | + | for red, redLevel := range xterm256ColorLevels { | |
| 41 | + | for green, greenLevel := range xterm256ColorLevels { | |
| 42 | + | for blue, blueLevel := range xterm256ColorLevels { | |
| 43 | + | candidate := renderer.RGB{R: redLevel, G: greenLevel, B: blueLevel} | |
| 44 | + | distance := rgbDistance(rgb, candidate) | |
| 45 | + | if distance < bestDistance { | |
| 46 | + | bestIndex = 16 + 36*red + 6*green + blue | |
| 47 | + | bestDistance = distance | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | for i, level := range xterm256GrayLevels { | |
| 54 | + | candidate := renderer.RGB{R: level, G: level, B: level} | |
| 55 | + | distance := rgbDistance(rgb, candidate) | |
| 56 | + | if distance < bestDistance { | |
| 57 | + | bestIndex = 232 + i | |
| 58 | + | bestDistance = distance | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | return bestIndex | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | func rgbDistance(a, b renderer.RGB) int { | |
| 66 | + | red := int(a.R) - int(b.R) | |
| 67 | + | green := int(a.G) - int(b.G) | |
| 68 | + | blue := int(a.B) - int(b.B) | |
| 69 | + | return red*red + green*green + blue*blue | |
| 36 | 70 | } | |
| @@ -28,9 +28,43 @@ func adaptFrameColors(frame renderer.Frame, trueColor bool) renderer.Frame { | |||||
| 28 | return adapted | 28 | return adapted | ||
| 29 | } | 29 | } | ||
| 30 | 30 | ||||
| 31 | + | var ( | |||
| 32 | + | xterm256ColorLevels = [...]uint8{0, 95, 135, 175, 215, 255} | |||
| 33 | + | xterm256GrayLevels = [...]uint8{8, 18, 28, 38, 48, 58, 68, 78, 88, 98, 108, 118, 128, 138, 148, 158, 168, 178, 188, 198, 208, 218, 228, 238} | |||
| 34 | + | ) | |||
| 35 | + | ||||
| 31 | func xterm256Color(rgb renderer.RGB) int { | 36 | func xterm256Color(rgb renderer.RGB) int { | ||
| 32 | - | red := (int(rgb.R)*5 + 127) / 255 | 37 | + | bestIndex := 16 |
| 33 | - | green := (int(rgb.G)*5 + 127) / 255 | 38 | + | bestDistance := 3*255*255 + 1 |
| 34 | - | blue := (int(rgb.B)*5 + 127) / 255 | 39 | + | |
| 35 | - | return 16 + 36*red + 6*green + blue | 40 | + | for red, redLevel := range xterm256ColorLevels { |
| 41 | + | for green, greenLevel := range xterm256ColorLevels { | |||
| 42 | + | for blue, blueLevel := range xterm256ColorLevels { | |||
| 43 | + | candidate := renderer.RGB{R: redLevel, G: greenLevel, B: blueLevel} | |||
| 44 | + | distance := rgbDistance(rgb, candidate) | |||
| 45 | + | if distance < bestDistance { | |||
| 46 | + | bestIndex = 16 + 36*red + 6*green + blue | |||
| 47 | + | bestDistance = distance | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | } | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | for i, level := range xterm256GrayLevels { | |||
| 54 | + | candidate := renderer.RGB{R: level, G: level, B: level} | |||
| 55 | + | distance := rgbDistance(rgb, candidate) | |||
| 56 | + | if distance < bestDistance { | |||
| 57 | + | bestIndex = 232 + i | |||
| 58 | + | bestDistance = distance | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | return bestIndex | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | func rgbDistance(a, b renderer.RGB) int { | |||
| 66 | + | red := int(a.R) - int(b.R) | |||
| 67 | + | green := int(a.G) - int(b.G) | |||
| 68 | + | blue := int(a.B) - int(b.B) | |||
| 69 | + | return red*red + green*green + blue*blue | |||
| 36 | } | 70 | } | ||
M internal/usecase/daemon/terminal_color_test.go +140 -18
| @@ -8,22 +8,144 @@ import ( | |||
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | 10 | func TestAdaptFrameColors(t *testing.T) { | |
| 11 | - | frame := renderer.NewFrame(1, 1) | |
| 12 | - | frame.Cells[0].Style.HasForegroundRGB = true | |
| 13 | - | frame.Cells[0].Style.ForegroundRGB = renderer.RGB{R: 255, G: 0, B: 0} | |
| 14 | - | frame.Cells[0].Style.HasBackgroundRGB = true | |
| 15 | - | frame.Cells[0].Style.BackgroundRGB = renderer.RGB{R: 0, G: 255, B: 0} | |
| 16 | - | frame.Cells[0].Style.HasUnderlineColor = true | |
| 17 | - | frame.Cells[0].Style.HasUnderlineColorRGB = true | |
| 18 | - | frame.Cells[0].Style.UnderlineColorRGB = renderer.RGB{R: 0, G: 0, B: 255} | |
| 19 | - | ||
| 20 | - | got := adaptFrameColors(frame, false) | |
| 21 | - | ||
| 22 | - | require.False(t, got.Cells[0].Style.HasForegroundRGB) | |
| 23 | - | require.Equal(t, 196, got.Cells[0].Style.Foreground) | |
| 24 | - | require.False(t, got.Cells[0].Style.HasBackgroundRGB) | |
| 25 | - | require.Equal(t, 46, got.Cells[0].Style.Background) | |
| 26 | - | require.False(t, got.Cells[0].Style.HasUnderlineColorRGB) | |
| 27 | - | require.Equal(t, 21, got.Cells[0].Style.UnderlineColor) | |
| 28 | - | require.True(t, frame.Cells[0].Style.HasForegroundRGB, "adaptation must not mutate composed state") | |
| 11 | + | cases := []struct { | |
| 12 | + | name string | |
| 13 | + | trueColor bool | |
| 14 | + | style renderer.Style | |
| 15 | + | want renderer.Style | |
| 16 | + | }{ | |
| 17 | + | { | |
| 18 | + | name: "truecolor passthrough", | |
| 19 | + | trueColor: true, | |
| 20 | + | style: renderer.Style{ | |
| 21 | + | Foreground: 17, | |
| 22 | + | Background: 18, | |
| 23 | + | HasForegroundRGB: true, | |
| 24 | + | ForegroundRGB: renderer.RGB{R: 12, G: 34, B: 56}, | |
| 25 | + | HasBackgroundRGB: true, | |
| 26 | + | BackgroundRGB: renderer.RGB{R: 78, G: 90, B: 123}, | |
| 27 | + | UnderlineStyle: renderer.UnderlineCurly, | |
| 28 | + | HasUnderlineColor: true, | |
| 29 | + | UnderlineColor: 19, | |
| 30 | + | HasUnderlineColorRGB: true, | |
| 31 | + | UnderlineColorRGB: renderer.RGB{R: 145, G: 167, B: 189}, | |
| 32 | + | }, | |
| 33 | + | want: renderer.Style{ | |
| 34 | + | Foreground: 17, | |
| 35 | + | Background: 18, | |
| 36 | + | HasForegroundRGB: true, | |
| 37 | + | ForegroundRGB: renderer.RGB{R: 12, G: 34, B: 56}, | |
| 38 | + | HasBackgroundRGB: true, | |
| 39 | + | BackgroundRGB: renderer.RGB{R: 78, G: 90, B: 123}, | |
| 40 | + | UnderlineStyle: renderer.UnderlineCurly, | |
| 41 | + | HasUnderlineColor: true, | |
| 42 | + | UnderlineColor: 19, | |
| 43 | + | HasUnderlineColorRGB: true, | |
| 44 | + | UnderlineColorRGB: renderer.RGB{R: 145, G: 167, B: 189}, | |
| 45 | + | }, | |
| 46 | + | }, | |
| 47 | + | { | |
| 48 | + | name: "indexed colors convert foreground background and underline", | |
| 49 | + | trueColor: false, | |
| 50 | + | style: renderer.Style{ | |
| 51 | + | Foreground: 1, | |
| 52 | + | Background: 2, | |
| 53 | + | HasForegroundRGB: true, | |
| 54 | + | ForegroundRGB: renderer.RGB{R: 255, G: 0, B: 0}, | |
| 55 | + | HasBackgroundRGB: true, | |
| 56 | + | BackgroundRGB: renderer.RGB{R: 0, G: 255, B: 0}, | |
| 57 | + | UnderlineStyle: renderer.UnderlineSingle, | |
| 58 | + | HasUnderlineColor: true, | |
| 59 | + | UnderlineColor: 3, | |
| 60 | + | HasUnderlineColorRGB: true, | |
| 61 | + | UnderlineColorRGB: renderer.RGB{R: 0, G: 0, B: 255}, | |
| 62 | + | }, | |
| 63 | + | want: renderer.Style{ | |
| 64 | + | Foreground: 196, | |
| 65 | + | Background: 46, | |
| 66 | + | HasForegroundRGB: false, | |
| 67 | + | ForegroundRGB: renderer.RGB{R: 255, G: 0, B: 0}, | |
| 68 | + | HasBackgroundRGB: false, | |
| 69 | + | BackgroundRGB: renderer.RGB{R: 0, G: 255, B: 0}, | |
| 70 | + | UnderlineStyle: renderer.UnderlineSingle, | |
| 71 | + | HasUnderlineColor: true, | |
| 72 | + | UnderlineColor: 21, | |
| 73 | + | HasUnderlineColorRGB: false, | |
| 74 | + | UnderlineColorRGB: renderer.RGB{R: 0, G: 0, B: 255}, | |
| 75 | + | }, | |
| 76 | + | }, | |
| 77 | + | { | |
| 78 | + | name: "neutral grays use the grayscale ramp", | |
| 79 | + | trueColor: false, | |
| 80 | + | style: renderer.Style{ | |
| 81 | + | Foreground: 1, | |
| 82 | + | Background: 2, | |
| 83 | + | HasForegroundRGB: true, | |
| 84 | + | ForegroundRGB: renderer.RGB{R: 128, G: 128, B: 128}, | |
| 85 | + | HasBackgroundRGB: true, | |
| 86 | + | BackgroundRGB: renderer.RGB{R: 100, G: 100, B: 100}, | |
| 87 | + | UnderlineStyle: renderer.UnderlineDashed, | |
| 88 | + | HasUnderlineColor: true, | |
| 89 | + | UnderlineColor: 3, | |
| 90 | + | HasUnderlineColorRGB: true, | |
| 91 | + | UnderlineColorRGB: renderer.RGB{R: 50, G: 50, B: 50}, | |
| 92 | + | }, | |
| 93 | + | want: renderer.Style{ | |
| 94 | + | Foreground: 244, | |
| 95 | + | Background: 241, | |
| 96 | + | HasForegroundRGB: false, | |
| 97 | + | ForegroundRGB: renderer.RGB{R: 128, G: 128, B: 128}, | |
| 98 | + | HasBackgroundRGB: false, | |
| 99 | + | BackgroundRGB: renderer.RGB{R: 100, G: 100, B: 100}, | |
| 100 | + | UnderlineStyle: renderer.UnderlineDashed, | |
| 101 | + | HasUnderlineColor: true, | |
| 102 | + | UnderlineColor: 236, | |
| 103 | + | HasUnderlineColorRGB: false, | |
| 104 | + | UnderlineColorRGB: renderer.RGB{R: 50, G: 50, B: 50}, | |
| 105 | + | }, | |
| 106 | + | }, | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | for _, tc := range cases { | |
| 110 | + | t.Run(tc.name, func(t *testing.T) { | |
| 111 | + | frame := renderer.NewFrame(1, 1) | |
| 112 | + | frame.Cells[0].Style = tc.style | |
| 113 | + | composed := frame.Clone() | |
| 114 | + | ||
| 115 | + | got := adaptFrameColors(frame, tc.trueColor) | |
| 116 | + | ||
| 117 | + | require.Equal(t, tc.want, got.Cells[0].Style) | |
| 118 | + | require.Equal(t, composed, frame, "adaptation must not mutate composed frame state") | |
| 119 | + | }) | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | func TestXterm256Color(t *testing.T) { | |
| 124 | + | cases := []struct { | |
| 125 | + | name string | |
| 126 | + | rgb renderer.RGB | |
| 127 | + | want int | |
| 128 | + | }{ | |
| 129 | + | { | |
| 130 | + | name: "exact neutral gray", | |
| 131 | + | rgb: renderer.RGB{R: 128, G: 128, B: 128}, | |
| 132 | + | want: 244, | |
| 133 | + | }, | |
| 134 | + | { | |
| 135 | + | name: "nearest neutral gray", | |
| 136 | + | rgb: renderer.RGB{R: 100, G: 100, B: 100}, | |
| 137 | + | want: 241, | |
| 138 | + | }, | |
| 139 | + | { | |
| 140 | + | name: "color between cube levels", | |
| 141 | + | rgb: renderer.RGB{R: 100, G: 100, B: 200}, | |
| 142 | + | want: 62, | |
| 143 | + | }, | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | for _, tc := range cases { | |
| 147 | + | t.Run(tc.name, func(t *testing.T) { | |
| 148 | + | require.Equal(t, tc.want, xterm256Color(tc.rgb)) | |
| 149 | + | }) | |
| 150 | + | } | |
| 29 | 151 | } | |
| @@ -8,22 +8,144 @@ import ( | |||||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| 10 | func TestAdaptFrameColors(t *testing.T) { | 10 | func TestAdaptFrameColors(t *testing.T) { | ||
| 11 | - | frame := renderer.NewFrame(1, 1) | 11 | + | cases := []struct { |
| 12 | - | frame.Cells[0].Style.HasForegroundRGB = true | 12 | + | name string |
| 13 | - | frame.Cells[0].Style.ForegroundRGB = renderer.RGB{R: 255, G: 0, B: 0} | 13 | + | trueColor bool |
| 14 | - | frame.Cells[0].Style.HasBackgroundRGB = true | 14 | + | style renderer.Style |
| 15 | - | frame.Cells[0].Style.BackgroundRGB = renderer.RGB{R: 0, G: 255, B: 0} | 15 | + | want renderer.Style |
| 16 | - | frame.Cells[0].Style.HasUnderlineColor = true | 16 | + | }{ |
| 17 | - | frame.Cells[0].Style.HasUnderlineColorRGB = true | 17 | + | { |
| 18 | - | frame.Cells[0].Style.UnderlineColorRGB = renderer.RGB{R: 0, G: 0, B: 255} | 18 | + | name: "truecolor passthrough", |
| 19 | - | 19 | + | trueColor: true, | |
| 20 | - | got := adaptFrameColors(frame, false) | 20 | + | style: renderer.Style{ |
| 21 | - | 21 | + | Foreground: 17, | |
| 22 | - | require.False(t, got.Cells[0].Style.HasForegroundRGB) | 22 | + | Background: 18, |
| 23 | - | require.Equal(t, 196, got.Cells[0].Style.Foreground) | 23 | + | HasForegroundRGB: true, |
| 24 | - | require.False(t, got.Cells[0].Style.HasBackgroundRGB) | 24 | + | ForegroundRGB: renderer.RGB{R: 12, G: 34, B: 56}, |
| 25 | - | require.Equal(t, 46, got.Cells[0].Style.Background) | 25 | + | HasBackgroundRGB: true, |
| 26 | - | require.False(t, got.Cells[0].Style.HasUnderlineColorRGB) | 26 | + | BackgroundRGB: renderer.RGB{R: 78, G: 90, B: 123}, |
| 27 | - | require.Equal(t, 21, got.Cells[0].Style.UnderlineColor) | 27 | + | UnderlineStyle: renderer.UnderlineCurly, |
| 28 | - | require.True(t, frame.Cells[0].Style.HasForegroundRGB, "adaptation must not mutate composed state") | 28 | + | HasUnderlineColor: true, |
| 29 | + | UnderlineColor: 19, | |||
| 30 | + | HasUnderlineColorRGB: true, | |||
| 31 | + | UnderlineColorRGB: renderer.RGB{R: 145, G: 167, B: 189}, | |||
| 32 | + | }, | |||
| 33 | + | want: renderer.Style{ | |||
| 34 | + | Foreground: 17, | |||
| 35 | + | Background: 18, | |||
| 36 | + | HasForegroundRGB: true, | |||
| 37 | + | ForegroundRGB: renderer.RGB{R: 12, G: 34, B: 56}, | |||
| 38 | + | HasBackgroundRGB: true, | |||
| 39 | + | BackgroundRGB: renderer.RGB{R: 78, G: 90, B: 123}, | |||
| 40 | + | UnderlineStyle: renderer.UnderlineCurly, | |||
| 41 | + | HasUnderlineColor: true, | |||
| 42 | + | UnderlineColor: 19, | |||
| 43 | + | HasUnderlineColorRGB: true, | |||
| 44 | + | UnderlineColorRGB: renderer.RGB{R: 145, G: 167, B: 189}, | |||
| 45 | + | }, | |||
| 46 | + | }, | |||
| 47 | + | { | |||
| 48 | + | name: "indexed colors convert foreground background and underline", | |||
| 49 | + | trueColor: false, | |||
| 50 | + | style: renderer.Style{ | |||
| 51 | + | Foreground: 1, | |||
| 52 | + | Background: 2, | |||
| 53 | + | HasForegroundRGB: true, | |||
| 54 | + | ForegroundRGB: renderer.RGB{R: 255, G: 0, B: 0}, | |||
| 55 | + | HasBackgroundRGB: true, | |||
| 56 | + | BackgroundRGB: renderer.RGB{R: 0, G: 255, B: 0}, | |||
| 57 | + | UnderlineStyle: renderer.UnderlineSingle, | |||
| 58 | + | HasUnderlineColor: true, | |||
| 59 | + | UnderlineColor: 3, | |||
| 60 | + | HasUnderlineColorRGB: true, | |||
| 61 | + | UnderlineColorRGB: renderer.RGB{R: 0, G: 0, B: 255}, | |||
| 62 | + | }, | |||
| 63 | + | want: renderer.Style{ | |||
| 64 | + | Foreground: 196, | |||
| 65 | + | Background: 46, | |||
| 66 | + | HasForegroundRGB: false, | |||
| 67 | + | ForegroundRGB: renderer.RGB{R: 255, G: 0, B: 0}, | |||
| 68 | + | HasBackgroundRGB: false, | |||
| 69 | + | BackgroundRGB: renderer.RGB{R: 0, G: 255, B: 0}, | |||
| 70 | + | UnderlineStyle: renderer.UnderlineSingle, | |||
| 71 | + | HasUnderlineColor: true, | |||
| 72 | + | UnderlineColor: 21, | |||
| 73 | + | HasUnderlineColorRGB: false, | |||
| 74 | + | UnderlineColorRGB: renderer.RGB{R: 0, G: 0, B: 255}, | |||
| 75 | + | }, | |||
| 76 | + | }, | |||
| 77 | + | { | |||
| 78 | + | name: "neutral grays use the grayscale ramp", | |||
| 79 | + | trueColor: false, | |||
| 80 | + | style: renderer.Style{ | |||
| 81 | + | Foreground: 1, | |||
| 82 | + | Background: 2, | |||
| 83 | + | HasForegroundRGB: true, | |||
| 84 | + | ForegroundRGB: renderer.RGB{R: 128, G: 128, B: 128}, | |||
| 85 | + | HasBackgroundRGB: true, | |||
| 86 | + | BackgroundRGB: renderer.RGB{R: 100, G: 100, B: 100}, | |||
| 87 | + | UnderlineStyle: renderer.UnderlineDashed, | |||
| 88 | + | HasUnderlineColor: true, | |||
| 89 | + | UnderlineColor: 3, | |||
| 90 | + | HasUnderlineColorRGB: true, | |||
| 91 | + | UnderlineColorRGB: renderer.RGB{R: 50, G: 50, B: 50}, | |||
| 92 | + | }, | |||
| 93 | + | want: renderer.Style{ | |||
| 94 | + | Foreground: 244, | |||
| 95 | + | Background: 241, | |||
| 96 | + | HasForegroundRGB: false, | |||
| 97 | + | ForegroundRGB: renderer.RGB{R: 128, G: 128, B: 128}, | |||
| 98 | + | HasBackgroundRGB: false, | |||
| 99 | + | BackgroundRGB: renderer.RGB{R: 100, G: 100, B: 100}, | |||
| 100 | + | UnderlineStyle: renderer.UnderlineDashed, | |||
| 101 | + | HasUnderlineColor: true, | |||
| 102 | + | UnderlineColor: 236, | |||
| 103 | + | HasUnderlineColorRGB: false, | |||
| 104 | + | UnderlineColorRGB: renderer.RGB{R: 50, G: 50, B: 50}, | |||
| 105 | + | }, | |||
| 106 | + | }, | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | for _, tc := range cases { | |||
| 110 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 111 | + | frame := renderer.NewFrame(1, 1) | |||
| 112 | + | frame.Cells[0].Style = tc.style | |||
| 113 | + | composed := frame.Clone() | |||
| 114 | + | ||||
| 115 | + | got := adaptFrameColors(frame, tc.trueColor) | |||
| 116 | + | ||||
| 117 | + | require.Equal(t, tc.want, got.Cells[0].Style) | |||
| 118 | + | require.Equal(t, composed, frame, "adaptation must not mutate composed frame state") | |||
| 119 | + | }) | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | func TestXterm256Color(t *testing.T) { | |||
| 124 | + | cases := []struct { | |||
| 125 | + | name string | |||
| 126 | + | rgb renderer.RGB | |||
| 127 | + | want int | |||
| 128 | + | }{ | |||
| 129 | + | { | |||
| 130 | + | name: "exact neutral gray", | |||
| 131 | + | rgb: renderer.RGB{R: 128, G: 128, B: 128}, | |||
| 132 | + | want: 244, | |||
| 133 | + | }, | |||
| 134 | + | { | |||
| 135 | + | name: "nearest neutral gray", | |||
| 136 | + | rgb: renderer.RGB{R: 100, G: 100, B: 100}, | |||
| 137 | + | want: 241, | |||
| 138 | + | }, | |||
| 139 | + | { | |||
| 140 | + | name: "color between cube levels", | |||
| 141 | + | rgb: renderer.RGB{R: 100, G: 100, B: 200}, | |||
| 142 | + | want: 62, | |||
| 143 | + | }, | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | for _, tc := range cases { | |||
| 147 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 148 | + | require.Equal(t, tc.want, xterm256Color(tc.rgb)) | |||
| 149 | + | }) | |||
| 150 | + | } | |||
| 29 | } | 151 | } | ||