detached 19b4814

fix(terminal): use vev-vt color profiles

commit
19b481449b36d621a6739f99b8a6e1814e33ccca
author
brice · · 2026-08-18 19:52 UTC
parent
f223378
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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
33 go 1.26
44
55 require (
6- github.com/bnema/vev-vt v0.2.1
6+ github.com/bnema/vev-vt v0.3.0
77 github.com/stretchr/testify v1.11.1
88 )
99
@@ -3,7 +3,7 @@ module github.com/bnema/vev
3 go 1.263 go 1.26
4 4
5 require (5 require (
6- github.com/bnema/vev-vt v0.2.16+ github.com/bnema/vev-vt v0.3.0
7 github.com/stretchr/testify v1.11.17 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,
684684 if !opts.capabilitiesSet {
685685 opts.terminalCapabilities = ports.TerminalCapabilities{ColorMode: ports.TerminalColorTrueColor}
686686 }
687- output := newOutputStateStream(opts.maxOutputInFlight)
687+ output := newOutputStateStreamForCapabilities(opts.terminalCapabilities, opts.maxOutputInFlight)
688688 ac := &attachedClient{
689689 tr: tr,
690690 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() {
6161 }
6262
6363 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 {
6476 window := uint8(maxUnackedOutputStates)
6577 if len(windowSize) > 0 {
6678 window = normalizeOutputWindow(windowSize[0])
6779 }
6880 stream := &outputStateStream{
69- renderer: renderer.New(renderer.Capabilities{}),
81+ renderer: renderer.NewWithColorProfile(renderer.Capabilities{}, profile),
7082 epoch: 1,
7183 maxOutstanding: uint64(window),
7284 }
@@ -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) {
3737 require.Equal(t, uint64(2), stream.outstanding())
3838 }
3939
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+
4080 func TestOutputStateStreamCapacityProbeDoesNotRaceWithSend(t *testing.T) {
4181 stream := newOutputStateStream(1)
4282 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
468468 }
469469 }
470470 endDiff := marks.span(ports.RuntimeDiffStart, ports.RuntimeDiffEnd, 0)
471- composed.frame = adaptFrameColors(composed.frame, ac.terminalCapabilities.TrueColor())
472471 var (
473472 preparedANSI *preparedOutput
474473 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 *preparedOutput472 preparedANSI *preparedOutput
474 err error473 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-}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close