fix(terminal): use vev-vt color profiles
- commit
- 19b481449b36d621a6739f99b8a6e1814e33ccca
- author
- brice · · 2026-08-18 19:52 UTC
- parent
- f223378
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +57 -227
M go.mod +1 -1
| @@ -3,7 +3,7 @@ module github.com/bnema/vev | |||
| 3 | 3 | go 1.26 | |
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | - | github.com/bnema/vev-vt v0.2.1 | |
| 6 | + | github.com/bnema/vev-vt v0.3.0 | |
| 7 | 7 | github.com/stretchr/testify v1.11.1 | |
| 8 | 8 | ) | |
| 9 | 9 | ||
| @@ -3,7 +3,7 @@ module github.com/bnema/vev | |||||
| 3 | go 1.26 | 3 | go 1.26 | ||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | - | github.com/bnema/vev-vt v0.2.1 | 6 | + | github.com/bnema/vev-vt v0.3.0 |
| 7 | github.com/stretchr/testify v1.11.1 | 7 | github.com/stretchr/testify v1.11.1 | ||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
M go.sum +2 -2 generated file: collapsed
M internal/usecase/daemon/client.go +1 -1
| @@ -684,7 +684,7 @@ func (d *Daemon) prepareAttachedClientLocked(tr ports.Transport, sz domain.Size, | |||
| 684 | 684 | if !opts.capabilitiesSet { | |
| 685 | 685 | opts.terminalCapabilities = ports.TerminalCapabilities{ColorMode: ports.TerminalColorTrueColor} | |
| 686 | 686 | } | |
| 687 | - | output := newOutputStateStream(opts.maxOutputInFlight) | |
| 687 | + | output := newOutputStateStreamForCapabilities(opts.terminalCapabilities, opts.maxOutputInFlight) | |
| 688 | 688 | ac := &attachedClient{ | |
| 689 | 689 | tr: tr, | |
| 690 | 690 | output: output, | |
| @@ -684,7 +684,7 @@ func (d *Daemon) prepareAttachedClientLocked(tr ports.Transport, sz domain.Size, | |||||
| 684 | if !opts.capabilitiesSet { | 684 | if !opts.capabilitiesSet { | ||
| 685 | opts.terminalCapabilities = ports.TerminalCapabilities{ColorMode: ports.TerminalColorTrueColor} | 685 | opts.terminalCapabilities = ports.TerminalCapabilities{ColorMode: ports.TerminalColorTrueColor} | ||
| 686 | } | 686 | } | ||
| 687 | - | output := newOutputStateStream(opts.maxOutputInFlight) | 687 | + | output := newOutputStateStreamForCapabilities(opts.terminalCapabilities, opts.maxOutputInFlight) |
| 688 | ac := &attachedClient{ | 688 | ac := &attachedClient{ | ||
| 689 | tr: tr, | 689 | tr: tr, | ||
| 690 | output: output, | 690 | output: output, | ||
M internal/usecase/daemon/output_state_stream.go +13 -1
| @@ -61,12 +61,24 @@ func (s *outputStateStream) unlockView() { | |||
| 61 | 61 | } | |
| 62 | 62 | ||
| 63 | 63 | func newOutputStateStream(windowSize ...uint8) *outputStateStream { | |
| 64 | + | return newOutputStateStreamForProfile(renderer.ColorProfileTrueColor, windowSize...) | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func newOutputStateStreamForCapabilities(capabilities ports.TerminalCapabilities, windowSize ...uint8) *outputStateStream { | |
| 68 | + | profile := renderer.ColorProfileANSI256 | |
| 69 | + | if capabilities.TrueColor() { | |
| 70 | + | profile = renderer.ColorProfileTrueColor | |
| 71 | + | } | |
| 72 | + | return newOutputStateStreamForProfile(profile, windowSize...) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func newOutputStateStreamForProfile(profile renderer.ColorProfile, windowSize ...uint8) *outputStateStream { | |
| 64 | 76 | window := uint8(maxUnackedOutputStates) | |
| 65 | 77 | if len(windowSize) > 0 { | |
| 66 | 78 | window = normalizeOutputWindow(windowSize[0]) | |
| 67 | 79 | } | |
| 68 | 80 | stream := &outputStateStream{ | |
| 69 | - | renderer: renderer.New(renderer.Capabilities{}), | |
| 81 | + | renderer: renderer.NewWithColorProfile(renderer.Capabilities{}, profile), | |
| 70 | 82 | epoch: 1, | |
| 71 | 83 | maxOutstanding: uint64(window), | |
| 72 | 84 | } | |
| @@ -61,12 +61,24 @@ func (s *outputStateStream) unlockView() { | |||||
| 61 | } | 61 | } | ||
| 62 | 62 | ||||
| 63 | func newOutputStateStream(windowSize ...uint8) *outputStateStream { | 63 | func newOutputStateStream(windowSize ...uint8) *outputStateStream { | ||
| 64 | + | return newOutputStateStreamForProfile(renderer.ColorProfileTrueColor, windowSize...) | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func newOutputStateStreamForCapabilities(capabilities ports.TerminalCapabilities, windowSize ...uint8) *outputStateStream { | |||
| 68 | + | profile := renderer.ColorProfileANSI256 | |||
| 69 | + | if capabilities.TrueColor() { | |||
| 70 | + | profile = renderer.ColorProfileTrueColor | |||
| 71 | + | } | |||
| 72 | + | return newOutputStateStreamForProfile(profile, windowSize...) | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func newOutputStateStreamForProfile(profile renderer.ColorProfile, windowSize ...uint8) *outputStateStream { | |||
| 64 | window := uint8(maxUnackedOutputStates) | 76 | window := uint8(maxUnackedOutputStates) | ||
| 65 | if len(windowSize) > 0 { | 77 | if len(windowSize) > 0 { | ||
| 66 | window = normalizeOutputWindow(windowSize[0]) | 78 | window = normalizeOutputWindow(windowSize[0]) | ||
| 67 | } | 79 | } | ||
| 68 | stream := &outputStateStream{ | 80 | stream := &outputStateStream{ | ||
| 69 | - | renderer: renderer.New(renderer.Capabilities{}), | 81 | + | renderer: renderer.NewWithColorProfile(renderer.Capabilities{}, profile), |
| 70 | epoch: 1, | 82 | epoch: 1, | ||
| 71 | maxOutstanding: uint64(window), | 83 | maxOutstanding: uint64(window), | ||
| 72 | } | 84 | } | ||
M internal/usecase/daemon/output_state_stream_test.go +40 -0
| @@ -37,6 +37,46 @@ func TestOutputStateStreamBuildsPipelinedDependencyChain(t *testing.T) { | |||
| 37 | 37 | require.Equal(t, uint64(2), stream.outstanding()) | |
| 38 | 38 | } | |
| 39 | 39 | ||
| 40 | + | func TestOutputStateStreamUsesTerminalColorProfile(t *testing.T) { | |
| 41 | + | frame := renderer.NewFrame(1, 1) | |
| 42 | + | frame.Set(0, 0, renderer.Cell{Rune: 'X', Style: renderer.Style{ | |
| 43 | + | Foreground: -1, | |
| 44 | + | Background: -1, | |
| 45 | + | HasForegroundRGB: true, | |
| 46 | + | ForegroundRGB: renderer.RGB{R: 255, G: 0, B: 0}, | |
| 47 | + | HasBackgroundRGB: true, | |
| 48 | + | BackgroundRGB: renderer.RGB{R: 0, G: 0, B: 255}, | |
| 49 | + | HasUnderlineColorRGB: true, | |
| 50 | + | UnderlineColorRGB: renderer.RGB{R: 0, G: 255, B: 0}, | |
| 51 | + | }}) | |
| 52 | + | ||
| 53 | + | cases := []struct { | |
| 54 | + | name string | |
| 55 | + | capabilities ports.TerminalCapabilities | |
| 56 | + | want string | |
| 57 | + | }{ | |
| 58 | + | { | |
| 59 | + | name: "truecolor", | |
| 60 | + | capabilities: ports.TerminalCapabilities{ColorMode: ports.TerminalColorTrueColor}, | |
| 61 | + | want: "\x1b[1;1H\x1b[0;38;2;255;0;0;48;2;0;0;255;58;2;0;255;0mX\x1b[0m", | |
| 62 | + | }, | |
| 63 | + | { | |
| 64 | + | name: "indexed 256", | |
| 65 | + | capabilities: ports.TerminalCapabilities{ColorMode: ports.TerminalColorIndexed256}, | |
| 66 | + | want: "\x1b[1;1H\x1b[0;38;5;196;48;5;21;58;5;46mX\x1b[0m", | |
| 67 | + | }, | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | for _, tc := range cases { | |
| 71 | + | t.Run(tc.name, func(t *testing.T) { | |
| 72 | + | stream := newOutputStateStreamForCapabilities(tc.capabilities) | |
| 73 | + | prepared, err := stream.prepare(frame, nil, true) | |
| 74 | + | require.NoError(t, err) | |
| 75 | + | require.Equal(t, tc.want, string(prepared.data)) | |
| 76 | + | }) | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | ||
| 40 | 80 | func TestOutputStateStreamCapacityProbeDoesNotRaceWithSend(t *testing.T) { | |
| 41 | 81 | stream := newOutputStateStream(1) | |
| 42 | 82 | frame := renderer.NewFrame(3, 1) | |
| @@ -37,6 +37,46 @@ func TestOutputStateStreamBuildsPipelinedDependencyChain(t *testing.T) { | |||||
| 37 | require.Equal(t, uint64(2), stream.outstanding()) | 37 | require.Equal(t, uint64(2), stream.outstanding()) | ||
| 38 | } | 38 | } | ||
| 39 | 39 | ||||
| 40 | + | func TestOutputStateStreamUsesTerminalColorProfile(t *testing.T) { | |||
| 41 | + | frame := renderer.NewFrame(1, 1) | |||
| 42 | + | frame.Set(0, 0, renderer.Cell{Rune: 'X', Style: renderer.Style{ | |||
| 43 | + | Foreground: -1, | |||
| 44 | + | Background: -1, | |||
| 45 | + | HasForegroundRGB: true, | |||
| 46 | + | ForegroundRGB: renderer.RGB{R: 255, G: 0, B: 0}, | |||
| 47 | + | HasBackgroundRGB: true, | |||
| 48 | + | BackgroundRGB: renderer.RGB{R: 0, G: 0, B: 255}, | |||
| 49 | + | HasUnderlineColorRGB: true, | |||
| 50 | + | UnderlineColorRGB: renderer.RGB{R: 0, G: 255, B: 0}, | |||
| 51 | + | }}) | |||
| 52 | + | ||||
| 53 | + | cases := []struct { | |||
| 54 | + | name string | |||
| 55 | + | capabilities ports.TerminalCapabilities | |||
| 56 | + | want string | |||
| 57 | + | }{ | |||
| 58 | + | { | |||
| 59 | + | name: "truecolor", | |||
| 60 | + | capabilities: ports.TerminalCapabilities{ColorMode: ports.TerminalColorTrueColor}, | |||
| 61 | + | want: "\x1b[1;1H\x1b[0;38;2;255;0;0;48;2;0;0;255;58;2;0;255;0mX\x1b[0m", | |||
| 62 | + | }, | |||
| 63 | + | { | |||
| 64 | + | name: "indexed 256", | |||
| 65 | + | capabilities: ports.TerminalCapabilities{ColorMode: ports.TerminalColorIndexed256}, | |||
| 66 | + | want: "\x1b[1;1H\x1b[0;38;5;196;48;5;21;58;5;46mX\x1b[0m", | |||
| 67 | + | }, | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | for _, tc := range cases { | |||
| 71 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 72 | + | stream := newOutputStateStreamForCapabilities(tc.capabilities) | |||
| 73 | + | prepared, err := stream.prepare(frame, nil, true) | |||
| 74 | + | require.NoError(t, err) | |||
| 75 | + | require.Equal(t, tc.want, string(prepared.data)) | |||
| 76 | + | }) | |||
| 77 | + | } | |||
| 78 | + | } | |||
| 79 | + | ||||
| 40 | func TestOutputStateStreamCapacityProbeDoesNotRaceWithSend(t *testing.T) { | 80 | func TestOutputStateStreamCapacityProbeDoesNotRaceWithSend(t *testing.T) { | ||
| 41 | stream := newOutputStateStream(1) | 81 | stream := newOutputStateStream(1) | ||
| 42 | frame := renderer.NewFrame(3, 1) | 82 | frame := renderer.NewFrame(3, 1) | ||
M internal/usecase/daemon/render_pipeline.go +0 -1
| @@ -468,7 +468,6 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe | |||
| 468 | 468 | } | |
| 469 | 469 | } | |
| 470 | 470 | endDiff := marks.span(ports.RuntimeDiffStart, ports.RuntimeDiffEnd, 0) | |
| 471 | - | composed.frame = adaptFrameColors(composed.frame, ac.terminalCapabilities.TrueColor()) | |
| 472 | 471 | var ( | |
| 473 | 472 | preparedANSI *preparedOutput | |
| 474 | 473 | err error | |
| @@ -468,7 +468,6 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe | |||||
| 468 | } | 468 | } | ||
| 469 | } | 469 | } | ||
| 470 | endDiff := marks.span(ports.RuntimeDiffStart, ports.RuntimeDiffEnd, 0) | 470 | endDiff := marks.span(ports.RuntimeDiffStart, ports.RuntimeDiffEnd, 0) | ||
| 471 | - | composed.frame = adaptFrameColors(composed.frame, ac.terminalCapabilities.TrueColor()) | |||
| 472 | var ( | 471 | var ( | ||
| 473 | preparedANSI *preparedOutput | 472 | preparedANSI *preparedOutput | ||
| 474 | err error | 473 | err error | ||
D internal/usecase/daemon/terminal_color.go +0 -70
| @@ -1,70 +0,0 @@ | |||
| 1 | - | package daemon | |
| 2 | - | ||
| 3 | - | import renderer "github.com/bnema/vev-vt" | |
| 4 | - | ||
| 5 | - | // adaptFrameColors preserves the composed frame for capable attachments and | |
| 6 | - | // converts direct colors to the xterm 256-color cube for constrained ones. | |
| 7 | - | func adaptFrameColors(frame renderer.Frame, trueColor bool) renderer.Frame { | |
| 8 | - | if trueColor { | |
| 9 | - | return frame | |
| 10 | - | } | |
| 11 | - | adapted := frame.Clone() | |
| 12 | - | for i := range adapted.Cells { | |
| 13 | - | style := &adapted.Cells[i].Style | |
| 14 | - | if style.HasForegroundRGB { | |
| 15 | - | style.Foreground = xterm256Color(style.ForegroundRGB) | |
| 16 | - | style.HasForegroundRGB = false | |
| 17 | - | } | |
| 18 | - | if style.HasBackgroundRGB { | |
| 19 | - | style.Background = xterm256Color(style.BackgroundRGB) | |
| 20 | - | style.HasBackgroundRGB = false | |
| 21 | - | } | |
| 22 | - | if style.HasUnderlineColorRGB { | |
| 23 | - | style.UnderlineColor = xterm256Color(style.UnderlineColorRGB) | |
| 24 | - | style.HasUnderlineColorRGB = false | |
| 25 | - | style.HasUnderlineColor = true | |
| 26 | - | } | |
| 27 | - | } | |
| 28 | - | return adapted | |
| 29 | - | } | |
| 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 | - | ||
| 36 | - | func xterm256Color(rgb renderer.RGB) int { | |
| 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 | |
| 70 | - | } | |
| @@ -1,70 +0,0 @@ | |||||
| 1 | - | package daemon | |||
| 2 | - | ||||
| 3 | - | import renderer "github.com/bnema/vev-vt" | |||
| 4 | - | ||||
| 5 | - | // adaptFrameColors preserves the composed frame for capable attachments and | |||
| 6 | - | // converts direct colors to the xterm 256-color cube for constrained ones. | |||
| 7 | - | func adaptFrameColors(frame renderer.Frame, trueColor bool) renderer.Frame { | |||
| 8 | - | if trueColor { | |||
| 9 | - | return frame | |||
| 10 | - | } | |||
| 11 | - | adapted := frame.Clone() | |||
| 12 | - | for i := range adapted.Cells { | |||
| 13 | - | style := &adapted.Cells[i].Style | |||
| 14 | - | if style.HasForegroundRGB { | |||
| 15 | - | style.Foreground = xterm256Color(style.ForegroundRGB) | |||
| 16 | - | style.HasForegroundRGB = false | |||
| 17 | - | } | |||
| 18 | - | if style.HasBackgroundRGB { | |||
| 19 | - | style.Background = xterm256Color(style.BackgroundRGB) | |||
| 20 | - | style.HasBackgroundRGB = false | |||
| 21 | - | } | |||
| 22 | - | if style.HasUnderlineColorRGB { | |||
| 23 | - | style.UnderlineColor = xterm256Color(style.UnderlineColorRGB) | |||
| 24 | - | style.HasUnderlineColorRGB = false | |||
| 25 | - | style.HasUnderlineColor = true | |||
| 26 | - | } | |||
| 27 | - | } | |||
| 28 | - | return adapted | |||
| 29 | - | } | |||
| 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 | - | ||||
| 36 | - | func xterm256Color(rgb renderer.RGB) int { | |||
| 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 | |||
| 70 | - | } | |||
D internal/usecase/daemon/terminal_color_test.go +0 -151
| @@ -1,151 +0,0 @@ | |||
| 1 | - | package daemon | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "testing" | |
| 5 | - | ||
| 6 | - | renderer "github.com/bnema/vev-vt" | |
| 7 | - | "github.com/stretchr/testify/require" | |
| 8 | - | ) | |
| 9 | - | ||
| 10 | - | func TestAdaptFrameColors(t *testing.T) { | |
| 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 | - | } | |
| 151 | - | } | |
| @@ -1,151 +0,0 @@ | |||||
| 1 | - | package daemon | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "testing" | |||
| 5 | - | ||||
| 6 | - | renderer "github.com/bnema/vev-vt" | |||
| 7 | - | "github.com/stretchr/testify/require" | |||
| 8 | - | ) | |||
| 9 | - | ||||
| 10 | - | func TestAdaptFrameColors(t *testing.T) { | |||
| 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 | - | } | |||
| 151 | - | } | |||