feat(protocol): carry color capabilities in Hello
Replace Hello.TrueColor and Theme.TrueColor with Hello.color, a
ColorCapabilities{mode, source} message (field 26; Hello 12 and Theme 5
reserved). Add ANSI16 and Monochrome modes and the Forced source to
terminalcap, map all four modes in colorprofile.Profile, and make
terminalcap.Resolve take the declared capability: declared and forced claims
are used verbatim, anything else falls back to detection. The daemon
downgrade toast names the actual mode and is suppressed for forced modes.
sessionwire rejects a missing color, an UNSPECIFIED mode, or unknown enum
values. Bump protocol.Version to 65.
The usecase theme.Theme TrueColor field was transport-only and is dropped in
the same change.- commit
- 4b57b07c6d492a944814f50bd4e83742ecc2d216
- author
- brice · · 2026-10-03 12:28 UTC
- committer
- brice · · 2026-10-03 14:14 UTC
- parent
- 0103d51
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
51 files · +785 -205
M buf.yaml +4 -0
| @@ -18,5 +18,9 @@ breaking: | |||
| 18 | 18 | ignore_only: | |
| 19 | 19 | # Protocol v58 intentionally replaces CommandResult.ok with a closed | |
| 20 | 20 | # outcome. Strict version equality prevents cross-version decoding. | |
| 21 | + | # Protocol v65 replaces the Hello and Theme true_color bools with | |
| 22 | + | # Hello.color capabilities under the same strict-equality rule. | |
| 21 | 23 | FIELD_NO_DELETE: | |
| 22 | 24 | - internal/protocol/wire/schema/command.proto | |
| 25 | + | - internal/protocol/wire/schema/session.proto | |
| 26 | + | - internal/protocol/wire/schema/terminal.proto | |
| @@ -18,5 +18,9 @@ breaking: | |||||
| 18 | ignore_only: | 18 | ignore_only: | ||
| 19 | # Protocol v58 intentionally replaces CommandResult.ok with a closed | 19 | # Protocol v58 intentionally replaces CommandResult.ok with a closed | ||
| 20 | # outcome. Strict version equality prevents cross-version decoding. | 20 | # outcome. Strict version equality prevents cross-version decoding. | ||
| 21 | + | # Protocol v65 replaces the Hello and Theme true_color bools with | |||
| 22 | + | # Hello.color capabilities under the same strict-equality rule. | |||
| 21 | FIELD_NO_DELETE: | 23 | FIELD_NO_DELETE: | ||
| 22 | - internal/protocol/wire/schema/command.proto | 24 | - internal/protocol/wire/schema/command.proto | ||
| 25 | + | - internal/protocol/wire/schema/session.proto | |||
| 26 | + | - internal/protocol/wire/schema/terminal.proto | |||
M docs/protocol.md +10 -0
| @@ -88,6 +88,16 @@ constant or the request fails | |||
| 88 | 88 | with the version-mismatch response above. There is no multi-version | |
| 89 | 89 | negotiation: equality is mandatory. | |
| 90 | 90 | ||
| 91 | + | `Hello.color` (`ColorCapabilities{mode, source}`, field 26) is the only carrier | |
| 92 | + | of terminal color capability; `Theme` has no capability field. `mode` is a | |
| 93 | + | closed enum (`TRUE_COLOR`, `ANSI256`, `ANSI16`, `MONOCHROME`) and a missing | |
| 94 | + | message, an `UNSPECIFIED` mode, or any unknown enum value is a malformed Hello | |
| 95 | + | (`sessionwire` rejects it before any mutation). `source` `UNSPECIFIED` means | |
| 96 | + | "unknown": the daemon then ignores the claimed mode and re-detects from | |
| 97 | + | `Hello.Env`; `DECLARED` and `FORCED` claims are used verbatim. | |
| 98 | + | (`internal/domain/terminalcap.Resolve`; wire tests in | |
| 99 | + | `sessionwire/proto_convert_test.go:TestProtoHelloColorWire`.) | |
| 100 | + | ||
| 91 | 101 | Command requests additionally carry a 10-second result deadline | |
| 92 | 102 | (`daemon/command_tracker.go:CommandRequestTimeout`), tracked per | |
| 93 | 103 | connection and correlated by `RequestID`. Every dispatched request produces | |
| @@ -88,6 +88,16 @@ constant or the request fails | |||||
| 88 | with the version-mismatch response above. There is no multi-version | 88 | with the version-mismatch response above. There is no multi-version | ||
| 89 | negotiation: equality is mandatory. | 89 | negotiation: equality is mandatory. | ||
| 90 | 90 | ||||
| 91 | + | `Hello.color` (`ColorCapabilities{mode, source}`, field 26) is the only carrier | |||
| 92 | + | of terminal color capability; `Theme` has no capability field. `mode` is a | |||
| 93 | + | closed enum (`TRUE_COLOR`, `ANSI256`, `ANSI16`, `MONOCHROME`) and a missing | |||
| 94 | + | message, an `UNSPECIFIED` mode, or any unknown enum value is a malformed Hello | |||
| 95 | + | (`sessionwire` rejects it before any mutation). `source` `UNSPECIFIED` means | |||
| 96 | + | "unknown": the daemon then ignores the claimed mode and re-detects from | |||
| 97 | + | `Hello.Env`; `DECLARED` and `FORCED` claims are used verbatim. | |||
| 98 | + | (`internal/domain/terminalcap.Resolve`; wire tests in | |||
| 99 | + | `sessionwire/proto_convert_test.go:TestProtoHelloColorWire`.) | |||
| 100 | + | ||||
| 91 | Command requests additionally carry a 10-second result deadline | 101 | Command requests additionally carry a 10-second result deadline | ||
| 92 | (`daemon/command_tracker.go:CommandRequestTimeout`), tracked per | 102 | (`daemon/command_tracker.go:CommandRequestTimeout`), tracked per | ||
| 93 | connection and correlated by `RequestID`. Every dispatched request produces | 103 | connection and correlated by `RequestID`. Every dispatched request produces | ||
M internal/adapters/sessionwire/client_test.go +1 -1
| @@ -26,7 +26,7 @@ func TestClientConnectionEncodesEveryClientMessage(t *testing.T) { | |||
| 26 | 26 | {name: "ping", message: protocol.Ping{}}, | |
| 27 | 27 | {name: "list", message: protocol.List{}}, | |
| 28 | 28 | {name: "kill", message: protocol.Kill{RequestID: 1, Name: "work"}}, | |
| 29 | - | {name: "theme", message: protocol.Theme{TrueColor: true}}, | |
| 29 | + | {name: "theme", message: protocol.Theme{SchemeKnown: true}}, | |
| 30 | 30 | {name: "ack", message: protocol.Ack{Epoch: 1, State: 1}}, | |
| 31 | 31 | {name: "image", message: protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}}}, | |
| 32 | 32 | {name: "notice", message: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}}, | |
| @@ -26,7 +26,7 @@ func TestClientConnectionEncodesEveryClientMessage(t *testing.T) { | |||||
| 26 | {name: "ping", message: protocol.Ping{}}, | 26 | {name: "ping", message: protocol.Ping{}}, | ||
| 27 | {name: "list", message: protocol.List{}}, | 27 | {name: "list", message: protocol.List{}}, | ||
| 28 | {name: "kill", message: protocol.Kill{RequestID: 1, Name: "work"}}, | 28 | {name: "kill", message: protocol.Kill{RequestID: 1, Name: "work"}}, | ||
| 29 | - | {name: "theme", message: protocol.Theme{TrueColor: true}}, | 29 | + | {name: "theme", message: protocol.Theme{SchemeKnown: true}}, |
| 30 | {name: "ack", message: protocol.Ack{Epoch: 1, State: 1}}, | 30 | {name: "ack", message: protocol.Ack{Epoch: 1, State: 1}}, | ||
| 31 | {name: "image", message: protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}}}, | 31 | {name: "image", message: protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}}}, | ||
| 32 | {name: "notice", message: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}}, | 32 | {name: "notice", message: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}}, | ||
M internal/adapters/sessionwire/proto_convert_misc.go +0 -2
| @@ -69,7 +69,6 @@ func themeToWire(message protocol.Theme) *wire.Theme { | |||
| 69 | 69 | Foreground: rgbToWire(message.Foreground), | |
| 70 | 70 | HasBackground: message.HasBackground, | |
| 71 | 71 | Background: rgbToWire(message.Background), | |
| 72 | - | TrueColor: message.TrueColor, | |
| 73 | 72 | SchemeKnown: message.SchemeKnown, | |
| 74 | 73 | Light: message.Light, | |
| 75 | 74 | PaletteKnown: uint32(message.PaletteKnown), | |
| @@ -97,7 +96,6 @@ func themeFromWire(message *wire.Theme) (protocol.Theme, error) { | |||
| 97 | 96 | theme.Foreground = foreground | |
| 98 | 97 | theme.HasBackground = message.GetHasBackground() | |
| 99 | 98 | theme.Background = background | |
| 100 | - | theme.TrueColor = message.GetTrueColor() | |
| 101 | 99 | theme.SchemeKnown = message.GetSchemeKnown() | |
| 102 | 100 | theme.Light = message.GetLight() | |
| 103 | 101 | known := message.GetPaletteKnown() | |
| @@ -69,7 +69,6 @@ func themeToWire(message protocol.Theme) *wire.Theme { | |||||
| 69 | Foreground: rgbToWire(message.Foreground), | 69 | Foreground: rgbToWire(message.Foreground), | ||
| 70 | HasBackground: message.HasBackground, | 70 | HasBackground: message.HasBackground, | ||
| 71 | Background: rgbToWire(message.Background), | 71 | Background: rgbToWire(message.Background), | ||
| 72 | - | TrueColor: message.TrueColor, | |||
| 73 | SchemeKnown: message.SchemeKnown, | 72 | SchemeKnown: message.SchemeKnown, | ||
| 74 | Light: message.Light, | 73 | Light: message.Light, | ||
| 75 | PaletteKnown: uint32(message.PaletteKnown), | 74 | PaletteKnown: uint32(message.PaletteKnown), | ||
| @@ -97,7 +96,6 @@ func themeFromWire(message *wire.Theme) (protocol.Theme, error) { | |||||
| 97 | theme.Foreground = foreground | 96 | theme.Foreground = foreground | ||
| 98 | theme.HasBackground = message.GetHasBackground() | 97 | theme.HasBackground = message.GetHasBackground() | ||
| 99 | theme.Background = background | 98 | theme.Background = background | ||
| 100 | - | theme.TrueColor = message.GetTrueColor() | |||
| 101 | theme.SchemeKnown = message.GetSchemeKnown() | 99 | theme.SchemeKnown = message.GetSchemeKnown() | ||
| 102 | theme.Light = message.GetLight() | 100 | theme.Light = message.GetLight() | ||
| 103 | known := message.GetPaletteKnown() | 101 | known := message.GetPaletteKnown() | ||
M internal/adapters/sessionwire/proto_convert_range_test.go +57 -0
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "testing" | |
| 5 | 5 | ||
| 6 | 6 | "github.com/bnema/vev/internal/domain" | |
| 7 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 7 | 8 | "github.com/bnema/vev/internal/protocol" | |
| 8 | 9 | "github.com/bnema/vev/internal/protocol/wire" | |
| 9 | 10 | "github.com/stretchr/testify/require" | |
| @@ -80,6 +81,62 @@ func TestNarrowingConversionsRejectOverflow(t *testing.T) { | |||
| 80 | 81 | }, | |
| 81 | 82 | wantErr: protocol.ErrInvalidHello, | |
| 82 | 83 | }, | |
| 84 | + | { | |
| 85 | + | name: "hello color missing", | |
| 86 | + | run: func(t *testing.T) error { | |
| 87 | + | hello := testWireHello(t) | |
| 88 | + | hello.Color = nil | |
| 89 | + | _, err := helloFromWire(hello) | |
| 90 | + | return err | |
| 91 | + | }, | |
| 92 | + | wantErr: errProtoConvertRange, | |
| 93 | + | }, | |
| 94 | + | { | |
| 95 | + | name: "hello color mode unspecified", | |
| 96 | + | run: func(t *testing.T) error { | |
| 97 | + | hello := testWireHello(t) | |
| 98 | + | hello.Color.Mode = wire.ColorMode_COLOR_MODE_UNSPECIFIED | |
| 99 | + | _, err := helloFromWire(hello) | |
| 100 | + | return err | |
| 101 | + | }, | |
| 102 | + | wantErr: errProtoConvertRange, | |
| 103 | + | }, | |
| 104 | + | { | |
| 105 | + | name: "hello color mode unknown", | |
| 106 | + | run: func(t *testing.T) error { | |
| 107 | + | hello := testWireHello(t) | |
| 108 | + | hello.Color.Mode = wire.ColorMode(5) | |
| 109 | + | _, err := helloFromWire(hello) | |
| 110 | + | return err | |
| 111 | + | }, | |
| 112 | + | wantErr: errProtoConvertRange, | |
| 113 | + | }, | |
| 114 | + | { | |
| 115 | + | name: "hello color source unknown", | |
| 116 | + | run: func(t *testing.T) error { | |
| 117 | + | hello := testWireHello(t) | |
| 118 | + | hello.Color.Source = wire.ColorSource(4) | |
| 119 | + | _, err := helloFromWire(hello) | |
| 120 | + | return err | |
| 121 | + | }, | |
| 122 | + | wantErr: errProtoConvertRange, | |
| 123 | + | }, | |
| 124 | + | { | |
| 125 | + | name: "hello color mode not encodable", | |
| 126 | + | run: func(t *testing.T) error { | |
| 127 | + | _, err := helloToWire(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, Color: terminalcap.ColorCapabilities{Mode: terminalcap.ColorMode(9)}}) | |
| 128 | + | return err | |
| 129 | + | }, | |
| 130 | + | wantErr: errProtoConvertRange, | |
| 131 | + | }, | |
| 132 | + | { | |
| 133 | + | name: "hello color source not encodable", | |
| 134 | + | run: func(t *testing.T) error { | |
| 135 | + | _, err := helloToWire(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, Color: terminalcap.ColorCapabilities{Source: terminalcap.Source(9)}}) | |
| 136 | + | return err | |
| 137 | + | }, | |
| 138 | + | wantErr: errProtoConvertRange, | |
| 139 | + | }, | |
| 83 | 140 | { | |
| 84 | 141 | name: "hello geometry", | |
| 85 | 142 | run: func(t *testing.T) error { | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "testing" | 4 | "testing" | ||
| 5 | 5 | ||||
| 6 | "github.com/bnema/vev/internal/domain" | 6 | "github.com/bnema/vev/internal/domain" | ||
| 7 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |||
| 7 | "github.com/bnema/vev/internal/protocol" | 8 | "github.com/bnema/vev/internal/protocol" | ||
| 8 | "github.com/bnema/vev/internal/protocol/wire" | 9 | "github.com/bnema/vev/internal/protocol/wire" | ||
| 9 | "github.com/stretchr/testify/require" | 10 | "github.com/stretchr/testify/require" | ||
| @@ -80,6 +81,62 @@ func TestNarrowingConversionsRejectOverflow(t *testing.T) { | |||||
| 80 | }, | 81 | }, | ||
| 81 | wantErr: protocol.ErrInvalidHello, | 82 | wantErr: protocol.ErrInvalidHello, | ||
| 82 | }, | 83 | }, | ||
| 84 | + | { | |||
| 85 | + | name: "hello color missing", | |||
| 86 | + | run: func(t *testing.T) error { | |||
| 87 | + | hello := testWireHello(t) | |||
| 88 | + | hello.Color = nil | |||
| 89 | + | _, err := helloFromWire(hello) | |||
| 90 | + | return err | |||
| 91 | + | }, | |||
| 92 | + | wantErr: errProtoConvertRange, | |||
| 93 | + | }, | |||
| 94 | + | { | |||
| 95 | + | name: "hello color mode unspecified", | |||
| 96 | + | run: func(t *testing.T) error { | |||
| 97 | + | hello := testWireHello(t) | |||
| 98 | + | hello.Color.Mode = wire.ColorMode_COLOR_MODE_UNSPECIFIED | |||
| 99 | + | _, err := helloFromWire(hello) | |||
| 100 | + | return err | |||
| 101 | + | }, | |||
| 102 | + | wantErr: errProtoConvertRange, | |||
| 103 | + | }, | |||
| 104 | + | { | |||
| 105 | + | name: "hello color mode unknown", | |||
| 106 | + | run: func(t *testing.T) error { | |||
| 107 | + | hello := testWireHello(t) | |||
| 108 | + | hello.Color.Mode = wire.ColorMode(5) | |||
| 109 | + | _, err := helloFromWire(hello) | |||
| 110 | + | return err | |||
| 111 | + | }, | |||
| 112 | + | wantErr: errProtoConvertRange, | |||
| 113 | + | }, | |||
| 114 | + | { | |||
| 115 | + | name: "hello color source unknown", | |||
| 116 | + | run: func(t *testing.T) error { | |||
| 117 | + | hello := testWireHello(t) | |||
| 118 | + | hello.Color.Source = wire.ColorSource(4) | |||
| 119 | + | _, err := helloFromWire(hello) | |||
| 120 | + | return err | |||
| 121 | + | }, | |||
| 122 | + | wantErr: errProtoConvertRange, | |||
| 123 | + | }, | |||
| 124 | + | { | |||
| 125 | + | name: "hello color mode not encodable", | |||
| 126 | + | run: func(t *testing.T) error { | |||
| 127 | + | _, err := helloToWire(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, Color: terminalcap.ColorCapabilities{Mode: terminalcap.ColorMode(9)}}) | |||
| 128 | + | return err | |||
| 129 | + | }, | |||
| 130 | + | wantErr: errProtoConvertRange, | |||
| 131 | + | }, | |||
| 132 | + | { | |||
| 133 | + | name: "hello color source not encodable", | |||
| 134 | + | run: func(t *testing.T) error { | |||
| 135 | + | _, err := helloToWire(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, Color: terminalcap.ColorCapabilities{Source: terminalcap.Source(9)}}) | |||
| 136 | + | return err | |||
| 137 | + | }, | |||
| 138 | + | wantErr: errProtoConvertRange, | |||
| 139 | + | }, | |||
| 83 | { | 140 | { | ||
| 84 | name: "hello geometry", | 141 | name: "hello geometry", | ||
| 85 | run: func(t *testing.T) error { | 142 | run: func(t *testing.T) error { | ||
M internal/adapters/sessionwire/proto_convert_session.go +80 -2
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | 11 | "google.golang.org/protobuf/proto" | |
| 12 | 12 | ||
| 13 | 13 | "github.com/bnema/vev/internal/domain" | |
| 14 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 14 | 15 | "github.com/bnema/vev/internal/protocol" | |
| 15 | 16 | "github.com/bnema/vev/internal/protocol/wire" | |
| 16 | 17 | ) | |
| @@ -29,6 +30,10 @@ func helloToWire(message protocol.Hello) (*wire.Hello, error) { | |||
| 29 | 30 | return nil, err | |
| 30 | 31 | } | |
| 31 | 32 | exact := exactTargetToWire(message.ExactTarget) | |
| 33 | + | color, err := colorToWire(message.Color) | |
| 34 | + | if err != nil { | |
| 35 | + | return nil, err | |
| 36 | + | } | |
| 32 | 37 | return &wire.Hello{ | |
| 33 | 38 | Version: uint32(message.Version), | |
| 34 | 39 | Intent: uint32(message.Intent), | |
| @@ -41,7 +46,7 @@ func helloToWire(message protocol.Hello) (*wire.Hello, error) { | |||
| 41 | 46 | PixelHeight: pixelHeight, | |
| 42 | 47 | TermEnv: message.TermEnv, | |
| 43 | 48 | Cwd: message.Cwd, | |
| 44 | - | TrueColor: message.TrueColor, | |
| 49 | + | Color: color, | |
| 45 | 50 | MaxOutputInFlight: uint32(message.MaxOutputInFlight), | |
| 46 | 51 | Env: append([]string(nil), message.Env...), | |
| 47 | 52 | SessionTarget: sessionTarget, | |
| @@ -57,6 +62,76 @@ func helloToWire(message protocol.Hello) (*wire.Hello, error) { | |||
| 57 | 62 | }, nil | |
| 58 | 63 | } | |
| 59 | 64 | ||
| 65 | + | // colorToWire maps a color capability to its wire message. Unknown semantic | |
| 66 | + | // values are never encoded. | |
| 67 | + | func colorToWire(color terminalcap.ColorCapabilities) (*wire.ColorCapabilities, error) { | |
| 68 | + | if !color.Valid() { | |
| 69 | + | return nil, errProtoConvertRange | |
| 70 | + | } | |
| 71 | + | var mode wire.ColorMode | |
| 72 | + | switch color.Mode { | |
| 73 | + | case terminalcap.TrueColor: | |
| 74 | + | mode = wire.ColorMode_COLOR_MODE_TRUE_COLOR | |
| 75 | + | case terminalcap.ANSI256: | |
| 76 | + | mode = wire.ColorMode_COLOR_MODE_ANSI256 | |
| 77 | + | case terminalcap.ANSI16: | |
| 78 | + | mode = wire.ColorMode_COLOR_MODE_ANSI16 | |
| 79 | + | case terminalcap.Monochrome: | |
| 80 | + | mode = wire.ColorMode_COLOR_MODE_MONOCHROME | |
| 81 | + | default: | |
| 82 | + | return nil, errProtoConvertRange | |
| 83 | + | } | |
| 84 | + | var source wire.ColorSource | |
| 85 | + | switch color.Source { | |
| 86 | + | case terminalcap.SourceUnknown: | |
| 87 | + | source = wire.ColorSource_COLOR_SOURCE_UNSPECIFIED | |
| 88 | + | case terminalcap.SourceHeuristic: | |
| 89 | + | source = wire.ColorSource_COLOR_SOURCE_HEURISTIC | |
| 90 | + | case terminalcap.SourceDeclared: | |
| 91 | + | source = wire.ColorSource_COLOR_SOURCE_DECLARED | |
| 92 | + | case terminalcap.SourceForced: | |
| 93 | + | source = wire.ColorSource_COLOR_SOURCE_FORCED | |
| 94 | + | default: | |
| 95 | + | return nil, errProtoConvertRange | |
| 96 | + | } | |
| 97 | + | return &wire.ColorCapabilities{Mode: mode, Source: source}, nil | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | // colorFromWire is strict: Hello must claim a color capability, so a missing | |
| 101 | + | // message, an UNSPECIFIED mode, or any value outside the closed enums is a | |
| 102 | + | // decode error. An UNSPECIFIED source is the explicit "unknown" source. | |
| 103 | + | func colorFromWire(message *wire.ColorCapabilities) (terminalcap.ColorCapabilities, error) { | |
| 104 | + | if message == nil { | |
| 105 | + | return terminalcap.ColorCapabilities{}, errProtoConvertRange | |
| 106 | + | } | |
| 107 | + | var color terminalcap.ColorCapabilities | |
| 108 | + | switch message.GetMode() { | |
| 109 | + | case wire.ColorMode_COLOR_MODE_TRUE_COLOR: | |
| 110 | + | color.Mode = terminalcap.TrueColor | |
| 111 | + | case wire.ColorMode_COLOR_MODE_ANSI256: | |
| 112 | + | color.Mode = terminalcap.ANSI256 | |
| 113 | + | case wire.ColorMode_COLOR_MODE_ANSI16: | |
| 114 | + | color.Mode = terminalcap.ANSI16 | |
| 115 | + | case wire.ColorMode_COLOR_MODE_MONOCHROME: | |
| 116 | + | color.Mode = terminalcap.Monochrome | |
| 117 | + | default: | |
| 118 | + | return terminalcap.ColorCapabilities{}, errProtoConvertRange | |
| 119 | + | } | |
| 120 | + | switch message.GetSource() { | |
| 121 | + | case wire.ColorSource_COLOR_SOURCE_UNSPECIFIED: | |
| 122 | + | color.Source = terminalcap.SourceUnknown | |
| 123 | + | case wire.ColorSource_COLOR_SOURCE_HEURISTIC: | |
| 124 | + | color.Source = terminalcap.SourceHeuristic | |
| 125 | + | case wire.ColorSource_COLOR_SOURCE_DECLARED: | |
| 126 | + | color.Source = terminalcap.SourceDeclared | |
| 127 | + | case wire.ColorSource_COLOR_SOURCE_FORCED: | |
| 128 | + | color.Source = terminalcap.SourceForced | |
| 129 | + | default: | |
| 130 | + | return terminalcap.ColorCapabilities{}, errProtoConvertRange | |
| 131 | + | } | |
| 132 | + | return color, nil | |
| 133 | + | } | |
| 134 | + | ||
| 60 | 135 | func geometryToWire(size domain.Size, pixelWidth, pixelHeight int) (uint32, uint32, uint32, uint32, error) { | |
| 61 | 136 | for _, value := range []int{size.Cols, size.Rows, pixelWidth, pixelHeight} { | |
| 62 | 137 | if value < 0 || value > math.MaxUint16 { | |
| @@ -108,7 +183,10 @@ func helloFromWire(message *wire.Hello) (protocol.Hello, error) { | |||
| 108 | 183 | hello.PixelHeight = int(pixelHeight) | |
| 109 | 184 | hello.TermEnv = message.GetTermEnv() | |
| 110 | 185 | hello.Cwd = message.GetCwd() | |
| 111 | - | hello.TrueColor = message.GetTrueColor() | |
| 186 | + | hello.Color, err = colorFromWire(message.GetColor()) | |
| 187 | + | if err != nil { | |
| 188 | + | return protocol.Hello{}, err | |
| 189 | + | } | |
| 112 | 190 | window, err := mustUint8(message.GetMaxOutputInFlight()) | |
| 113 | 191 | if err != nil { | |
| 114 | 192 | return protocol.Hello{}, err | |
| @@ -11,6 +11,7 @@ import ( | |||||
| 11 | "google.golang.org/protobuf/proto" | 11 | "google.golang.org/protobuf/proto" | ||
| 12 | 12 | ||||
| 13 | "github.com/bnema/vev/internal/domain" | 13 | "github.com/bnema/vev/internal/domain" | ||
| 14 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |||
| 14 | "github.com/bnema/vev/internal/protocol" | 15 | "github.com/bnema/vev/internal/protocol" | ||
| 15 | "github.com/bnema/vev/internal/protocol/wire" | 16 | "github.com/bnema/vev/internal/protocol/wire" | ||
| 16 | ) | 17 | ) | ||
| @@ -29,6 +30,10 @@ func helloToWire(message protocol.Hello) (*wire.Hello, error) { | |||||
| 29 | return nil, err | 30 | return nil, err | ||
| 30 | } | 31 | } | ||
| 31 | exact := exactTargetToWire(message.ExactTarget) | 32 | exact := exactTargetToWire(message.ExactTarget) | ||
| 33 | + | color, err := colorToWire(message.Color) | |||
| 34 | + | if err != nil { | |||
| 35 | + | return nil, err | |||
| 36 | + | } | |||
| 32 | return &wire.Hello{ | 37 | return &wire.Hello{ | ||
| 33 | Version: uint32(message.Version), | 38 | Version: uint32(message.Version), | ||
| 34 | Intent: uint32(message.Intent), | 39 | Intent: uint32(message.Intent), | ||
| @@ -41,7 +46,7 @@ func helloToWire(message protocol.Hello) (*wire.Hello, error) { | |||||
| 41 | PixelHeight: pixelHeight, | 46 | PixelHeight: pixelHeight, | ||
| 42 | TermEnv: message.TermEnv, | 47 | TermEnv: message.TermEnv, | ||
| 43 | Cwd: message.Cwd, | 48 | Cwd: message.Cwd, | ||
| 44 | - | TrueColor: message.TrueColor, | 49 | + | Color: color, |
| 45 | MaxOutputInFlight: uint32(message.MaxOutputInFlight), | 50 | MaxOutputInFlight: uint32(message.MaxOutputInFlight), | ||
| 46 | Env: append([]string(nil), message.Env...), | 51 | Env: append([]string(nil), message.Env...), | ||
| 47 | SessionTarget: sessionTarget, | 52 | SessionTarget: sessionTarget, | ||
| @@ -57,6 +62,76 @@ func helloToWire(message protocol.Hello) (*wire.Hello, error) { | |||||
| 57 | }, nil | 62 | }, nil | ||
| 58 | } | 63 | } | ||
| 59 | 64 | ||||
| 65 | + | // colorToWire maps a color capability to its wire message. Unknown semantic | |||
| 66 | + | // values are never encoded. | |||
| 67 | + | func colorToWire(color terminalcap.ColorCapabilities) (*wire.ColorCapabilities, error) { | |||
| 68 | + | if !color.Valid() { | |||
| 69 | + | return nil, errProtoConvertRange | |||
| 70 | + | } | |||
| 71 | + | var mode wire.ColorMode | |||
| 72 | + | switch color.Mode { | |||
| 73 | + | case terminalcap.TrueColor: | |||
| 74 | + | mode = wire.ColorMode_COLOR_MODE_TRUE_COLOR | |||
| 75 | + | case terminalcap.ANSI256: | |||
| 76 | + | mode = wire.ColorMode_COLOR_MODE_ANSI256 | |||
| 77 | + | case terminalcap.ANSI16: | |||
| 78 | + | mode = wire.ColorMode_COLOR_MODE_ANSI16 | |||
| 79 | + | case terminalcap.Monochrome: | |||
| 80 | + | mode = wire.ColorMode_COLOR_MODE_MONOCHROME | |||
| 81 | + | default: | |||
| 82 | + | return nil, errProtoConvertRange | |||
| 83 | + | } | |||
| 84 | + | var source wire.ColorSource | |||
| 85 | + | switch color.Source { | |||
| 86 | + | case terminalcap.SourceUnknown: | |||
| 87 | + | source = wire.ColorSource_COLOR_SOURCE_UNSPECIFIED | |||
| 88 | + | case terminalcap.SourceHeuristic: | |||
| 89 | + | source = wire.ColorSource_COLOR_SOURCE_HEURISTIC | |||
| 90 | + | case terminalcap.SourceDeclared: | |||
| 91 | + | source = wire.ColorSource_COLOR_SOURCE_DECLARED | |||
| 92 | + | case terminalcap.SourceForced: | |||
| 93 | + | source = wire.ColorSource_COLOR_SOURCE_FORCED | |||
| 94 | + | default: | |||
| 95 | + | return nil, errProtoConvertRange | |||
| 96 | + | } | |||
| 97 | + | return &wire.ColorCapabilities{Mode: mode, Source: source}, nil | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | // colorFromWire is strict: Hello must claim a color capability, so a missing | |||
| 101 | + | // message, an UNSPECIFIED mode, or any value outside the closed enums is a | |||
| 102 | + | // decode error. An UNSPECIFIED source is the explicit "unknown" source. | |||
| 103 | + | func colorFromWire(message *wire.ColorCapabilities) (terminalcap.ColorCapabilities, error) { | |||
| 104 | + | if message == nil { | |||
| 105 | + | return terminalcap.ColorCapabilities{}, errProtoConvertRange | |||
| 106 | + | } | |||
| 107 | + | var color terminalcap.ColorCapabilities | |||
| 108 | + | switch message.GetMode() { | |||
| 109 | + | case wire.ColorMode_COLOR_MODE_TRUE_COLOR: | |||
| 110 | + | color.Mode = terminalcap.TrueColor | |||
| 111 | + | case wire.ColorMode_COLOR_MODE_ANSI256: | |||
| 112 | + | color.Mode = terminalcap.ANSI256 | |||
| 113 | + | case wire.ColorMode_COLOR_MODE_ANSI16: | |||
| 114 | + | color.Mode = terminalcap.ANSI16 | |||
| 115 | + | case wire.ColorMode_COLOR_MODE_MONOCHROME: | |||
| 116 | + | color.Mode = terminalcap.Monochrome | |||
| 117 | + | default: | |||
| 118 | + | return terminalcap.ColorCapabilities{}, errProtoConvertRange | |||
| 119 | + | } | |||
| 120 | + | switch message.GetSource() { | |||
| 121 | + | case wire.ColorSource_COLOR_SOURCE_UNSPECIFIED: | |||
| 122 | + | color.Source = terminalcap.SourceUnknown | |||
| 123 | + | case wire.ColorSource_COLOR_SOURCE_HEURISTIC: | |||
| 124 | + | color.Source = terminalcap.SourceHeuristic | |||
| 125 | + | case wire.ColorSource_COLOR_SOURCE_DECLARED: | |||
| 126 | + | color.Source = terminalcap.SourceDeclared | |||
| 127 | + | case wire.ColorSource_COLOR_SOURCE_FORCED: | |||
| 128 | + | color.Source = terminalcap.SourceForced | |||
| 129 | + | default: | |||
| 130 | + | return terminalcap.ColorCapabilities{}, errProtoConvertRange | |||
| 131 | + | } | |||
| 132 | + | return color, nil | |||
| 133 | + | } | |||
| 134 | + | ||||
| 60 | func geometryToWire(size domain.Size, pixelWidth, pixelHeight int) (uint32, uint32, uint32, uint32, error) { | 135 | func geometryToWire(size domain.Size, pixelWidth, pixelHeight int) (uint32, uint32, uint32, uint32, error) { | ||
| 61 | for _, value := range []int{size.Cols, size.Rows, pixelWidth, pixelHeight} { | 136 | for _, value := range []int{size.Cols, size.Rows, pixelWidth, pixelHeight} { | ||
| 62 | if value < 0 || value > math.MaxUint16 { | 137 | if value < 0 || value > math.MaxUint16 { | ||
| @@ -108,7 +183,10 @@ func helloFromWire(message *wire.Hello) (protocol.Hello, error) { | |||||
| 108 | hello.PixelHeight = int(pixelHeight) | 183 | hello.PixelHeight = int(pixelHeight) | ||
| 109 | hello.TermEnv = message.GetTermEnv() | 184 | hello.TermEnv = message.GetTermEnv() | ||
| 110 | hello.Cwd = message.GetCwd() | 185 | hello.Cwd = message.GetCwd() | ||
| 111 | - | hello.TrueColor = message.GetTrueColor() | 186 | + | hello.Color, err = colorFromWire(message.GetColor()) |
| 187 | + | if err != nil { | |||
| 188 | + | return protocol.Hello{}, err | |||
| 189 | + | } | |||
| 112 | window, err := mustUint8(message.GetMaxOutputInFlight()) | 190 | window, err := mustUint8(message.GetMaxOutputInFlight()) | ||
| 113 | if err != nil { | 191 | if err != nil { | ||
| 114 | return protocol.Hello{}, err | 192 | return protocol.Hello{}, err | ||
M internal/adapters/sessionwire/proto_convert_test.go +96 -1
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "google.golang.org/protobuf/proto" | |
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/vev/internal/domain" | |
| 13 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 13 | 14 | "github.com/bnema/vev/internal/protocol" | |
| 14 | 15 | "github.com/bnema/vev/internal/protocol/wire" | |
| 15 | 16 | "github.com/stretchr/testify/require" | |
| @@ -40,13 +41,15 @@ func TestProtoClientRoundTrips(t *testing.T) { | |||
| 40 | 41 | protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}, | |
| 41 | 42 | protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, KittyDirectGraphics: true, KittyKeyboard: true}, | |
| 42 | 43 | protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, ClientPID: 4242}, | |
| 44 | + | protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceForced}}, | |
| 45 | + | protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, Color: terminalcap.ColorCapabilities{Mode: terminalcap.Monochrome, Source: terminalcap.SourceDeclared}}, | |
| 43 | 46 | protocol.Input{InputSeq: 1, ActionID: 2, Data: []byte("x")}, | |
| 44 | 47 | protocol.Resize{Size: domain.Size{Cols: 80, Rows: 24}}, | |
| 45 | 48 | protocol.Detach{}, | |
| 46 | 49 | protocol.Ping{}, | |
| 47 | 50 | protocol.List{}, | |
| 48 | 51 | protocol.Kill{RequestID: 1, Name: "work"}, | |
| 49 | - | protocol.Theme{TrueColor: true}, | |
| 52 | + | protocol.Theme{SchemeKnown: true}, | |
| 50 | 53 | protocol.Ack{Epoch: 1, State: 1}, | |
| 51 | 54 | protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}}, | |
| 52 | 55 | protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}, | |
| @@ -353,3 +356,95 @@ func protoShortName(message any) string { | |||
| 353 | 356 | } | |
| 354 | 357 | return reflect.TypeOf(message).Name() | |
| 355 | 358 | } | |
| 359 | + | ||
| 360 | + | // TestProtoHelloColorWire pins the Hello color capability byte for byte for | |
| 361 | + | // every mode and source, and refuses truncated prefixes, trailing garbage, | |
| 362 | + | // and values outside the closed enums. | |
| 363 | + | func TestProtoHelloColorWire(t *testing.T) { | |
| 364 | + | modes := []struct { | |
| 365 | + | name string | |
| 366 | + | mode terminalcap.ColorMode | |
| 367 | + | code byte | |
| 368 | + | }{ | |
| 369 | + | {name: "truecolor", mode: terminalcap.TrueColor, code: 1}, | |
| 370 | + | {name: "ansi256", mode: terminalcap.ANSI256, code: 2}, | |
| 371 | + | {name: "ansi16", mode: terminalcap.ANSI16, code: 3}, | |
| 372 | + | {name: "monochrome", mode: terminalcap.Monochrome, code: 4}, | |
| 373 | + | } | |
| 374 | + | sources := []struct { | |
| 375 | + | name string | |
| 376 | + | source terminalcap.Source | |
| 377 | + | code byte | |
| 378 | + | }{ | |
| 379 | + | {name: "unknown", source: terminalcap.SourceUnknown, code: 0}, | |
| 380 | + | {name: "heuristic", source: terminalcap.SourceHeuristic, code: 1}, | |
| 381 | + | {name: "declared", source: terminalcap.SourceDeclared, code: 2}, | |
| 382 | + | {name: "forced", source: terminalcap.SourceForced, code: 3}, | |
| 383 | + | } | |
| 384 | + | for _, m := range modes { | |
| 385 | + | for _, src := range sources { | |
| 386 | + | t.Run(m.name+"/"+src.name, func(t *testing.T) { | |
| 387 | + | color := terminalcap.ColorCapabilities{Mode: m.mode, Source: src.source} | |
| 388 | + | // The minimal attach Hello: version, intent, empty client id, cols, | |
| 389 | + | // rows, then color (field 26, length-delimited), preceded by the | |
| 390 | + | // oneof tag for Hello (field 1). | |
| 391 | + | colorBytes := []byte{0xD2, 0x01} | |
| 392 | + | if src.code == 0 { | |
| 393 | + | colorBytes = append(colorBytes, 0x02, 0x08, m.code) | |
| 394 | + | } else { | |
| 395 | + | colorBytes = append(colorBytes, 0x04, 0x08, m.code, 0x10, src.code) | |
| 396 | + | } | |
| 397 | + | hello := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, ClientID: [16]byte{1}, Size: domain.Size{Cols: 80, Rows: 24}, Color: color} | |
| 398 | + | envelope, err := encodeProtoClient(hello) | |
| 399 | + | require.NoError(t, err) | |
| 400 | + | raw, err := proto.MarshalOptions{Deterministic: true}.Marshal(envelope) | |
| 401 | + | require.NoError(t, err) | |
| 402 | + | require.True(t, bytes.HasSuffix(raw, colorBytes), "color is the last Hello field on the wire: % x", raw) | |
| 403 | + | // ClientEnvelope tag 1 (Hello), then the Hello length. | |
| 404 | + | require.Equal(t, byte(0x0A), raw[0]) | |
| 405 | + | ||
| 406 | + | require.Error(t, wire.ScanEnvelope(&wire.ClientEnvelope{}, raw[:len(raw)-1]), "truncated prefix") | |
| 407 | + | require.Error(t, wire.ScanEnvelope(&wire.ClientEnvelope{}, append(append([]byte(nil), raw...), 0xFF)), "trailing garbage") | |
| 408 | + | require.NoError(t, wire.ScanEnvelope(&wire.ClientEnvelope{}, raw)) | |
| 409 | + | decoded := &wire.ClientEnvelope{} | |
| 410 | + | require.NoError(t, proto.Unmarshal(raw, decoded)) | |
| 411 | + | got, err := decodeProtoClient(decoded) | |
| 412 | + | require.NoError(t, err) | |
| 413 | + | require.Equal(t, hello, got) | |
| 414 | + | }) | |
| 415 | + | } | |
| 416 | + | } | |
| 417 | + | ||
| 418 | + | t.Run("missing color is rejected", func(t *testing.T) { | |
| 419 | + | message := &wire.Hello{Version: uint32(protocol.Version), Intent: uint32(protocol.IntentAttach), ClientId: make([]byte, 16), Cols: 80, Rows: 24} | |
| 420 | + | raw, err := proto.Marshal(&wire.ClientEnvelope{Payload: &wire.ClientEnvelope_Hello{Hello: message}}) | |
| 421 | + | require.NoError(t, err) | |
| 422 | + | require.NoError(t, wire.ScanEnvelope(&wire.ClientEnvelope{}, raw), "structurally valid") | |
| 423 | + | decoded := &wire.ClientEnvelope{} | |
| 424 | + | require.NoError(t, proto.Unmarshal(raw, decoded)) | |
| 425 | + | _, err = decodeProtoClient(decoded) | |
| 426 | + | require.Error(t, err) | |
| 427 | + | }) | |
| 428 | + | ||
| 429 | + | t.Run("unspecified mode on the wire is rejected", func(t *testing.T) { | |
| 430 | + | message := &wire.Hello{Version: uint32(protocol.Version), Intent: uint32(protocol.IntentAttach), ClientId: make([]byte, 16), Cols: 80, Rows: 24, Color: &wire.ColorCapabilities{Source: wire.ColorSource_COLOR_SOURCE_DECLARED}} | |
| 431 | + | _, err := decodeProtoClient(&wire.ClientEnvelope{Payload: &wire.ClientEnvelope_Hello{Hello: message}}) | |
| 432 | + | require.Error(t, err) | |
| 433 | + | }) | |
| 434 | + | ||
| 435 | + | t.Run("unknown enum values are rejected", func(t *testing.T) { | |
| 436 | + | for _, color := range []*wire.ColorCapabilities{ | |
| 437 | + | {Mode: wire.ColorMode(5)}, | |
| 438 | + | {Mode: wire.ColorMode_COLOR_MODE_TRUE_COLOR, Source: wire.ColorSource(4)}, | |
| 439 | + | } { | |
| 440 | + | message := &wire.Hello{Version: uint32(protocol.Version), Intent: uint32(protocol.IntentAttach), ClientId: make([]byte, 16), Cols: 80, Rows: 24, Color: color} | |
| 441 | + | _, err := decodeProtoClient(&wire.ClientEnvelope{Payload: &wire.ClientEnvelope_Hello{Hello: message}}) | |
| 442 | + | require.Error(t, err) | |
| 443 | + | } | |
| 444 | + | }) | |
| 445 | + | ||
| 446 | + | t.Run("invalid semantic color is never encoded", func(t *testing.T) { | |
| 447 | + | _, err := encodeProtoClient(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, Color: terminalcap.ColorCapabilities{Mode: terminalcap.ColorMode(9)}}) | |
| 448 | + | require.Error(t, err) | |
| 449 | + | }) | |
| 450 | + | } | |
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | "google.golang.org/protobuf/proto" | 10 | "google.golang.org/protobuf/proto" | ||
| 11 | 11 | ||||
| 12 | "github.com/bnema/vev/internal/domain" | 12 | "github.com/bnema/vev/internal/domain" | ||
| 13 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |||
| 13 | "github.com/bnema/vev/internal/protocol" | 14 | "github.com/bnema/vev/internal/protocol" | ||
| 14 | "github.com/bnema/vev/internal/protocol/wire" | 15 | "github.com/bnema/vev/internal/protocol/wire" | ||
| 15 | "github.com/stretchr/testify/require" | 16 | "github.com/stretchr/testify/require" | ||
| @@ -40,13 +41,15 @@ func TestProtoClientRoundTrips(t *testing.T) { | |||||
| 40 | protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}, | 41 | protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}, | ||
| 41 | protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, KittyDirectGraphics: true, KittyKeyboard: true}, | 42 | protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, KittyDirectGraphics: true, KittyKeyboard: true}, | ||
| 42 | protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, ClientPID: 4242}, | 43 | protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, ClientPID: 4242}, | ||
| 44 | + | protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceForced}}, | |||
| 45 | + | protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, Color: terminalcap.ColorCapabilities{Mode: terminalcap.Monochrome, Source: terminalcap.SourceDeclared}}, | |||
| 43 | protocol.Input{InputSeq: 1, ActionID: 2, Data: []byte("x")}, | 46 | protocol.Input{InputSeq: 1, ActionID: 2, Data: []byte("x")}, | ||
| 44 | protocol.Resize{Size: domain.Size{Cols: 80, Rows: 24}}, | 47 | protocol.Resize{Size: domain.Size{Cols: 80, Rows: 24}}, | ||
| 45 | protocol.Detach{}, | 48 | protocol.Detach{}, | ||
| 46 | protocol.Ping{}, | 49 | protocol.Ping{}, | ||
| 47 | protocol.List{}, | 50 | protocol.List{}, | ||
| 48 | protocol.Kill{RequestID: 1, Name: "work"}, | 51 | protocol.Kill{RequestID: 1, Name: "work"}, | ||
| 49 | - | protocol.Theme{TrueColor: true}, | 52 | + | protocol.Theme{SchemeKnown: true}, |
| 50 | protocol.Ack{Epoch: 1, State: 1}, | 53 | protocol.Ack{Epoch: 1, State: 1}, | ||
| 51 | protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}}, | 54 | protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}}, | ||
| 52 | protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}, | 55 | protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}, | ||
| @@ -353,3 +356,95 @@ func protoShortName(message any) string { | |||||
| 353 | } | 356 | } | ||
| 354 | return reflect.TypeOf(message).Name() | 357 | return reflect.TypeOf(message).Name() | ||
| 355 | } | 358 | } | ||
| 359 | + | ||||
| 360 | + | // TestProtoHelloColorWire pins the Hello color capability byte for byte for | |||
| 361 | + | // every mode and source, and refuses truncated prefixes, trailing garbage, | |||
| 362 | + | // and values outside the closed enums. | |||
| 363 | + | func TestProtoHelloColorWire(t *testing.T) { | |||
| 364 | + | modes := []struct { | |||
| 365 | + | name string | |||
| 366 | + | mode terminalcap.ColorMode | |||
| 367 | + | code byte | |||
| 368 | + | }{ | |||
| 369 | + | {name: "truecolor", mode: terminalcap.TrueColor, code: 1}, | |||
| 370 | + | {name: "ansi256", mode: terminalcap.ANSI256, code: 2}, | |||
| 371 | + | {name: "ansi16", mode: terminalcap.ANSI16, code: 3}, | |||
| 372 | + | {name: "monochrome", mode: terminalcap.Monochrome, code: 4}, | |||
| 373 | + | } | |||
| 374 | + | sources := []struct { | |||
| 375 | + | name string | |||
| 376 | + | source terminalcap.Source | |||
| 377 | + | code byte | |||
| 378 | + | }{ | |||
| 379 | + | {name: "unknown", source: terminalcap.SourceUnknown, code: 0}, | |||
| 380 | + | {name: "heuristic", source: terminalcap.SourceHeuristic, code: 1}, | |||
| 381 | + | {name: "declared", source: terminalcap.SourceDeclared, code: 2}, | |||
| 382 | + | {name: "forced", source: terminalcap.SourceForced, code: 3}, | |||
| 383 | + | } | |||
| 384 | + | for _, m := range modes { | |||
| 385 | + | for _, src := range sources { | |||
| 386 | + | t.Run(m.name+"/"+src.name, func(t *testing.T) { | |||
| 387 | + | color := terminalcap.ColorCapabilities{Mode: m.mode, Source: src.source} | |||
| 388 | + | // The minimal attach Hello: version, intent, empty client id, cols, | |||
| 389 | + | // rows, then color (field 26, length-delimited), preceded by the | |||
| 390 | + | // oneof tag for Hello (field 1). | |||
| 391 | + | colorBytes := []byte{0xD2, 0x01} | |||
| 392 | + | if src.code == 0 { | |||
| 393 | + | colorBytes = append(colorBytes, 0x02, 0x08, m.code) | |||
| 394 | + | } else { | |||
| 395 | + | colorBytes = append(colorBytes, 0x04, 0x08, m.code, 0x10, src.code) | |||
| 396 | + | } | |||
| 397 | + | hello := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, ClientID: [16]byte{1}, Size: domain.Size{Cols: 80, Rows: 24}, Color: color} | |||
| 398 | + | envelope, err := encodeProtoClient(hello) | |||
| 399 | + | require.NoError(t, err) | |||
| 400 | + | raw, err := proto.MarshalOptions{Deterministic: true}.Marshal(envelope) | |||
| 401 | + | require.NoError(t, err) | |||
| 402 | + | require.True(t, bytes.HasSuffix(raw, colorBytes), "color is the last Hello field on the wire: % x", raw) | |||
| 403 | + | // ClientEnvelope tag 1 (Hello), then the Hello length. | |||
| 404 | + | require.Equal(t, byte(0x0A), raw[0]) | |||
| 405 | + | ||||
| 406 | + | require.Error(t, wire.ScanEnvelope(&wire.ClientEnvelope{}, raw[:len(raw)-1]), "truncated prefix") | |||
| 407 | + | require.Error(t, wire.ScanEnvelope(&wire.ClientEnvelope{}, append(append([]byte(nil), raw...), 0xFF)), "trailing garbage") | |||
| 408 | + | require.NoError(t, wire.ScanEnvelope(&wire.ClientEnvelope{}, raw)) | |||
| 409 | + | decoded := &wire.ClientEnvelope{} | |||
| 410 | + | require.NoError(t, proto.Unmarshal(raw, decoded)) | |||
| 411 | + | got, err := decodeProtoClient(decoded) | |||
| 412 | + | require.NoError(t, err) | |||
| 413 | + | require.Equal(t, hello, got) | |||
| 414 | + | }) | |||
| 415 | + | } | |||
| 416 | + | } | |||
| 417 | + | ||||
| 418 | + | t.Run("missing color is rejected", func(t *testing.T) { | |||
| 419 | + | message := &wire.Hello{Version: uint32(protocol.Version), Intent: uint32(protocol.IntentAttach), ClientId: make([]byte, 16), Cols: 80, Rows: 24} | |||
| 420 | + | raw, err := proto.Marshal(&wire.ClientEnvelope{Payload: &wire.ClientEnvelope_Hello{Hello: message}}) | |||
| 421 | + | require.NoError(t, err) | |||
| 422 | + | require.NoError(t, wire.ScanEnvelope(&wire.ClientEnvelope{}, raw), "structurally valid") | |||
| 423 | + | decoded := &wire.ClientEnvelope{} | |||
| 424 | + | require.NoError(t, proto.Unmarshal(raw, decoded)) | |||
| 425 | + | _, err = decodeProtoClient(decoded) | |||
| 426 | + | require.Error(t, err) | |||
| 427 | + | }) | |||
| 428 | + | ||||
| 429 | + | t.Run("unspecified mode on the wire is rejected", func(t *testing.T) { | |||
| 430 | + | message := &wire.Hello{Version: uint32(protocol.Version), Intent: uint32(protocol.IntentAttach), ClientId: make([]byte, 16), Cols: 80, Rows: 24, Color: &wire.ColorCapabilities{Source: wire.ColorSource_COLOR_SOURCE_DECLARED}} | |||
| 431 | + | _, err := decodeProtoClient(&wire.ClientEnvelope{Payload: &wire.ClientEnvelope_Hello{Hello: message}}) | |||
| 432 | + | require.Error(t, err) | |||
| 433 | + | }) | |||
| 434 | + | ||||
| 435 | + | t.Run("unknown enum values are rejected", func(t *testing.T) { | |||
| 436 | + | for _, color := range []*wire.ColorCapabilities{ | |||
| 437 | + | {Mode: wire.ColorMode(5)}, | |||
| 438 | + | {Mode: wire.ColorMode_COLOR_MODE_TRUE_COLOR, Source: wire.ColorSource(4)}, | |||
| 439 | + | } { | |||
| 440 | + | message := &wire.Hello{Version: uint32(protocol.Version), Intent: uint32(protocol.IntentAttach), ClientId: make([]byte, 16), Cols: 80, Rows: 24, Color: color} | |||
| 441 | + | _, err := decodeProtoClient(&wire.ClientEnvelope{Payload: &wire.ClientEnvelope_Hello{Hello: message}}) | |||
| 442 | + | require.Error(t, err) | |||
| 443 | + | } | |||
| 444 | + | }) | |||
| 445 | + | ||||
| 446 | + | t.Run("invalid semantic color is never encoded", func(t *testing.T) { | |||
| 447 | + | _, err := encodeProtoClient(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, Color: terminalcap.ColorCapabilities{Mode: terminalcap.ColorMode(9)}}) | |||
| 448 | + | require.Error(t, err) | |||
| 449 | + | }) | |||
| 450 | + | } | |||
M internal/adapters/sessionwire/server_test.go +1 -1
| @@ -30,7 +30,7 @@ func TestServerConnectionDecodesEveryClientMessage(t *testing.T) { | |||
| 30 | 30 | {name: "ping", want: protocol.Ping{}}, | |
| 31 | 31 | {name: "list", want: protocol.List{}}, | |
| 32 | 32 | {name: "kill", want: protocol.Kill{RequestID: 1, Name: "work"}}, | |
| 33 | - | {name: "theme", want: protocol.Theme{TrueColor: true}}, | |
| 33 | + | {name: "theme", want: protocol.Theme{SchemeKnown: true}}, | |
| 34 | 34 | {name: "ack", want: protocol.Ack{Epoch: 1, State: 2}}, | |
| 35 | 35 | {name: "image", want: protocol.ImagePush{InputSeq: 2, Mime: "image/png", Data: []byte{1}}}, | |
| 36 | 36 | {name: "notice", want: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}}, | |
| @@ -30,7 +30,7 @@ func TestServerConnectionDecodesEveryClientMessage(t *testing.T) { | |||||
| 30 | {name: "ping", want: protocol.Ping{}}, | 30 | {name: "ping", want: protocol.Ping{}}, | ||
| 31 | {name: "list", want: protocol.List{}}, | 31 | {name: "list", want: protocol.List{}}, | ||
| 32 | {name: "kill", want: protocol.Kill{RequestID: 1, Name: "work"}}, | 32 | {name: "kill", want: protocol.Kill{RequestID: 1, Name: "work"}}, | ||
| 33 | - | {name: "theme", want: protocol.Theme{TrueColor: true}}, | 33 | + | {name: "theme", want: protocol.Theme{SchemeKnown: true}}, |
| 34 | {name: "ack", want: protocol.Ack{Epoch: 1, State: 2}}, | 34 | {name: "ack", want: protocol.Ack{Epoch: 1, State: 2}}, | ||
| 35 | {name: "image", want: protocol.ImagePush{InputSeq: 2, Mime: "image/png", Data: []byte{1}}}, | 35 | {name: "image", want: protocol.ImagePush{InputSeq: 2, Mime: "image/png", Data: []byte{1}}}, | ||
| 36 | {name: "notice", want: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}}, | 36 | {name: "notice", want: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}}, | ||
M internal/app/daemon_integration_test.go +2 -1
| @@ -28,6 +28,7 @@ import ( | |||
| 28 | 28 | "github.com/bnema/vev/internal/adapters/sessionwire" | |
| 29 | 29 | "github.com/bnema/vev/internal/adapters/snapshot" | |
| 30 | 30 | "github.com/bnema/vev/internal/domain" | |
| 31 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 31 | 32 | "github.com/bnema/vev/internal/persist" | |
| 32 | 33 | "github.com/bnema/vev/internal/ports" | |
| 33 | 34 | "github.com/bnema/vev/internal/protocol" | |
| @@ -115,7 +116,7 @@ func attachWithEnvironment(t *testing.T, dir string, intent uint8, name string, | |||
| 115 | 116 | raw, err := ipc.DialContext(context.Background(), dir) | |
| 116 | 117 | require.NoError(t, err) | |
| 117 | 118 | conn := sessionwire.NewClientConnection(raw) | |
| 118 | - | hello := protocol.Hello{Version: protocol.Version, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color", TrueColor: true, Env: env} | |
| 119 | + | hello := protocol.Hello{Version: protocol.Version, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color", Color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor, Source: terminalcap.SourceDeclared}, Env: env} | |
| 119 | 120 | require.NoError(t, conn.SendClient(hello)) | |
| 120 | 121 | p := recvTypedPump(conn) | |
| 121 | 122 | select { | |
| @@ -28,6 +28,7 @@ import ( | |||||
| 28 | "github.com/bnema/vev/internal/adapters/sessionwire" | 28 | "github.com/bnema/vev/internal/adapters/sessionwire" | ||
| 29 | "github.com/bnema/vev/internal/adapters/snapshot" | 29 | "github.com/bnema/vev/internal/adapters/snapshot" | ||
| 30 | "github.com/bnema/vev/internal/domain" | 30 | "github.com/bnema/vev/internal/domain" | ||
| 31 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |||
| 31 | "github.com/bnema/vev/internal/persist" | 32 | "github.com/bnema/vev/internal/persist" | ||
| 32 | "github.com/bnema/vev/internal/ports" | 33 | "github.com/bnema/vev/internal/ports" | ||
| 33 | "github.com/bnema/vev/internal/protocol" | 34 | "github.com/bnema/vev/internal/protocol" | ||
| @@ -115,7 +116,7 @@ func attachWithEnvironment(t *testing.T, dir string, intent uint8, name string, | |||||
| 115 | raw, err := ipc.DialContext(context.Background(), dir) | 116 | raw, err := ipc.DialContext(context.Background(), dir) | ||
| 116 | require.NoError(t, err) | 117 | require.NoError(t, err) | ||
| 117 | conn := sessionwire.NewClientConnection(raw) | 118 | conn := sessionwire.NewClientConnection(raw) | ||
| 118 | - | hello := protocol.Hello{Version: protocol.Version, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color", TrueColor: true, Env: env} | 119 | + | hello := protocol.Hello{Version: protocol.Version, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color", Color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor, Source: terminalcap.SourceDeclared}, Env: env} |
| 119 | require.NoError(t, conn.SendClient(hello)) | 120 | require.NoError(t, conn.SendClient(hello)) | ||
| 120 | p := recvTypedPump(conn) | 121 | p := recvTypedPump(conn) | ||
| 121 | select { | 122 | select { | ||
M internal/domain/terminalcap/capabilities.go +28 -5
| @@ -11,8 +11,13 @@ const ( | |||
| 11 | 11 | // historical renderer behavior; live Hello paths always use the detector. | |
| 12 | 12 | TrueColor ColorMode = iota | |
| 13 | 13 | ANSI256 | |
| 14 | + | ANSI16 | |
| 15 | + | Monochrome | |
| 14 | 16 | ) | |
| 15 | 17 | ||
| 18 | + | // Valid reports whether m is one of the defined color modes. | |
| 19 | + | func (m ColorMode) Valid() bool { return m <= Monochrome } | |
| 20 | + | ||
| 16 | 21 | // ColorCapabilities is the color output capability selected for one attachment | |
| 17 | 22 | // and how confidently it was selected. | |
| 18 | 23 | type ColorCapabilities struct { | |
| @@ -20,6 +25,9 @@ type ColorCapabilities struct { | |||
| 20 | 25 | Source Source | |
| 21 | 26 | } | |
| 22 | 27 | ||
| 28 | + | // Valid reports whether both the mode and the source are defined values. | |
| 29 | + | func (c ColorCapabilities) Valid() bool { return c.Mode.Valid() && c.Source.Valid() } | |
| 30 | + | ||
| 23 | 31 | // RGB reports whether the attachment can receive RGB ANSI output. | |
| 24 | 32 | func (c ColorCapabilities) RGB() bool { return c.Mode == TrueColor } | |
| 25 | 33 | ||
| @@ -28,6 +36,10 @@ func (c ColorCapabilities) Colors() int { | |||
| 28 | 36 | switch c.Mode { | |
| 29 | 37 | case ANSI256: | |
| 30 | 38 | return 256 | |
| 39 | + | case ANSI16: | |
| 40 | + | return 16 | |
| 41 | + | case Monochrome: | |
| 42 | + | return 0 | |
| 31 | 43 | default: | |
| 32 | 44 | return 1 << 24 | |
| 33 | 45 | } | |
| @@ -40,8 +52,14 @@ const ( | |||
| 40 | 52 | SourceUnknown Source = iota | |
| 41 | 53 | SourceHeuristic | |
| 42 | 54 | SourceDeclared | |
| 55 | + | // SourceForced is an explicit user override that detection must not | |
| 56 | + | // second-guess and that never warrants a downgrade notice. | |
| 57 | + | SourceForced | |
| 43 | 58 | ) | |
| 44 | 59 | ||
| 60 | + | // Valid reports whether s is one of the defined sources. | |
| 61 | + | func (s Source) Valid() bool { return s <= SourceForced } | |
| 62 | + | ||
| 45 | 63 | // Application identifies a known terminal application when its environment | |
| 46 | 64 | // provides a trustworthy origin signal. | |
| 47 | 65 | type Application uint8 | |
| @@ -111,12 +129,17 @@ func environmentValues(env []string) map[string]string { | |||
| 111 | 129 | } | |
| 112 | 130 | ||
| 113 | 131 | // Resolve selects attachment capabilities from the client environment and the | |
| 114 | - | // client's explicit color declaration. A declared truecolor client upgrades a | |
| 115 | - | // weaker environment detection to declared TrueColor. | |
| 116 | - | func Resolve(env []string, declaredTrueColor bool) Capabilities { | |
| 132 | + | // client's color claim. A declared or forced client color is used verbatim. A | |
| 133 | + | // heuristic truecolor claim upgrades a weaker daemon-side detection, because | |
| 134 | + | // the daemon may not see the client environment that produced the inference. | |
| 135 | + | // Any other claim falls back to environment detection. | |
| 136 | + | func Resolve(env []string, declared ColorCapabilities) Capabilities { | |
| 117 | 137 | caps := Detect(env) | |
| 118 | - | if declaredTrueColor && !caps.Color.RGB() { | |
| 119 | - | caps.Color = ColorCapabilities{Mode: TrueColor, Source: SourceDeclared} | |
| 138 | + | switch { | |
| 139 | + | case declared.Source == SourceDeclared || declared.Source == SourceForced: | |
| 140 | + | caps.Color = declared | |
| 141 | + | case declared.Source == SourceHeuristic && declared.RGB() && !caps.Color.RGB(): | |
| 142 | + | caps.Color = declared | |
| 120 | 143 | } | |
| 121 | 144 | return caps | |
| 122 | 145 | } | |
| @@ -11,8 +11,13 @@ const ( | |||||
| 11 | // historical renderer behavior; live Hello paths always use the detector. | 11 | // historical renderer behavior; live Hello paths always use the detector. | ||
| 12 | TrueColor ColorMode = iota | 12 | TrueColor ColorMode = iota | ||
| 13 | ANSI256 | 13 | ANSI256 | ||
| 14 | + | ANSI16 | |||
| 15 | + | Monochrome | |||
| 14 | ) | 16 | ) | ||
| 15 | 17 | ||||
| 18 | + | // Valid reports whether m is one of the defined color modes. | |||
| 19 | + | func (m ColorMode) Valid() bool { return m <= Monochrome } | |||
| 20 | + | ||||
| 16 | // ColorCapabilities is the color output capability selected for one attachment | 21 | // ColorCapabilities is the color output capability selected for one attachment | ||
| 17 | // and how confidently it was selected. | 22 | // and how confidently it was selected. | ||
| 18 | type ColorCapabilities struct { | 23 | type ColorCapabilities struct { | ||
| @@ -20,6 +25,9 @@ type ColorCapabilities struct { | |||||
| 20 | Source Source | 25 | Source Source | ||
| 21 | } | 26 | } | ||
| 22 | 27 | ||||
| 28 | + | // Valid reports whether both the mode and the source are defined values. | |||
| 29 | + | func (c ColorCapabilities) Valid() bool { return c.Mode.Valid() && c.Source.Valid() } | |||
| 30 | + | ||||
| 23 | // RGB reports whether the attachment can receive RGB ANSI output. | 31 | // RGB reports whether the attachment can receive RGB ANSI output. | ||
| 24 | func (c ColorCapabilities) RGB() bool { return c.Mode == TrueColor } | 32 | func (c ColorCapabilities) RGB() bool { return c.Mode == TrueColor } | ||
| 25 | 33 | ||||
| @@ -28,6 +36,10 @@ func (c ColorCapabilities) Colors() int { | |||||
| 28 | switch c.Mode { | 36 | switch c.Mode { | ||
| 29 | case ANSI256: | 37 | case ANSI256: | ||
| 30 | return 256 | 38 | return 256 | ||
| 39 | + | case ANSI16: | |||
| 40 | + | return 16 | |||
| 41 | + | case Monochrome: | |||
| 42 | + | return 0 | |||
| 31 | default: | 43 | default: | ||
| 32 | return 1 << 24 | 44 | return 1 << 24 | ||
| 33 | } | 45 | } | ||
| @@ -40,8 +52,14 @@ const ( | |||||
| 40 | SourceUnknown Source = iota | 52 | SourceUnknown Source = iota | ||
| 41 | SourceHeuristic | 53 | SourceHeuristic | ||
| 42 | SourceDeclared | 54 | SourceDeclared | ||
| 55 | + | // SourceForced is an explicit user override that detection must not | |||
| 56 | + | // second-guess and that never warrants a downgrade notice. | |||
| 57 | + | SourceForced | |||
| 43 | ) | 58 | ) | ||
| 44 | 59 | ||||
| 60 | + | // Valid reports whether s is one of the defined sources. | |||
| 61 | + | func (s Source) Valid() bool { return s <= SourceForced } | |||
| 62 | + | ||||
| 45 | // Application identifies a known terminal application when its environment | 63 | // Application identifies a known terminal application when its environment | ||
| 46 | // provides a trustworthy origin signal. | 64 | // provides a trustworthy origin signal. | ||
| 47 | type Application uint8 | 65 | type Application uint8 | ||
| @@ -111,12 +129,17 @@ func environmentValues(env []string) map[string]string { | |||||
| 111 | } | 129 | } | ||
| 112 | 130 | ||||
| 113 | // Resolve selects attachment capabilities from the client environment and the | 131 | // Resolve selects attachment capabilities from the client environment and the | ||
| 114 | - | // client's explicit color declaration. A declared truecolor client upgrades a | 132 | + | // client's color claim. A declared or forced client color is used verbatim. A |
| 115 | - | // weaker environment detection to declared TrueColor. | 133 | + | // heuristic truecolor claim upgrades a weaker daemon-side detection, because |
| 116 | - | func Resolve(env []string, declaredTrueColor bool) Capabilities { | 134 | + | // the daemon may not see the client environment that produced the inference. |
| 135 | + | // Any other claim falls back to environment detection. | |||
| 136 | + | func Resolve(env []string, declared ColorCapabilities) Capabilities { | |||
| 117 | caps := Detect(env) | 137 | caps := Detect(env) | ||
| 118 | - | if declaredTrueColor && !caps.Color.RGB() { | 138 | + | switch { |
| 119 | - | caps.Color = ColorCapabilities{Mode: TrueColor, Source: SourceDeclared} | 139 | + | case declared.Source == SourceDeclared || declared.Source == SourceForced: |
| 140 | + | caps.Color = declared | |||
| 141 | + | case declared.Source == SourceHeuristic && declared.RGB() && !caps.Color.RGB(): | |||
| 142 | + | caps.Color = declared | |||
| 120 | } | 143 | } | ||
| 121 | return caps | 144 | return caps | ||
| 122 | } | 145 | } | ||
M internal/domain/terminalcap/capabilities_test.go +33 -7
| @@ -16,6 +16,8 @@ func TestColorCapabilities(t *testing.T) { | |||
| 16 | 16 | {name: "zero value is truecolor", in: ColorCapabilities{}, wantRGB: true, wantColors: 16777216}, | |
| 17 | 17 | {name: "truecolor", in: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantRGB: true, wantColors: 16777216}, | |
| 18 | 18 | {name: "ansi256", in: ColorCapabilities{Mode: ANSI256}, wantRGB: false, wantColors: 256}, | |
| 19 | + | {name: "ansi16", in: ColorCapabilities{Mode: ANSI16, Source: SourceForced}, wantRGB: false, wantColors: 16}, | |
| 20 | + | {name: "monochrome", in: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, wantRGB: false, wantColors: 0}, | |
| 19 | 21 | } | |
| 20 | 22 | for _, tt := range tests { | |
| 21 | 23 | t.Run(tt.name, func(t *testing.T) { | |
| @@ -25,20 +27,44 @@ func TestColorCapabilities(t *testing.T) { | |||
| 25 | 27 | } | |
| 26 | 28 | } | |
| 27 | 29 | ||
| 30 | + | func TestColorCapabilitiesValid(t *testing.T) { | |
| 31 | + | tests := []struct { | |
| 32 | + | name string | |
| 33 | + | in ColorCapabilities | |
| 34 | + | want bool | |
| 35 | + | }{ | |
| 36 | + | {name: "zero value", in: ColorCapabilities{}, want: true}, | |
| 37 | + | {name: "every mode and source", in: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, want: true}, | |
| 38 | + | {name: "unknown mode", in: ColorCapabilities{Mode: Monochrome + 1}}, | |
| 39 | + | {name: "unknown source", in: ColorCapabilities{Source: SourceForced + 1}}, | |
| 40 | + | } | |
| 41 | + | for _, tt := range tests { | |
| 42 | + | t.Run(tt.name, func(t *testing.T) { | |
| 43 | + | require.Equal(t, tt.want, tt.in.Valid()) | |
| 44 | + | }) | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 28 | 48 | func TestResolve(t *testing.T) { | |
| 29 | 49 | tests := []struct { | |
| 30 | 50 | name string | |
| 31 | 51 | env []string | |
| 32 | - | declared bool | |
| 52 | + | declared ColorCapabilities | |
| 33 | 53 | want ColorCapabilities | |
| 34 | 54 | wantApp Application | |
| 35 | 55 | }{ | |
| 36 | - | {name: "256 color without declaration stays indexed", env: []string{"TERM=xterm-256color"}, want: ColorCapabilities{Mode: ANSI256, Source: SourceDeclared}}, | |
| 37 | - | {name: "declaration upgrades indexed terminal", env: []string{"TERM=xterm-256color"}, declared: true, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}}, | |
| 38 | - | {name: "declaration upgrades unknown terminal", env: []string{"TERM=unknown"}, declared: true, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}}, | |
| 39 | - | {name: "declaration keeps environment truecolor detection", env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, declared: true, want: ColorCapabilities{Mode: TrueColor, Source: SourceHeuristic}, wantApp: ApplicationKitty}, | |
| 40 | - | {name: "environment truecolor without declaration", env: []string{"TERM=xterm-256color", "COLORTERM=truecolor"}, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}}, | |
| 41 | - | {name: "declaration keeps application", env: []string{"TERM=tmux-256color", "KITTY_WINDOW_ID=1"}, declared: true, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantApp: ApplicationKitty}, | |
| 56 | + | {name: "unknown claim falls back to detection", env: []string{"TERM=xterm-256color"}, want: ColorCapabilities{Mode: ANSI256, Source: SourceDeclared}}, | |
| 57 | + | {name: "unknown truecolor claim falls back to detection", env: []string{"TERM=xterm-256color"}, declared: ColorCapabilities{Mode: TrueColor}, want: ColorCapabilities{Mode: ANSI256, Source: SourceDeclared}}, | |
| 58 | + | {name: "unknown claim with detected truecolor", env: []string{"TERM=xterm-256color", "COLORTERM=truecolor"}, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}}, | |
| 59 | + | {name: "declared truecolor overrides indexed detection", env: []string{"TERM=xterm-256color"}, declared: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}}, | |
| 60 | + | {name: "declared truecolor on unknown terminal", env: []string{"TERM=unknown"}, declared: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}}, | |
| 61 | + | {name: "declared 256 overrides detected truecolor verbatim", env: []string{"TERM=xterm-256color", "COLORTERM=truecolor"}, declared: ColorCapabilities{Mode: ANSI256, Source: SourceDeclared}, want: ColorCapabilities{Mode: ANSI256, Source: SourceDeclared}}, | |
| 62 | + | {name: "forced 16 colors", env: []string{"TERM=xterm-256color", "COLORTERM=truecolor"}, declared: ColorCapabilities{Mode: ANSI16, Source: SourceForced}, want: ColorCapabilities{Mode: ANSI16, Source: SourceForced}}, | |
| 63 | + | {name: "forced monochrome", env: nil, declared: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, want: ColorCapabilities{Mode: Monochrome, Source: SourceForced}}, | |
| 64 | + | {name: "heuristic truecolor upgrades weaker detection", env: []string{"TERM=xterm-256color"}, declared: ColorCapabilities{Mode: TrueColor, Source: SourceHeuristic}, want: ColorCapabilities{Mode: TrueColor, Source: SourceHeuristic}}, | |
| 65 | + | {name: "heuristic truecolor keeps stronger detection", env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, declared: ColorCapabilities{Mode: TrueColor, Source: SourceHeuristic}, want: ColorCapabilities{Mode: TrueColor, Source: SourceHeuristic}, wantApp: ApplicationKitty}, | |
| 66 | + | {name: "heuristic 16 colors falls back to detection", env: []string{"TERM=xterm-256color"}, declared: ColorCapabilities{Mode: ANSI16, Source: SourceHeuristic}, want: ColorCapabilities{Mode: ANSI256, Source: SourceDeclared}}, | |
| 67 | + | {name: "declared claim keeps detected application", env: []string{"TERM=tmux-256color", "KITTY_WINDOW_ID=1"}, declared: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantApp: ApplicationKitty}, | |
| 42 | 68 | } | |
| 43 | 69 | for _, tt := range tests { | |
| 44 | 70 | t.Run(tt.name, func(t *testing.T) { | |
| @@ -16,6 +16,8 @@ func TestColorCapabilities(t *testing.T) { | |||||
| 16 | {name: "zero value is truecolor", in: ColorCapabilities{}, wantRGB: true, wantColors: 16777216}, | 16 | {name: "zero value is truecolor", in: ColorCapabilities{}, wantRGB: true, wantColors: 16777216}, | ||
| 17 | {name: "truecolor", in: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantRGB: true, wantColors: 16777216}, | 17 | {name: "truecolor", in: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantRGB: true, wantColors: 16777216}, | ||
| 18 | {name: "ansi256", in: ColorCapabilities{Mode: ANSI256}, wantRGB: false, wantColors: 256}, | 18 | {name: "ansi256", in: ColorCapabilities{Mode: ANSI256}, wantRGB: false, wantColors: 256}, | ||
| 19 | + | {name: "ansi16", in: ColorCapabilities{Mode: ANSI16, Source: SourceForced}, wantRGB: false, wantColors: 16}, | |||
| 20 | + | {name: "monochrome", in: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, wantRGB: false, wantColors: 0}, | |||
| 19 | } | 21 | } | ||
| 20 | for _, tt := range tests { | 22 | for _, tt := range tests { | ||
| 21 | t.Run(tt.name, func(t *testing.T) { | 23 | t.Run(tt.name, func(t *testing.T) { | ||
| @@ -25,20 +27,44 @@ func TestColorCapabilities(t *testing.T) { | |||||
| 25 | } | 27 | } | ||
| 26 | } | 28 | } | ||
| 27 | 29 | ||||
| 30 | + | func TestColorCapabilitiesValid(t *testing.T) { | |||
| 31 | + | tests := []struct { | |||
| 32 | + | name string | |||
| 33 | + | in ColorCapabilities | |||
| 34 | + | want bool | |||
| 35 | + | }{ | |||
| 36 | + | {name: "zero value", in: ColorCapabilities{}, want: true}, | |||
| 37 | + | {name: "every mode and source", in: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, want: true}, | |||
| 38 | + | {name: "unknown mode", in: ColorCapabilities{Mode: Monochrome + 1}}, | |||
| 39 | + | {name: "unknown source", in: ColorCapabilities{Source: SourceForced + 1}}, | |||
| 40 | + | } | |||
| 41 | + | for _, tt := range tests { | |||
| 42 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 43 | + | require.Equal(t, tt.want, tt.in.Valid()) | |||
| 44 | + | }) | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | ||||
| 28 | func TestResolve(t *testing.T) { | 48 | func TestResolve(t *testing.T) { | ||
| 29 | tests := []struct { | 49 | tests := []struct { | ||
| 30 | name string | 50 | name string | ||
| 31 | env []string | 51 | env []string | ||
| 32 | - | declared bool | 52 | + | declared ColorCapabilities |
| 33 | want ColorCapabilities | 53 | want ColorCapabilities | ||
| 34 | wantApp Application | 54 | wantApp Application | ||
| 35 | }{ | 55 | }{ | ||
| 36 | - | {name: "256 color without declaration stays indexed", env: []string{"TERM=xterm-256color"}, want: ColorCapabilities{Mode: ANSI256, Source: SourceDeclared}}, | 56 | + | {name: "unknown claim falls back to detection", env: []string{"TERM=xterm-256color"}, want: ColorCapabilities{Mode: ANSI256, Source: SourceDeclared}}, |
| 37 | - | {name: "declaration upgrades indexed terminal", env: []string{"TERM=xterm-256color"}, declared: true, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}}, | 57 | + | {name: "unknown truecolor claim falls back to detection", env: []string{"TERM=xterm-256color"}, declared: ColorCapabilities{Mode: TrueColor}, want: ColorCapabilities{Mode: ANSI256, Source: SourceDeclared}}, |
| 38 | - | {name: "declaration upgrades unknown terminal", env: []string{"TERM=unknown"}, declared: true, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}}, | 58 | + | {name: "unknown claim with detected truecolor", env: []string{"TERM=xterm-256color", "COLORTERM=truecolor"}, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}}, |
| 39 | - | {name: "declaration keeps environment truecolor detection", env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, declared: true, want: ColorCapabilities{Mode: TrueColor, Source: SourceHeuristic}, wantApp: ApplicationKitty}, | 59 | + | {name: "declared truecolor overrides indexed detection", env: []string{"TERM=xterm-256color"}, declared: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}}, |
| 40 | - | {name: "environment truecolor without declaration", env: []string{"TERM=xterm-256color", "COLORTERM=truecolor"}, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}}, | 60 | + | {name: "declared truecolor on unknown terminal", env: []string{"TERM=unknown"}, declared: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}}, |
| 41 | - | {name: "declaration keeps application", env: []string{"TERM=tmux-256color", "KITTY_WINDOW_ID=1"}, declared: true, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantApp: ApplicationKitty}, | 61 | + | {name: "declared 256 overrides detected truecolor verbatim", env: []string{"TERM=xterm-256color", "COLORTERM=truecolor"}, declared: ColorCapabilities{Mode: ANSI256, Source: SourceDeclared}, want: ColorCapabilities{Mode: ANSI256, Source: SourceDeclared}}, |
| 62 | + | {name: "forced 16 colors", env: []string{"TERM=xterm-256color", "COLORTERM=truecolor"}, declared: ColorCapabilities{Mode: ANSI16, Source: SourceForced}, want: ColorCapabilities{Mode: ANSI16, Source: SourceForced}}, | |||
| 63 | + | {name: "forced monochrome", env: nil, declared: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, want: ColorCapabilities{Mode: Monochrome, Source: SourceForced}}, | |||
| 64 | + | {name: "heuristic truecolor upgrades weaker detection", env: []string{"TERM=xterm-256color"}, declared: ColorCapabilities{Mode: TrueColor, Source: SourceHeuristic}, want: ColorCapabilities{Mode: TrueColor, Source: SourceHeuristic}}, | |||
| 65 | + | {name: "heuristic truecolor keeps stronger detection", env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, declared: ColorCapabilities{Mode: TrueColor, Source: SourceHeuristic}, want: ColorCapabilities{Mode: TrueColor, Source: SourceHeuristic}, wantApp: ApplicationKitty}, | |||
| 66 | + | {name: "heuristic 16 colors falls back to detection", env: []string{"TERM=xterm-256color"}, declared: ColorCapabilities{Mode: ANSI16, Source: SourceHeuristic}, want: ColorCapabilities{Mode: ANSI256, Source: SourceDeclared}}, | |||
| 67 | + | {name: "declared claim keeps detected application", env: []string{"TERM=tmux-256color", "KITTY_WINDOW_ID=1"}, declared: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, want: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantApp: ApplicationKitty}, | |||
| 42 | } | 68 | } | ||
| 43 | for _, tt := range tests { | 69 | for _, tt := range tests { | ||
| 44 | t.Run(tt.name, func(t *testing.T) { | 70 | t.Run(tt.name, func(t *testing.T) { | ||
M internal/protocol/session.go +6 -4
| @@ -10,13 +10,14 @@ import ( | |||
| 10 | 10 | ||
| 11 | 11 | renderer "github.com/bnema/vev-vt" | |
| 12 | 12 | "github.com/bnema/vev/internal/domain" | |
| 13 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 13 | 14 | ) | |
| 14 | 15 | ||
| 15 | 16 | // Version is the negotiated vev session protocol version. It carries the | |
| 16 | 17 | // daemonmux physical preamble conversation (multiplex.proto), whose | |
| 17 | 18 | // negotiated ceilings and accepted daemon binding ride the same wire | |
| 18 | 19 | // version as the session and broker conversations. | |
| 19 | - | const Version uint16 = 64 | |
| 20 | + | const Version uint16 = 65 | |
| 20 | 21 | ||
| 21 | 22 | // HandshakeTimeout bounds every transport handshake from connect through the | |
| 22 | 23 | // first committed publication. It excludes the preceding client-local | |
| @@ -110,7 +111,9 @@ type Hello struct { | |||
| 110 | 111 | PixelHeight int | |
| 111 | 112 | TermEnv string | |
| 112 | 113 | Cwd string | |
| 113 | - | TrueColor bool | |
| 114 | + | // Color is the color output capability the client claims for this | |
| 115 | + | // attachment. It travels only in Hello; Theme carries no capability. | |
| 116 | + | Color terminalcap.ColorCapabilities | |
| 114 | 117 | // KittyDirectGraphics is an explicit declaration that the client has | |
| 115 | 118 | // probed its direct outer terminal and it accepts Kitty graphics output. | |
| 116 | 119 | KittyDirectGraphics bool | |
| @@ -158,7 +161,6 @@ type Theme struct { | |||
| 158 | 161 | Foreground renderer.RGB | |
| 159 | 162 | HasBackground bool | |
| 160 | 163 | Background renderer.RGB | |
| 161 | - | TrueColor bool | |
| 162 | 164 | SchemeKnown bool | |
| 163 | 165 | Light bool | |
| 164 | 166 | PaletteKnown uint16 | |
| @@ -436,7 +438,7 @@ func ValidateHello(h Hello) error { | |||
| 436 | 438 | if err := ValidateGeometry(domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}); err != nil { | |
| 437 | 439 | return fmt.Errorf("%w: geometry", ErrInvalidHello) | |
| 438 | 440 | } | |
| 439 | - | if !validEnvironmentPolicy(h.EnvironmentPolicy) || !h.TerminalFocus.Valid() || (h.Remote && h.ClientPID != 0) { | |
| 441 | + | if !validEnvironmentPolicy(h.EnvironmentPolicy) || !h.TerminalFocus.Valid() || !h.Color.Valid() || (h.Remote && h.ClientPID != 0) { | |
| 440 | 442 | return ErrInvalidHello | |
| 441 | 443 | } | |
| 442 | 444 | if err := ValidateNavigation(h.Intent, h.NavigationCapabilities); err != nil { | |
| @@ -10,13 +10,14 @@ import ( | |||||
| 10 | 10 | ||||
| 11 | renderer "github.com/bnema/vev-vt" | 11 | renderer "github.com/bnema/vev-vt" | ||
| 12 | "github.com/bnema/vev/internal/domain" | 12 | "github.com/bnema/vev/internal/domain" | ||
| 13 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |||
| 13 | ) | 14 | ) | ||
| 14 | 15 | ||||
| 15 | // Version is the negotiated vev session protocol version. It carries the | 16 | // Version is the negotiated vev session protocol version. It carries the | ||
| 16 | // daemonmux physical preamble conversation (multiplex.proto), whose | 17 | // daemonmux physical preamble conversation (multiplex.proto), whose | ||
| 17 | // negotiated ceilings and accepted daemon binding ride the same wire | 18 | // negotiated ceilings and accepted daemon binding ride the same wire | ||
| 18 | // version as the session and broker conversations. | 19 | // version as the session and broker conversations. | ||
| 19 | - | const Version uint16 = 64 | 20 | + | const Version uint16 = 65 |
| 20 | 21 | ||||
| 21 | // HandshakeTimeout bounds every transport handshake from connect through the | 22 | // HandshakeTimeout bounds every transport handshake from connect through the | ||
| 22 | // first committed publication. It excludes the preceding client-local | 23 | // first committed publication. It excludes the preceding client-local | ||
| @@ -110,7 +111,9 @@ type Hello struct { | |||||
| 110 | PixelHeight int | 111 | PixelHeight int | ||
| 111 | TermEnv string | 112 | TermEnv string | ||
| 112 | Cwd string | 113 | Cwd string | ||
| 113 | - | TrueColor bool | 114 | + | // Color is the color output capability the client claims for this |
| 115 | + | // attachment. It travels only in Hello; Theme carries no capability. | |||
| 116 | + | Color terminalcap.ColorCapabilities | |||
| 114 | // KittyDirectGraphics is an explicit declaration that the client has | 117 | // KittyDirectGraphics is an explicit declaration that the client has | ||
| 115 | // probed its direct outer terminal and it accepts Kitty graphics output. | 118 | // probed its direct outer terminal and it accepts Kitty graphics output. | ||
| 116 | KittyDirectGraphics bool | 119 | KittyDirectGraphics bool | ||
| @@ -158,7 +161,6 @@ type Theme struct { | |||||
| 158 | Foreground renderer.RGB | 161 | Foreground renderer.RGB | ||
| 159 | HasBackground bool | 162 | HasBackground bool | ||
| 160 | Background renderer.RGB | 163 | Background renderer.RGB | ||
| 161 | - | TrueColor bool | |||
| 162 | SchemeKnown bool | 164 | SchemeKnown bool | ||
| 163 | Light bool | 165 | Light bool | ||
| 164 | PaletteKnown uint16 | 166 | PaletteKnown uint16 | ||
| @@ -436,7 +438,7 @@ func ValidateHello(h Hello) error { | |||||
| 436 | if err := ValidateGeometry(domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}); err != nil { | 438 | if err := ValidateGeometry(domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}); err != nil { | ||
| 437 | return fmt.Errorf("%w: geometry", ErrInvalidHello) | 439 | return fmt.Errorf("%w: geometry", ErrInvalidHello) | ||
| 438 | } | 440 | } | ||
| 439 | - | if !validEnvironmentPolicy(h.EnvironmentPolicy) || !h.TerminalFocus.Valid() || (h.Remote && h.ClientPID != 0) { | 441 | + | if !validEnvironmentPolicy(h.EnvironmentPolicy) || !h.TerminalFocus.Valid() || !h.Color.Valid() || (h.Remote && h.ClientPID != 0) { |
| 440 | return ErrInvalidHello | 442 | return ErrInvalidHello | ||
| 441 | } | 443 | } | ||
| 442 | if err := ValidateNavigation(h.Intent, h.NavigationCapabilities); err != nil { | 444 | if err := ValidateNavigation(h.Intent, h.NavigationCapabilities); err != nil { | ||
M internal/protocol/session_test.go +1 -1
| @@ -10,7 +10,7 @@ import ( | |||
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | 12 | func TestVersionRemainsWireCompatible(t *testing.T) { | |
| 13 | - | require.Equal(t, uint16(64), Version) | |
| 13 | + | require.Equal(t, uint16(65), Version) | |
| 14 | 14 | require.Equal(t, (16<<20)-407, MaxOutputDataLen) | |
| 15 | 15 | } | |
| 16 | 16 | ||
| @@ -10,7 +10,7 @@ import ( | |||||
| 10 | ) | 10 | ) | ||
| 11 | 11 | ||||
| 12 | func TestVersionRemainsWireCompatible(t *testing.T) { | 12 | func TestVersionRemainsWireCompatible(t *testing.T) { | ||
| 13 | - | require.Equal(t, uint16(64), Version) | 13 | + | require.Equal(t, uint16(65), Version) |
| 14 | require.Equal(t, (16<<20)-407, MaxOutputDataLen) | 14 | require.Equal(t, (16<<20)-407, MaxOutputDataLen) | ||
| 15 | } | 15 | } | ||
| 16 | 16 | ||||
M internal/protocol/wire/baseline_bench_test.go +3 -2
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "google.golang.org/protobuf/proto" | |
| 8 | 8 | ||
| 9 | 9 | "github.com/bnema/vev/internal/domain" | |
| 10 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 10 | 11 | "github.com/bnema/vev/internal/protocol" | |
| 11 | 12 | ) | |
| 12 | 13 | ||
| @@ -24,7 +25,7 @@ func baselineHello() protocol.Hello { | |||
| 24 | 25 | Size: domain.Size{Cols: 120, Rows: 40}, | |
| 25 | 26 | TermEnv: "xterm-256color", | |
| 26 | 27 | Cwd: "/tmp/project", | |
| 27 | - | TrueColor: true, | |
| 28 | + | Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256, Source: terminalcap.SourceDeclared}, | |
| 28 | 29 | MaxOutputInFlight: 8, | |
| 29 | 30 | } | |
| 30 | 31 | } | |
| @@ -48,7 +49,7 @@ func BenchmarkHelloEnvelope(b *testing.B) { | |||
| 48 | 49 | Rows: uint32(hello.Size.Rows), | |
| 49 | 50 | TermEnv: hello.TermEnv, | |
| 50 | 51 | Cwd: hello.Cwd, | |
| 51 | - | TrueColor: hello.TrueColor, | |
| 52 | + | Color: &ColorCapabilities{Mode: ColorMode_COLOR_MODE_ANSI256, Source: ColorSource_COLOR_SOURCE_DECLARED}, | |
| 52 | 53 | MaxOutputInFlight: uint32(hello.MaxOutputInFlight), | |
| 53 | 54 | } | |
| 54 | 55 | b.ReportAllocs() | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "google.golang.org/protobuf/proto" | 7 | "google.golang.org/protobuf/proto" | ||
| 8 | 8 | ||||
| 9 | "github.com/bnema/vev/internal/domain" | 9 | "github.com/bnema/vev/internal/domain" | ||
| 10 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |||
| 10 | "github.com/bnema/vev/internal/protocol" | 11 | "github.com/bnema/vev/internal/protocol" | ||
| 11 | ) | 12 | ) | ||
| 12 | 13 | ||||
| @@ -24,7 +25,7 @@ func baselineHello() protocol.Hello { | |||||
| 24 | Size: domain.Size{Cols: 120, Rows: 40}, | 25 | Size: domain.Size{Cols: 120, Rows: 40}, | ||
| 25 | TermEnv: "xterm-256color", | 26 | TermEnv: "xterm-256color", | ||
| 26 | Cwd: "/tmp/project", | 27 | Cwd: "/tmp/project", | ||
| 27 | - | TrueColor: true, | 28 | + | Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256, Source: terminalcap.SourceDeclared}, |
| 28 | MaxOutputInFlight: 8, | 29 | MaxOutputInFlight: 8, | ||
| 29 | } | 30 | } | ||
| 30 | } | 31 | } | ||
| @@ -48,7 +49,7 @@ func BenchmarkHelloEnvelope(b *testing.B) { | |||||
| 48 | Rows: uint32(hello.Size.Rows), | 49 | Rows: uint32(hello.Size.Rows), | ||
| 49 | TermEnv: hello.TermEnv, | 50 | TermEnv: hello.TermEnv, | ||
| 50 | Cwd: hello.Cwd, | 51 | Cwd: hello.Cwd, | ||
| 51 | - | TrueColor: hello.TrueColor, | 52 | + | Color: &ColorCapabilities{Mode: ColorMode_COLOR_MODE_ANSI256, Source: ColorSource_COLOR_SOURCE_DECLARED}, |
| 52 | MaxOutputInFlight: uint32(hello.MaxOutputInFlight), | 53 | MaxOutputInFlight: uint32(hello.MaxOutputInFlight), | ||
| 53 | } | 54 | } | ||
| 54 | b.ReportAllocs() | 55 | b.ReportAllocs() | ||
M internal/protocol/wire/common.pb.go +222 -36 generated file: collapsed
M internal/protocol/wire/envelope_inventory_test.go +2 -2
| @@ -15,8 +15,8 @@ import ( | |||
| 15 | 15 | // shares a payload type except route navigation failure, and the preamble | |
| 16 | 16 | // constants agree with the schema authority. | |
| 17 | 17 | func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) { | |
| 18 | - | if protocol.Version != 64 { | |
| 19 | - | t.Fatalf("protocol.Version = %d, want 64", protocol.Version) | |
| 18 | + | if protocol.Version != 65 { | |
| 19 | + | t.Fatalf("protocol.Version = %d, want 65", protocol.Version) | |
| 20 | 20 | } | |
| 21 | 21 | if ProtocolEpoch != 1 { | |
| 22 | 22 | t.Fatalf("ProtocolEpoch = %d, want 1", ProtocolEpoch) | |
| @@ -15,8 +15,8 @@ import ( | |||||
| 15 | // shares a payload type except route navigation failure, and the preamble | 15 | // shares a payload type except route navigation failure, and the preamble | ||
| 16 | // constants agree with the schema authority. | 16 | // constants agree with the schema authority. | ||
| 17 | func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) { | 17 | func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) { | ||
| 18 | - | if protocol.Version != 64 { | 18 | + | if protocol.Version != 65 { |
| 19 | - | t.Fatalf("protocol.Version = %d, want 64", protocol.Version) | 19 | + | t.Fatalf("protocol.Version = %d, want 65", protocol.Version) |
| 20 | } | 20 | } | ||
| 21 | if ProtocolEpoch != 1 { | 21 | if ProtocolEpoch != 1 { | ||
| 22 | t.Fatalf("ProtocolEpoch = %d, want 1", ProtocolEpoch) | 22 | t.Fatalf("ProtocolEpoch = %d, want 1", ProtocolEpoch) | ||
M internal/protocol/wire/schema/common.proto +26 -0
| @@ -65,6 +65,32 @@ message RGB { | |||
| 65 | 65 | uint32 b = 3; | |
| 66 | 66 | } | |
| 67 | 67 | ||
| 68 | + | // ColorMode is the closed color output mode of one attachment | |
| 69 | + | // (terminalcap.ColorMode). UNSPECIFIED is never a valid value on the wire. | |
| 70 | + | enum ColorMode { | |
| 71 | + | COLOR_MODE_UNSPECIFIED = 0; | |
| 72 | + | COLOR_MODE_TRUE_COLOR = 1; | |
| 73 | + | COLOR_MODE_ANSI256 = 2; | |
| 74 | + | COLOR_MODE_ANSI16 = 3; | |
| 75 | + | COLOR_MODE_MONOCHROME = 4; | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | // ColorSource records how confidently a color mode was selected | |
| 79 | + | // (terminalcap.Source). UNSPECIFIED is the explicit "unknown" source. | |
| 80 | + | enum ColorSource { | |
| 81 | + | COLOR_SOURCE_UNSPECIFIED = 0; | |
| 82 | + | COLOR_SOURCE_HEURISTIC = 1; | |
| 83 | + | COLOR_SOURCE_DECLARED = 2; | |
| 84 | + | COLOR_SOURCE_FORCED = 3; | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // ColorCapabilities is the color output capability a client claims for one | |
| 88 | + | // attachment (terminalcap.ColorCapabilities). | |
| 89 | + | message ColorCapabilities { | |
| 90 | + | ColorMode mode = 1; | |
| 91 | + | ColorSource source = 2; | |
| 92 | + | } | |
| 93 | + | ||
| 68 | 94 | // CellStyle is the bounded style block of one preview cell | |
| 69 | 95 | // (renderer.Style). Indexed colors use int32 so -1 (unset) survives. | |
| 70 | 96 | message CellStyle { | |
| @@ -65,6 +65,32 @@ message RGB { | |||||
| 65 | uint32 b = 3; | 65 | uint32 b = 3; | ||
| 66 | } | 66 | } | ||
| 67 | 67 | ||||
| 68 | + | // ColorMode is the closed color output mode of one attachment | |||
| 69 | + | // (terminalcap.ColorMode). UNSPECIFIED is never a valid value on the wire. | |||
| 70 | + | enum ColorMode { | |||
| 71 | + | COLOR_MODE_UNSPECIFIED = 0; | |||
| 72 | + | COLOR_MODE_TRUE_COLOR = 1; | |||
| 73 | + | COLOR_MODE_ANSI256 = 2; | |||
| 74 | + | COLOR_MODE_ANSI16 = 3; | |||
| 75 | + | COLOR_MODE_MONOCHROME = 4; | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | // ColorSource records how confidently a color mode was selected | |||
| 79 | + | // (terminalcap.Source). UNSPECIFIED is the explicit "unknown" source. | |||
| 80 | + | enum ColorSource { | |||
| 81 | + | COLOR_SOURCE_UNSPECIFIED = 0; | |||
| 82 | + | COLOR_SOURCE_HEURISTIC = 1; | |||
| 83 | + | COLOR_SOURCE_DECLARED = 2; | |||
| 84 | + | COLOR_SOURCE_FORCED = 3; | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // ColorCapabilities is the color output capability a client claims for one | |||
| 88 | + | // attachment (terminalcap.ColorCapabilities). | |||
| 89 | + | message ColorCapabilities { | |||
| 90 | + | ColorMode mode = 1; | |||
| 91 | + | ColorSource source = 2; | |||
| 92 | + | } | |||
| 93 | + | ||||
| 68 | // CellStyle is the bounded style block of one preview cell | 94 | // CellStyle is the bounded style block of one preview cell | ||
| 69 | // (renderer.Style). Indexed colors use int32 so -1 (unset) survives. | 95 | // (renderer.Style). Indexed colors use int32 so -1 (unset) survives. | ||
| 70 | message CellStyle { | 96 | message CellStyle { | ||
M internal/protocol/wire/schema/session.proto +5 -1
| @@ -21,7 +21,8 @@ message Hello { | |||
| 21 | 21 | uint32 pixel_height = 9; | |
| 22 | 22 | string term_env = 10; | |
| 23 | 23 | string cwd = 11; | |
| 24 | - | bool true_color = 12; | |
| 24 | + | reserved 12; | |
| 25 | + | reserved "true_color"; | |
| 25 | 26 | uint32 max_output_in_flight = 13; | |
| 26 | 27 | repeated string env = 14; | |
| 27 | 28 | reserved 15; | |
| @@ -39,6 +40,9 @@ message Hello { | |||
| 39 | 40 | uint32 terminal_focus = 24; | |
| 40 | 41 | // PID of a local client process, for bar scripts. 0 when unknown or remote. | |
| 41 | 42 | uint32 client_pid = 25; | |
| 43 | + | // Color output capability the client claims. Required: a missing message | |
| 44 | + | // or an UNSPECIFIED mode is a decode error. | |
| 45 | + | ColorCapabilities color = 26; | |
| 42 | 46 | } | |
| 43 | 47 | ||
| 44 | 48 | // Welcome answers an accepted attach. | |
| @@ -21,7 +21,8 @@ message Hello { | |||||
| 21 | uint32 pixel_height = 9; | 21 | uint32 pixel_height = 9; | ||
| 22 | string term_env = 10; | 22 | string term_env = 10; | ||
| 23 | string cwd = 11; | 23 | string cwd = 11; | ||
| 24 | - | bool true_color = 12; | 24 | + | reserved 12; |
| 25 | + | reserved "true_color"; | |||
| 25 | uint32 max_output_in_flight = 13; | 26 | uint32 max_output_in_flight = 13; | ||
| 26 | repeated string env = 14; | 27 | repeated string env = 14; | ||
| 27 | reserved 15; | 28 | reserved 15; | ||
| @@ -39,6 +40,9 @@ message Hello { | |||||
| 39 | uint32 terminal_focus = 24; | 40 | uint32 terminal_focus = 24; | ||
| 40 | // PID of a local client process, for bar scripts. 0 when unknown or remote. | 41 | // PID of a local client process, for bar scripts. 0 when unknown or remote. | ||
| 41 | uint32 client_pid = 25; | 42 | uint32 client_pid = 25; | ||
| 43 | + | // Color output capability the client claims. Required: a missing message | |||
| 44 | + | // or an UNSPECIFIED mode is a decode error. | |||
| 45 | + | ColorCapabilities color = 26; | |||
| 42 | } | 46 | } | ||
| 43 | 47 | ||||
| 44 | // Welcome answers an accepted attach. | 48 | // Welcome answers an accepted attach. | ||
M internal/protocol/wire/schema/terminal.proto +2 -1
| @@ -62,7 +62,8 @@ message Theme { | |||
| 62 | 62 | RGB foreground = 2; | |
| 63 | 63 | bool has_background = 3; | |
| 64 | 64 | RGB background = 4; | |
| 65 | - | bool true_color = 5; | |
| 65 | + | reserved 5; | |
| 66 | + | reserved "true_color"; | |
| 66 | 67 | bool scheme_known = 6; | |
| 67 | 68 | bool light = 7; | |
| 68 | 69 | uint32 palette_known = 8; | |
| @@ -62,7 +62,8 @@ message Theme { | |||||
| 62 | RGB foreground = 2; | 62 | RGB foreground = 2; | ||
| 63 | bool has_background = 3; | 63 | bool has_background = 3; | ||
| 64 | RGB background = 4; | 64 | RGB background = 4; | ||
| 65 | - | bool true_color = 5; | 65 | + | reserved 5; |
| 66 | + | reserved "true_color"; | |||
| 66 | bool scheme_known = 6; | 67 | bool scheme_known = 6; | ||
| 67 | bool light = 7; | 68 | bool light = 7; | ||
| 68 | uint32 palette_known = 8; | 69 | uint32 palette_known = 8; | ||
M internal/protocol/wire/session.pb.go +46 -42
| @@ -38,7 +38,6 @@ type Hello struct { | |||
| 38 | 38 | PixelHeight uint32 `protobuf:"varint,9,opt,name=pixel_height,json=pixelHeight,proto3" json:"pixel_height,omitempty"` | |
| 39 | 39 | TermEnv string `protobuf:"bytes,10,opt,name=term_env,json=termEnv,proto3" json:"term_env,omitempty"` | |
| 40 | 40 | Cwd string `protobuf:"bytes,11,opt,name=cwd,proto3" json:"cwd,omitempty"` | |
| 41 | - | TrueColor bool `protobuf:"varint,12,opt,name=true_color,json=trueColor,proto3" json:"true_color,omitempty"` | |
| 42 | 41 | MaxOutputInFlight uint32 `protobuf:"varint,13,opt,name=max_output_in_flight,json=maxOutputInFlight,proto3" json:"max_output_in_flight,omitempty"` | |
| 43 | 42 | Env []string `protobuf:"bytes,14,rep,name=env,proto3" json:"env,omitempty"` | |
| 44 | 43 | EnvironmentPolicy uint32 `protobuf:"varint,16,opt,name=environment_policy,json=environmentPolicy,proto3" json:"environment_policy,omitempty"` // 0 = client-owned, 1 = daemon-owned | |
| @@ -53,7 +52,10 @@ type Hello struct { | |||
| 53 | 52 | // 1 = focused, 2 = unfocused. Later changes travel as TerminalFocus. | |
| 54 | 53 | TerminalFocus uint32 `protobuf:"varint,24,opt,name=terminal_focus,json=terminalFocus,proto3" json:"terminal_focus,omitempty"` | |
| 55 | 54 | // PID of a local client process, for bar scripts. 0 when unknown or remote. | |
| 56 | - | ClientPid uint32 `protobuf:"varint,25,opt,name=client_pid,json=clientPid,proto3" json:"client_pid,omitempty"` | |
| 55 | + | ClientPid uint32 `protobuf:"varint,25,opt,name=client_pid,json=clientPid,proto3" json:"client_pid,omitempty"` | |
| 56 | + | // Color output capability the client claims. Required: a missing message | |
| 57 | + | // or an UNSPECIFIED mode is a decode error. | |
| 58 | + | Color *ColorCapabilities `protobuf:"bytes,26,opt,name=color,proto3" json:"color,omitempty"` | |
| 57 | 59 | unknownFields protoimpl.UnknownFields | |
| 58 | 60 | sizeCache protoimpl.SizeCache | |
| 59 | 61 | } | |
| @@ -165,13 +167,6 @@ func (x *Hello) GetCwd() string { | |||
| 165 | 167 | return "" | |
| 166 | 168 | } | |
| 167 | 169 | ||
| 168 | - | func (x *Hello) GetTrueColor() bool { | |
| 169 | - | if x != nil { | |
| 170 | - | return x.TrueColor | |
| 171 | - | } | |
| 172 | - | return false | |
| 173 | - | } | |
| 174 | - | ||
| 175 | 170 | func (x *Hello) GetMaxOutputInFlight() uint32 { | |
| 176 | 171 | if x != nil { | |
| 177 | 172 | return x.MaxOutputInFlight | |
| @@ -256,6 +251,13 @@ func (x *Hello) GetClientPid() uint32 { | |||
| 256 | 251 | return 0 | |
| 257 | 252 | } | |
| 258 | 253 | ||
| 254 | + | func (x *Hello) GetColor() *ColorCapabilities { | |
| 255 | + | if x != nil { | |
| 256 | + | return x.Color | |
| 257 | + | } | |
| 258 | + | return nil | |
| 259 | + | } | |
| 260 | + | ||
| 259 | 261 | // Welcome answers an accepted attach. | |
| 260 | 262 | type Welcome struct { | |
| 261 | 263 | state protoimpl.MessageState `protogen:"open.v1"` | |
| @@ -2306,7 +2308,7 @@ var File_session_proto protoreflect.FileDescriptor | |||
| 2306 | 2308 | ||
| 2307 | 2309 | const file_session_proto_rawDesc = "" + | |
| 2308 | 2310 | "\n" + | |
| 2309 | - | "\rsession.proto\x12\vvev.wire.v1\x1a\fcommon.proto\"\xee\x06\n" + | |
| 2311 | + | "\rsession.proto\x12\vvev.wire.v1\x1a\fcommon.proto\"\x97\a\n" + | |
| 2310 | 2312 | "\x05Hello\x12\x18\n" + | |
| 2311 | 2313 | "\aversion\x18\x01 \x01(\rR\aversion\x12\x16\n" + | |
| 2312 | 2314 | "\x06intent\x18\x02 \x01(\rR\x06intent\x12\x1b\n" + | |
| @@ -2320,9 +2322,7 @@ const file_session_proto_rawDesc = "" + | |||
| 2320 | 2322 | "\fpixel_height\x18\t \x01(\rR\vpixelHeight\x12\x19\n" + | |
| 2321 | 2323 | "\bterm_env\x18\n" + | |
| 2322 | 2324 | " \x01(\tR\atermEnv\x12\x10\n" + | |
| 2323 | - | "\x03cwd\x18\v \x01(\tR\x03cwd\x12\x1d\n" + | |
| 2324 | - | "\n" + | |
| 2325 | - | "true_color\x18\f \x01(\bR\ttrueColor\x12/\n" + | |
| 2325 | + | "\x03cwd\x18\v \x01(\tR\x03cwd\x12/\n" + | |
| 2326 | 2326 | "\x14max_output_in_flight\x18\r \x01(\rR\x11maxOutputInFlight\x12\x10\n" + | |
| 2327 | 2327 | "\x03env\x18\x0e \x03(\tR\x03env\x12-\n" + | |
| 2328 | 2328 | "\x12environment_policy\x18\x10 \x01(\rR\x11environmentPolicy\x12;\n" + | |
| @@ -2335,7 +2335,9 @@ const file_session_proto_rawDesc = "" + | |||
| 2335 | 2335 | "\x0ekitty_keyboard\x18\x17 \x01(\bR\rkittyKeyboard\x12%\n" + | |
| 2336 | 2336 | "\x0eterminal_focus\x18\x18 \x01(\rR\rterminalFocus\x12\x1d\n" + | |
| 2337 | 2337 | "\n" + | |
| 2338 | - | "client_pid\x18\x19 \x01(\rR\tclientPidJ\x04\b\x0f\x10\x10R\rremote_target\"\x84\x02\n" + | |
| 2338 | + | "client_pid\x18\x19 \x01(\rR\tclientPid\x124\n" + | |
| 2339 | + | "\x05color\x18\x1a \x01(\v2\x1e.vev.wire.v1.ColorCapabilitiesR\x05colorJ\x04\b\f\x10\rJ\x04\b\x0f\x10\x10R\n" + | |
| 2340 | + | "true_colorR\rremote_target\"\x84\x02\n" + | |
| 2339 | 2341 | "\aWelcome\x12\x1d\n" + | |
| 2340 | 2342 | "\n" + | |
| 2341 | 2343 | "session_id\x18\x01 \x01(\tR\tsessionId\x12!\n" + | |
| @@ -2557,38 +2559,40 @@ var file_session_proto_goTypes = []any{ | |||
| 2557 | 2559 | (*AttachmentActivated)(nil), // 32: vev.wire.v1.AttachmentActivated | |
| 2558 | 2560 | (*ExactTarget)(nil), // 33: vev.wire.v1.ExactTarget | |
| 2559 | 2561 | (*SessionAttachTarget)(nil), // 34: vev.wire.v1.SessionAttachTarget | |
| 2562 | + | (*ColorCapabilities)(nil), // 35: vev.wire.v1.ColorCapabilities | |
| 2560 | 2563 | } | |
| 2561 | 2564 | var file_session_proto_depIdxs = []int32{ | |
| 2562 | 2565 | 33, // 0: vev.wire.v1.Hello.exact_target:type_name -> vev.wire.v1.ExactTarget | |
| 2563 | 2566 | 34, // 1: vev.wire.v1.Hello.session_target:type_name -> vev.wire.v1.SessionAttachTarget | |
| 2564 | - | 3, // 2: vev.wire.v1.Welcome.committed_identity:type_name -> vev.wire.v1.CommittedRouteIdentity | |
| 2565 | - | 33, // 3: vev.wire.v1.CommittedRouteIdentity.target:type_name -> vev.wire.v1.ExactTarget | |
| 2566 | - | 33, // 4: vev.wire.v1.AttachTarget.exact_target:type_name -> vev.wire.v1.ExactTarget | |
| 2567 | - | 34, // 5: vev.wire.v1.AttachTarget.session_target:type_name -> vev.wire.v1.SessionAttachTarget | |
| 2568 | - | 5, // 6: vev.wire.v1.Sessions.sessions:type_name -> vev.wire.v1.SessionInfo | |
| 2569 | - | 8, // 7: vev.wire.v1.KillResult.failures:type_name -> vev.wire.v1.KillFailure | |
| 2570 | - | 15, // 8: vev.wire.v1.RouteAttentionTarget.ref:type_name -> vev.wire.v1.RouteRef | |
| 2571 | - | 33, // 9: vev.wire.v1.RouteAttentionTarget.target:type_name -> vev.wire.v1.ExactTarget | |
| 2572 | - | 16, // 10: vev.wire.v1.RouteAttentionSubscription.targets:type_name -> vev.wire.v1.RouteAttentionTarget | |
| 2573 | - | 33, // 11: vev.wire.v1.RecentRouteEntry.target:type_name -> vev.wire.v1.ExactTarget | |
| 2574 | - | 15, // 12: vev.wire.v1.RecentRouteSnapshot.active:type_name -> vev.wire.v1.RouteRef | |
| 2575 | - | 18, // 13: vev.wire.v1.RecentRouteSnapshot.active_entry:type_name -> vev.wire.v1.RecentRouteEntry | |
| 2576 | - | 15, // 14: vev.wire.v1.RecentRouteSnapshot.previous:type_name -> vev.wire.v1.RouteRef | |
| 2577 | - | 15, // 15: vev.wire.v1.RecentRouteSnapshot.home:type_name -> vev.wire.v1.RouteRef | |
| 2578 | - | 18, // 16: vev.wire.v1.RecentRouteSnapshot.entries:type_name -> vev.wire.v1.RecentRouteEntry | |
| 2579 | - | 19, // 17: vev.wire.v1.RecentRouteSnapshot.hosts:type_name -> vev.wire.v1.RouteHost | |
| 2580 | - | 33, // 18: vev.wire.v1.RoutePosition.target:type_name -> vev.wire.v1.ExactTarget | |
| 2581 | - | 15, // 19: vev.wire.v1.RouteRetired.ref:type_name -> vev.wire.v1.RouteRef | |
| 2582 | - | 33, // 20: vev.wire.v1.RouteRetired.target:type_name -> vev.wire.v1.ExactTarget | |
| 2583 | - | 33, // 21: vev.wire.v1.SamePeerSwitchRequest.target:type_name -> vev.wire.v1.ExactTarget | |
| 2584 | - | 33, // 22: vev.wire.v1.AttachmentSuspended.target:type_name -> vev.wire.v1.ExactTarget | |
| 2585 | - | 33, // 23: vev.wire.v1.ActivateAttachment.target:type_name -> vev.wire.v1.ExactTarget | |
| 2586 | - | 3, // 24: vev.wire.v1.AttachmentActivated.identity:type_name -> vev.wire.v1.CommittedRouteIdentity | |
| 2587 | - | 25, // [25:25] is the sub-list for method output_type | |
| 2588 | - | 25, // [25:25] is the sub-list for method input_type | |
| 2589 | - | 25, // [25:25] is the sub-list for extension type_name | |
| 2590 | - | 25, // [25:25] is the sub-list for extension extendee | |
| 2591 | - | 0, // [0:25] is the sub-list for field type_name | |
| 2567 | + | 35, // 2: vev.wire.v1.Hello.color:type_name -> vev.wire.v1.ColorCapabilities | |
| 2568 | + | 3, // 3: vev.wire.v1.Welcome.committed_identity:type_name -> vev.wire.v1.CommittedRouteIdentity | |
| 2569 | + | 33, // 4: vev.wire.v1.CommittedRouteIdentity.target:type_name -> vev.wire.v1.ExactTarget | |
| 2570 | + | 33, // 5: vev.wire.v1.AttachTarget.exact_target:type_name -> vev.wire.v1.ExactTarget | |
| 2571 | + | 34, // 6: vev.wire.v1.AttachTarget.session_target:type_name -> vev.wire.v1.SessionAttachTarget | |
| 2572 | + | 5, // 7: vev.wire.v1.Sessions.sessions:type_name -> vev.wire.v1.SessionInfo | |
| 2573 | + | 8, // 8: vev.wire.v1.KillResult.failures:type_name -> vev.wire.v1.KillFailure | |
| 2574 | + | 15, // 9: vev.wire.v1.RouteAttentionTarget.ref:type_name -> vev.wire.v1.RouteRef | |
| 2575 | + | 33, // 10: vev.wire.v1.RouteAttentionTarget.target:type_name -> vev.wire.v1.ExactTarget | |
| 2576 | + | 16, // 11: vev.wire.v1.RouteAttentionSubscription.targets:type_name -> vev.wire.v1.RouteAttentionTarget | |
| 2577 | + | 33, // 12: vev.wire.v1.RecentRouteEntry.target:type_name -> vev.wire.v1.ExactTarget | |
| 2578 | + | 15, // 13: vev.wire.v1.RecentRouteSnapshot.active:type_name -> vev.wire.v1.RouteRef | |
| 2579 | + | 18, // 14: vev.wire.v1.RecentRouteSnapshot.active_entry:type_name -> vev.wire.v1.RecentRouteEntry | |
| 2580 | + | 15, // 15: vev.wire.v1.RecentRouteSnapshot.previous:type_name -> vev.wire.v1.RouteRef | |
| 2581 | + | 15, // 16: vev.wire.v1.RecentRouteSnapshot.home:type_name -> vev.wire.v1.RouteRef | |
| 2582 | + | 18, // 17: vev.wire.v1.RecentRouteSnapshot.entries:type_name -> vev.wire.v1.RecentRouteEntry | |
| 2583 | + | 19, // 18: vev.wire.v1.RecentRouteSnapshot.hosts:type_name -> vev.wire.v1.RouteHost | |
| 2584 | + | 33, // 19: vev.wire.v1.RoutePosition.target:type_name -> vev.wire.v1.ExactTarget | |
| 2585 | + | 15, // 20: vev.wire.v1.RouteRetired.ref:type_name -> vev.wire.v1.RouteRef | |
| 2586 | + | 33, // 21: vev.wire.v1.RouteRetired.target:type_name -> vev.wire.v1.ExactTarget | |
| 2587 | + | 33, // 22: vev.wire.v1.SamePeerSwitchRequest.target:type_name -> vev.wire.v1.ExactTarget | |
| 2588 | + | 33, // 23: vev.wire.v1.AttachmentSuspended.target:type_name -> vev.wire.v1.ExactTarget | |
| 2589 | + | 33, // 24: vev.wire.v1.ActivateAttachment.target:type_name -> vev.wire.v1.ExactTarget | |
| 2590 | + | 3, // 25: vev.wire.v1.AttachmentActivated.identity:type_name -> vev.wire.v1.CommittedRouteIdentity | |
| 2591 | + | 26, // [26:26] is the sub-list for method output_type | |
| 2592 | + | 26, // [26:26] is the sub-list for method input_type | |
| 2593 | + | 26, // [26:26] is the sub-list for extension type_name | |
| 2594 | + | 26, // [26:26] is the sub-list for extension extendee | |
| 2595 | + | 0, // [0:26] is the sub-list for field type_name | |
| 2592 | 2596 | } | |
| 2593 | 2597 | ||
| 2594 | 2598 | func init() { file_session_proto_init() } | |
| @@ -38,7 +38,6 @@ type Hello struct { | |||||
| 38 | PixelHeight uint32 `protobuf:"varint,9,opt,name=pixel_height,json=pixelHeight,proto3" json:"pixel_height,omitempty"` | 38 | PixelHeight uint32 `protobuf:"varint,9,opt,name=pixel_height,json=pixelHeight,proto3" json:"pixel_height,omitempty"` | ||
| 39 | TermEnv string `protobuf:"bytes,10,opt,name=term_env,json=termEnv,proto3" json:"term_env,omitempty"` | 39 | TermEnv string `protobuf:"bytes,10,opt,name=term_env,json=termEnv,proto3" json:"term_env,omitempty"` | ||
| 40 | Cwd string `protobuf:"bytes,11,opt,name=cwd,proto3" json:"cwd,omitempty"` | 40 | Cwd string `protobuf:"bytes,11,opt,name=cwd,proto3" json:"cwd,omitempty"` | ||
| 41 | - | TrueColor bool `protobuf:"varint,12,opt,name=true_color,json=trueColor,proto3" json:"true_color,omitempty"` | |||
| 42 | MaxOutputInFlight uint32 `protobuf:"varint,13,opt,name=max_output_in_flight,json=maxOutputInFlight,proto3" json:"max_output_in_flight,omitempty"` | 41 | MaxOutputInFlight uint32 `protobuf:"varint,13,opt,name=max_output_in_flight,json=maxOutputInFlight,proto3" json:"max_output_in_flight,omitempty"` | ||
| 43 | Env []string `protobuf:"bytes,14,rep,name=env,proto3" json:"env,omitempty"` | 42 | Env []string `protobuf:"bytes,14,rep,name=env,proto3" json:"env,omitempty"` | ||
| 44 | EnvironmentPolicy uint32 `protobuf:"varint,16,opt,name=environment_policy,json=environmentPolicy,proto3" json:"environment_policy,omitempty"` // 0 = client-owned, 1 = daemon-owned | 43 | EnvironmentPolicy uint32 `protobuf:"varint,16,opt,name=environment_policy,json=environmentPolicy,proto3" json:"environment_policy,omitempty"` // 0 = client-owned, 1 = daemon-owned | ||
| @@ -53,7 +52,10 @@ type Hello struct { | |||||
| 53 | // 1 = focused, 2 = unfocused. Later changes travel as TerminalFocus. | 52 | // 1 = focused, 2 = unfocused. Later changes travel as TerminalFocus. | ||
| 54 | TerminalFocus uint32 `protobuf:"varint,24,opt,name=terminal_focus,json=terminalFocus,proto3" json:"terminal_focus,omitempty"` | 53 | TerminalFocus uint32 `protobuf:"varint,24,opt,name=terminal_focus,json=terminalFocus,proto3" json:"terminal_focus,omitempty"` | ||
| 55 | // PID of a local client process, for bar scripts. 0 when unknown or remote. | 54 | // PID of a local client process, for bar scripts. 0 when unknown or remote. | ||
| 56 | - | ClientPid uint32 `protobuf:"varint,25,opt,name=client_pid,json=clientPid,proto3" json:"client_pid,omitempty"` | 55 | + | ClientPid uint32 `protobuf:"varint,25,opt,name=client_pid,json=clientPid,proto3" json:"client_pid,omitempty"` |
| 56 | + | // Color output capability the client claims. Required: a missing message | |||
| 57 | + | // or an UNSPECIFIED mode is a decode error. | |||
| 58 | + | Color *ColorCapabilities `protobuf:"bytes,26,opt,name=color,proto3" json:"color,omitempty"` | |||
| 57 | unknownFields protoimpl.UnknownFields | 59 | unknownFields protoimpl.UnknownFields | ||
| 58 | sizeCache protoimpl.SizeCache | 60 | sizeCache protoimpl.SizeCache | ||
| 59 | } | 61 | } | ||
| @@ -165,13 +167,6 @@ func (x *Hello) GetCwd() string { | |||||
| 165 | return "" | 167 | return "" | ||
| 166 | } | 168 | } | ||
| 167 | 169 | ||||
| 168 | - | func (x *Hello) GetTrueColor() bool { | |||
| 169 | - | if x != nil { | |||
| 170 | - | return x.TrueColor | |||
| 171 | - | } | |||
| 172 | - | return false | |||
| 173 | - | } | |||
| 174 | - | ||||
| 175 | func (x *Hello) GetMaxOutputInFlight() uint32 { | 170 | func (x *Hello) GetMaxOutputInFlight() uint32 { | ||
| 176 | if x != nil { | 171 | if x != nil { | ||
| 177 | return x.MaxOutputInFlight | 172 | return x.MaxOutputInFlight | ||
| @@ -256,6 +251,13 @@ func (x *Hello) GetClientPid() uint32 { | |||||
| 256 | return 0 | 251 | return 0 | ||
| 257 | } | 252 | } | ||
| 258 | 253 | ||||
| 254 | + | func (x *Hello) GetColor() *ColorCapabilities { | |||
| 255 | + | if x != nil { | |||
| 256 | + | return x.Color | |||
| 257 | + | } | |||
| 258 | + | return nil | |||
| 259 | + | } | |||
| 260 | + | ||||
| 259 | // Welcome answers an accepted attach. | 261 | // Welcome answers an accepted attach. | ||
| 260 | type Welcome struct { | 262 | type Welcome struct { | ||
| 261 | state protoimpl.MessageState `protogen:"open.v1"` | 263 | state protoimpl.MessageState `protogen:"open.v1"` | ||
| @@ -2306,7 +2308,7 @@ var File_session_proto protoreflect.FileDescriptor | |||||
| 2306 | 2308 | ||||
| 2307 | const file_session_proto_rawDesc = "" + | 2309 | const file_session_proto_rawDesc = "" + | ||
| 2308 | "\n" + | 2310 | "\n" + | ||
| 2309 | - | "\rsession.proto\x12\vvev.wire.v1\x1a\fcommon.proto\"\xee\x06\n" + | 2311 | + | "\rsession.proto\x12\vvev.wire.v1\x1a\fcommon.proto\"\x97\a\n" + |
| 2310 | "\x05Hello\x12\x18\n" + | 2312 | "\x05Hello\x12\x18\n" + | ||
| 2311 | "\aversion\x18\x01 \x01(\rR\aversion\x12\x16\n" + | 2313 | "\aversion\x18\x01 \x01(\rR\aversion\x12\x16\n" + | ||
| 2312 | "\x06intent\x18\x02 \x01(\rR\x06intent\x12\x1b\n" + | 2314 | "\x06intent\x18\x02 \x01(\rR\x06intent\x12\x1b\n" + | ||
| @@ -2320,9 +2322,7 @@ const file_session_proto_rawDesc = "" + | |||||
| 2320 | "\fpixel_height\x18\t \x01(\rR\vpixelHeight\x12\x19\n" + | 2322 | "\fpixel_height\x18\t \x01(\rR\vpixelHeight\x12\x19\n" + | ||
| 2321 | "\bterm_env\x18\n" + | 2323 | "\bterm_env\x18\n" + | ||
| 2322 | " \x01(\tR\atermEnv\x12\x10\n" + | 2324 | " \x01(\tR\atermEnv\x12\x10\n" + | ||
| 2323 | - | "\x03cwd\x18\v \x01(\tR\x03cwd\x12\x1d\n" + | 2325 | + | "\x03cwd\x18\v \x01(\tR\x03cwd\x12/\n" + |
| 2324 | - | "\n" + | |||
| 2325 | - | "true_color\x18\f \x01(\bR\ttrueColor\x12/\n" + | |||
| 2326 | "\x14max_output_in_flight\x18\r \x01(\rR\x11maxOutputInFlight\x12\x10\n" + | 2326 | "\x14max_output_in_flight\x18\r \x01(\rR\x11maxOutputInFlight\x12\x10\n" + | ||
| 2327 | "\x03env\x18\x0e \x03(\tR\x03env\x12-\n" + | 2327 | "\x03env\x18\x0e \x03(\tR\x03env\x12-\n" + | ||
| 2328 | "\x12environment_policy\x18\x10 \x01(\rR\x11environmentPolicy\x12;\n" + | 2328 | "\x12environment_policy\x18\x10 \x01(\rR\x11environmentPolicy\x12;\n" + | ||
| @@ -2335,7 +2335,9 @@ const file_session_proto_rawDesc = "" + | |||||
| 2335 | "\x0ekitty_keyboard\x18\x17 \x01(\bR\rkittyKeyboard\x12%\n" + | 2335 | "\x0ekitty_keyboard\x18\x17 \x01(\bR\rkittyKeyboard\x12%\n" + | ||
| 2336 | "\x0eterminal_focus\x18\x18 \x01(\rR\rterminalFocus\x12\x1d\n" + | 2336 | "\x0eterminal_focus\x18\x18 \x01(\rR\rterminalFocus\x12\x1d\n" + | ||
| 2337 | "\n" + | 2337 | "\n" + | ||
| 2338 | - | "client_pid\x18\x19 \x01(\rR\tclientPidJ\x04\b\x0f\x10\x10R\rremote_target\"\x84\x02\n" + | 2338 | + | "client_pid\x18\x19 \x01(\rR\tclientPid\x124\n" + |
| 2339 | + | "\x05color\x18\x1a \x01(\v2\x1e.vev.wire.v1.ColorCapabilitiesR\x05colorJ\x04\b\f\x10\rJ\x04\b\x0f\x10\x10R\n" + | |||
| 2340 | + | "true_colorR\rremote_target\"\x84\x02\n" + | |||
| 2339 | "\aWelcome\x12\x1d\n" + | 2341 | "\aWelcome\x12\x1d\n" + | ||
| 2340 | "\n" + | 2342 | "\n" + | ||
| 2341 | "session_id\x18\x01 \x01(\tR\tsessionId\x12!\n" + | 2343 | "session_id\x18\x01 \x01(\tR\tsessionId\x12!\n" + | ||
| @@ -2557,38 +2559,40 @@ var file_session_proto_goTypes = []any{ | |||||
| 2557 | (*AttachmentActivated)(nil), // 32: vev.wire.v1.AttachmentActivated | 2559 | (*AttachmentActivated)(nil), // 32: vev.wire.v1.AttachmentActivated | ||
| 2558 | (*ExactTarget)(nil), // 33: vev.wire.v1.ExactTarget | 2560 | (*ExactTarget)(nil), // 33: vev.wire.v1.ExactTarget | ||
| 2559 | (*SessionAttachTarget)(nil), // 34: vev.wire.v1.SessionAttachTarget | 2561 | (*SessionAttachTarget)(nil), // 34: vev.wire.v1.SessionAttachTarget | ||
| 2562 | + | (*ColorCapabilities)(nil), // 35: vev.wire.v1.ColorCapabilities | |||
| 2560 | } | 2563 | } | ||
| 2561 | var file_session_proto_depIdxs = []int32{ | 2564 | var file_session_proto_depIdxs = []int32{ | ||
| 2562 | 33, // 0: vev.wire.v1.Hello.exact_target:type_name -> vev.wire.v1.ExactTarget | 2565 | 33, // 0: vev.wire.v1.Hello.exact_target:type_name -> vev.wire.v1.ExactTarget | ||
| 2563 | 34, // 1: vev.wire.v1.Hello.session_target:type_name -> vev.wire.v1.SessionAttachTarget | 2566 | 34, // 1: vev.wire.v1.Hello.session_target:type_name -> vev.wire.v1.SessionAttachTarget | ||
| 2564 | - | 3, // 2: vev.wire.v1.Welcome.committed_identity:type_name -> vev.wire.v1.CommittedRouteIdentity | 2567 | + | 35, // 2: vev.wire.v1.Hello.color:type_name -> vev.wire.v1.ColorCapabilities |
| 2565 | - | 33, // 3: vev.wire.v1.CommittedRouteIdentity.target:type_name -> vev.wire.v1.ExactTarget | 2568 | + | 3, // 3: vev.wire.v1.Welcome.committed_identity:type_name -> vev.wire.v1.CommittedRouteIdentity |
| 2566 | - | 33, // 4: vev.wire.v1.AttachTarget.exact_target:type_name -> vev.wire.v1.ExactTarget | 2569 | + | 33, // 4: vev.wire.v1.CommittedRouteIdentity.target:type_name -> vev.wire.v1.ExactTarget |
| 2567 | - | 34, // 5: vev.wire.v1.AttachTarget.session_target:type_name -> vev.wire.v1.SessionAttachTarget | 2570 | + | 33, // 5: vev.wire.v1.AttachTarget.exact_target:type_name -> vev.wire.v1.ExactTarget |
| 2568 | - | 5, // 6: vev.wire.v1.Sessions.sessions:type_name -> vev.wire.v1.SessionInfo | 2571 | + | 34, // 6: vev.wire.v1.AttachTarget.session_target:type_name -> vev.wire.v1.SessionAttachTarget |
| 2569 | - | 8, // 7: vev.wire.v1.KillResult.failures:type_name -> vev.wire.v1.KillFailure | 2572 | + | 5, // 7: vev.wire.v1.Sessions.sessions:type_name -> vev.wire.v1.SessionInfo |
| 2570 | - | 15, // 8: vev.wire.v1.RouteAttentionTarget.ref:type_name -> vev.wire.v1.RouteRef | 2573 | + | 8, // 8: vev.wire.v1.KillResult.failures:type_name -> vev.wire.v1.KillFailure |
| 2571 | - | 33, // 9: vev.wire.v1.RouteAttentionTarget.target:type_name -> vev.wire.v1.ExactTarget | 2574 | + | 15, // 9: vev.wire.v1.RouteAttentionTarget.ref:type_name -> vev.wire.v1.RouteRef |
| 2572 | - | 16, // 10: vev.wire.v1.RouteAttentionSubscription.targets:type_name -> vev.wire.v1.RouteAttentionTarget | 2575 | + | 33, // 10: vev.wire.v1.RouteAttentionTarget.target:type_name -> vev.wire.v1.ExactTarget |
| 2573 | - | 33, // 11: vev.wire.v1.RecentRouteEntry.target:type_name -> vev.wire.v1.ExactTarget | 2576 | + | 16, // 11: vev.wire.v1.RouteAttentionSubscription.targets:type_name -> vev.wire.v1.RouteAttentionTarget |
| 2574 | - | 15, // 12: vev.wire.v1.RecentRouteSnapshot.active:type_name -> vev.wire.v1.RouteRef | 2577 | + | 33, // 12: vev.wire.v1.RecentRouteEntry.target:type_name -> vev.wire.v1.ExactTarget |
| 2575 | - | 18, // 13: vev.wire.v1.RecentRouteSnapshot.active_entry:type_name -> vev.wire.v1.RecentRouteEntry | 2578 | + | 15, // 13: vev.wire.v1.RecentRouteSnapshot.active:type_name -> vev.wire.v1.RouteRef |
| 2576 | - | 15, // 14: vev.wire.v1.RecentRouteSnapshot.previous:type_name -> vev.wire.v1.RouteRef | 2579 | + | 18, // 14: vev.wire.v1.RecentRouteSnapshot.active_entry:type_name -> vev.wire.v1.RecentRouteEntry |
| 2577 | - | 15, // 15: vev.wire.v1.RecentRouteSnapshot.home:type_name -> vev.wire.v1.RouteRef | 2580 | + | 15, // 15: vev.wire.v1.RecentRouteSnapshot.previous:type_name -> vev.wire.v1.RouteRef |
| 2578 | - | 18, // 16: vev.wire.v1.RecentRouteSnapshot.entries:type_name -> vev.wire.v1.RecentRouteEntry | 2581 | + | 15, // 16: vev.wire.v1.RecentRouteSnapshot.home:type_name -> vev.wire.v1.RouteRef |
| 2579 | - | 19, // 17: vev.wire.v1.RecentRouteSnapshot.hosts:type_name -> vev.wire.v1.RouteHost | 2582 | + | 18, // 17: vev.wire.v1.RecentRouteSnapshot.entries:type_name -> vev.wire.v1.RecentRouteEntry |
| 2580 | - | 33, // 18: vev.wire.v1.RoutePosition.target:type_name -> vev.wire.v1.ExactTarget | 2583 | + | 19, // 18: vev.wire.v1.RecentRouteSnapshot.hosts:type_name -> vev.wire.v1.RouteHost |
| 2581 | - | 15, // 19: vev.wire.v1.RouteRetired.ref:type_name -> vev.wire.v1.RouteRef | 2584 | + | 33, // 19: vev.wire.v1.RoutePosition.target:type_name -> vev.wire.v1.ExactTarget |
| 2582 | - | 33, // 20: vev.wire.v1.RouteRetired.target:type_name -> vev.wire.v1.ExactTarget | 2585 | + | 15, // 20: vev.wire.v1.RouteRetired.ref:type_name -> vev.wire.v1.RouteRef |
| 2583 | - | 33, // 21: vev.wire.v1.SamePeerSwitchRequest.target:type_name -> vev.wire.v1.ExactTarget | 2586 | + | 33, // 21: vev.wire.v1.RouteRetired.target:type_name -> vev.wire.v1.ExactTarget |
| 2584 | - | 33, // 22: vev.wire.v1.AttachmentSuspended.target:type_name -> vev.wire.v1.ExactTarget | 2587 | + | 33, // 22: vev.wire.v1.SamePeerSwitchRequest.target:type_name -> vev.wire.v1.ExactTarget |
| 2585 | - | 33, // 23: vev.wire.v1.ActivateAttachment.target:type_name -> vev.wire.v1.ExactTarget | 2588 | + | 33, // 23: vev.wire.v1.AttachmentSuspended.target:type_name -> vev.wire.v1.ExactTarget |
| 2586 | - | 3, // 24: vev.wire.v1.AttachmentActivated.identity:type_name -> vev.wire.v1.CommittedRouteIdentity | 2589 | + | 33, // 24: vev.wire.v1.ActivateAttachment.target:type_name -> vev.wire.v1.ExactTarget |
| 2587 | - | 25, // [25:25] is the sub-list for method output_type | 2590 | + | 3, // 25: vev.wire.v1.AttachmentActivated.identity:type_name -> vev.wire.v1.CommittedRouteIdentity |
| 2588 | - | 25, // [25:25] is the sub-list for method input_type | 2591 | + | 26, // [26:26] is the sub-list for method output_type |
| 2589 | - | 25, // [25:25] is the sub-list for extension type_name | 2592 | + | 26, // [26:26] is the sub-list for method input_type |
| 2590 | - | 25, // [25:25] is the sub-list for extension extendee | 2593 | + | 26, // [26:26] is the sub-list for extension type_name |
| 2591 | - | 0, // [0:25] is the sub-list for field type_name | 2594 | + | 26, // [26:26] is the sub-list for extension extendee |
| 2595 | + | 0, // [0:26] is the sub-list for field type_name | |||
| 2592 | } | 2596 | } | ||
| 2593 | 2597 | ||||
| 2594 | func init() { file_session_proto_init() } | 2598 | func init() { file_session_proto_init() } | ||
M internal/protocol/wire/terminal.pb.go +4 -13 generated file: collapsed
M internal/usecase/client/attachment_input.go +1 -1
| @@ -161,7 +161,7 @@ func (in *attachmentInput) start(ctx context.Context) error { | |||
| 161 | 161 | if in.palette == nil { | |
| 162 | 162 | return nil | |
| 163 | 163 | } | |
| 164 | - | retained := in.themes.update(func(current *protocol.Theme) { current.TrueColor = in.worker.cfg.Color.RGB() }) | |
| 164 | + | retained := in.themes.update(func(*protocol.Theme) {}) | |
| 165 | 165 | return in.apply(ctx, in.palette.start(retained, false)) | |
| 166 | 166 | } | |
| 167 | 167 | ||
| @@ -161,7 +161,7 @@ func (in *attachmentInput) start(ctx context.Context) error { | |||||
| 161 | if in.palette == nil { | 161 | if in.palette == nil { | ||
| 162 | return nil | 162 | return nil | ||
| 163 | } | 163 | } | ||
| 164 | - | retained := in.themes.update(func(current *protocol.Theme) { current.TrueColor = in.worker.cfg.Color.RGB() }) | 164 | + | retained := in.themes.update(func(*protocol.Theme) {}) |
| 165 | return in.apply(ctx, in.palette.start(retained, false)) | 165 | return in.apply(ctx, in.palette.start(retained, false)) | ||
| 166 | } | 166 | } | ||
| 167 | 167 | ||||
M internal/usecase/client/attachment_input_test.go +0 -1
| @@ -189,7 +189,6 @@ func TestAttachmentInputPublishesClearedThenDefinitivePalette(t *testing.T) { | |||
| 189 | 189 | first.awaitQueries(t, paletteColorBatch, 1) | |
| 190 | 190 | require.True(t, strings.HasPrefix(first.term.written(), "\x1b[Hready"), "the query follows the committed frame") | |
| 191 | 191 | cleared := first.awaitThemes(t, 1)[0] | |
| 192 | - | require.True(t, cleared.TrueColor) | |
| 193 | 192 | require.Zero(t, cleared.PaletteKnown) | |
| 194 | 193 | require.False(t, cleared.HasForeground) | |
| 195 | 194 | ||
| @@ -189,7 +189,6 @@ func TestAttachmentInputPublishesClearedThenDefinitivePalette(t *testing.T) { | |||||
| 189 | first.awaitQueries(t, paletteColorBatch, 1) | 189 | first.awaitQueries(t, paletteColorBatch, 1) | ||
| 190 | require.True(t, strings.HasPrefix(first.term.written(), "\x1b[Hready"), "the query follows the committed frame") | 190 | require.True(t, strings.HasPrefix(first.term.written(), "\x1b[Hready"), "the query follows the committed frame") | ||
| 191 | cleared := first.awaitThemes(t, 1)[0] | 191 | cleared := first.awaitThemes(t, 1)[0] | ||
| 192 | - | require.True(t, cleared.TrueColor) | |||
| 193 | require.Zero(t, cleared.PaletteKnown) | 192 | require.Zero(t, cleared.PaletteKnown) | ||
| 194 | require.False(t, cleared.HasForeground) | 193 | require.False(t, cleared.HasForeground) | ||
| 195 | 194 | ||||
M internal/usecase/client/attachment_session.go +1 -1
| @@ -752,7 +752,7 @@ func (w *sessionAttachmentWorker) hello(stream ports.BrokerLogicalConnection) pr | |||
| 752 | 752 | PixelHeight: geometry.PixelHeight, | |
| 753 | 753 | TermEnv: w.cfg.TermEnv, | |
| 754 | 754 | Cwd: cwd, | |
| 755 | - | TrueColor: w.cfg.Color.RGB(), | |
| 755 | + | Color: w.cfg.Color, | |
| 756 | 756 | // A remote attach reaches the terminal through this client, so both | |
| 757 | 757 | // declarations describe this client's outer terminal. | |
| 758 | 758 | KittyDirectGraphics: w.cfg.Capabilities.KittyGraphics, | |
| @@ -752,7 +752,7 @@ func (w *sessionAttachmentWorker) hello(stream ports.BrokerLogicalConnection) pr | |||||
| 752 | PixelHeight: geometry.PixelHeight, | 752 | PixelHeight: geometry.PixelHeight, | ||
| 753 | TermEnv: w.cfg.TermEnv, | 753 | TermEnv: w.cfg.TermEnv, | ||
| 754 | Cwd: cwd, | 754 | Cwd: cwd, | ||
| 755 | - | TrueColor: w.cfg.Color.RGB(), | 755 | + | Color: w.cfg.Color, |
| 756 | // A remote attach reaches the terminal through this client, so both | 756 | // A remote attach reaches the terminal through this client, so both | ||
| 757 | // declarations describe this client's outer terminal. | 757 | // declarations describe this client's outer terminal. | ||
| 758 | KittyDirectGraphics: w.cfg.Capabilities.KittyGraphics, | 758 | KittyDirectGraphics: w.cfg.Capabilities.KittyGraphics, | ||
M internal/usecase/client/attachment_session_test.go +6 -5
| @@ -529,11 +529,12 @@ func TestSessionAttachmentWorkerHelloEnvironment(t *testing.T) { | |||
| 529 | 529 | tests := []struct { | |
| 530 | 530 | name string | |
| 531 | 531 | env AttachmentEnvironment | |
| 532 | - | want bool | |
| 532 | + | want terminalcap.ColorCapabilities | |
| 533 | 533 | }{ | |
| 534 | - | {name: "unset defaults to truecolor", want: true}, | |
| 535 | - | {name: "supplied truecolor", env: AttachmentEnvironment{TermEnv: "xterm-256color", Cwd: "/workspace", Color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor}}, want: true}, | |
| 536 | - | {name: "supplied ansi256", env: AttachmentEnvironment{TermEnv: "xterm-256color", Cwd: "/workspace", Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256}}}, | |
| 534 | + | {name: "unset defaults to truecolor", want: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor}}, | |
| 535 | + | {name: "supplied truecolor", env: AttachmentEnvironment{TermEnv: "xterm-256color", Cwd: "/workspace", Color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor, Source: terminalcap.SourceDeclared}}, want: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor, Source: terminalcap.SourceDeclared}}, | |
| 536 | + | {name: "supplied ansi256", env: AttachmentEnvironment{TermEnv: "xterm-256color", Cwd: "/workspace", Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256}}, want: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256}}, | |
| 537 | + | {name: "supplied forced ansi16", env: AttachmentEnvironment{TermEnv: "xterm-256color", Cwd: "/workspace", Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceForced}}, want: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceForced}}, | |
| 537 | 538 | } | |
| 538 | 539 | for _, tt := range tests { | |
| 539 | 540 | t.Run(tt.name, func(t *testing.T) { | |
| @@ -554,7 +555,7 @@ func TestSessionAttachmentWorkerHelloEnvironment(t *testing.T) { | |||
| 554 | 555 | hello := awaitHello(t, stream) | |
| 555 | 556 | require.Equal(t, tt.env.TermEnv, hello.TermEnv) | |
| 556 | 557 | require.Equal(t, tt.env.Cwd, hello.Cwd) | |
| 557 | - | require.Equal(t, tt.want, hello.TrueColor) | |
| 558 | + | require.Equal(t, tt.want, hello.Color) | |
| 558 | 559 | require.NoError(t, stream.Close()) | |
| 559 | 560 | <-events | |
| 560 | 561 | }) | |
| @@ -529,11 +529,12 @@ func TestSessionAttachmentWorkerHelloEnvironment(t *testing.T) { | |||||
| 529 | tests := []struct { | 529 | tests := []struct { | ||
| 530 | name string | 530 | name string | ||
| 531 | env AttachmentEnvironment | 531 | env AttachmentEnvironment | ||
| 532 | - | want bool | 532 | + | want terminalcap.ColorCapabilities |
| 533 | }{ | 533 | }{ | ||
| 534 | - | {name: "unset defaults to truecolor", want: true}, | 534 | + | {name: "unset defaults to truecolor", want: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor}}, |
| 535 | - | {name: "supplied truecolor", env: AttachmentEnvironment{TermEnv: "xterm-256color", Cwd: "/workspace", Color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor}}, want: true}, | 535 | + | {name: "supplied truecolor", env: AttachmentEnvironment{TermEnv: "xterm-256color", Cwd: "/workspace", Color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor, Source: terminalcap.SourceDeclared}}, want: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor, Source: terminalcap.SourceDeclared}}, |
| 536 | - | {name: "supplied ansi256", env: AttachmentEnvironment{TermEnv: "xterm-256color", Cwd: "/workspace", Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256}}}, | 536 | + | {name: "supplied ansi256", env: AttachmentEnvironment{TermEnv: "xterm-256color", Cwd: "/workspace", Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256}}, want: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256}}, |
| 537 | + | {name: "supplied forced ansi16", env: AttachmentEnvironment{TermEnv: "xterm-256color", Cwd: "/workspace", Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceForced}}, want: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceForced}}, | |||
| 537 | } | 538 | } | ||
| 538 | for _, tt := range tests { | 539 | for _, tt := range tests { | ||
| 539 | t.Run(tt.name, func(t *testing.T) { | 540 | t.Run(tt.name, func(t *testing.T) { | ||
| @@ -554,7 +555,7 @@ func TestSessionAttachmentWorkerHelloEnvironment(t *testing.T) { | |||||
| 554 | hello := awaitHello(t, stream) | 555 | hello := awaitHello(t, stream) | ||
| 555 | require.Equal(t, tt.env.TermEnv, hello.TermEnv) | 556 | require.Equal(t, tt.env.TermEnv, hello.TermEnv) | ||
| 556 | require.Equal(t, tt.env.Cwd, hello.Cwd) | 557 | require.Equal(t, tt.env.Cwd, hello.Cwd) | ||
| 557 | - | require.Equal(t, tt.want, hello.TrueColor) | 558 | + | require.Equal(t, tt.want, hello.Color) |
| 558 | require.NoError(t, stream.Close()) | 559 | require.NoError(t, stream.Close()) | ||
| 559 | <-events | 560 | <-events | ||
| 560 | }) | 561 | }) | ||
M internal/usecase/client/palette_generation.go +0 -1
| @@ -141,7 +141,6 @@ func (c *paletteGenerationCoordinator) start(retained protocol.Theme, replacemen | |||
| 141 | 141 | id: id, | |
| 142 | 142 | phase: generationCollecting, | |
| 143 | 143 | theme: protocol.Theme{ | |
| 144 | - | TrueColor: retained.TrueColor, | |
| 145 | 144 | SchemeKnown: retained.SchemeKnown, | |
| 146 | 145 | Light: retained.Light, | |
| 147 | 146 | }, | |
| @@ -141,7 +141,6 @@ func (c *paletteGenerationCoordinator) start(retained protocol.Theme, replacemen | |||||
| 141 | id: id, | 141 | id: id, | ||
| 142 | phase: generationCollecting, | 142 | phase: generationCollecting, | ||
| 143 | theme: protocol.Theme{ | 143 | theme: protocol.Theme{ | ||
| 144 | - | TrueColor: retained.TrueColor, | |||
| 145 | SchemeKnown: retained.SchemeKnown, | 144 | SchemeKnown: retained.SchemeKnown, | ||
| 146 | Light: retained.Light, | 145 | Light: retained.Light, | ||
| 147 | }, | 146 | }, | ||
M internal/usecase/client/palette_generation_test.go +0 -2
| @@ -54,7 +54,6 @@ func (c *manualPaletteClock) fireCompletion(g *paletteGenerationCoordinator, id | |||
| 54 | 54 | ||
| 55 | 55 | func generationTheme() protocol.Theme { | |
| 56 | 56 | return protocol.Theme{ | |
| 57 | - | TrueColor: true, | |
| 58 | 57 | HasForeground: true, | |
| 59 | 58 | Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | |
| 60 | 59 | HasBackground: true, | |
| @@ -119,7 +118,6 @@ func TestPaletteGenerationInitialPublishesClearedThenDefinitive(t *testing.T) { | |||
| 119 | 118 | paletteActionPublishFinal, | |
| 120 | 119 | }, actionKinds(final)) | |
| 121 | 120 | got := findAction(t, final, paletteActionPublishFinal).theme | |
| 122 | - | require.True(t, got.TrueColor) | |
| 123 | 121 | require.True(t, got.HasForeground) | |
| 124 | 122 | require.Equal(t, renderer.RGB{R: 10, G: 11, B: 12}, got.Foreground) | |
| 125 | 123 | require.True(t, got.HasBackground) | |
| @@ -54,7 +54,6 @@ func (c *manualPaletteClock) fireCompletion(g *paletteGenerationCoordinator, id | |||||
| 54 | 54 | ||||
| 55 | func generationTheme() protocol.Theme { | 55 | func generationTheme() protocol.Theme { | ||
| 56 | return protocol.Theme{ | 56 | return protocol.Theme{ | ||
| 57 | - | TrueColor: true, | |||
| 58 | HasForeground: true, | 57 | HasForeground: true, | ||
| 59 | Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | 58 | Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | ||
| 60 | HasBackground: true, | 59 | HasBackground: true, | ||
| @@ -119,7 +118,6 @@ func TestPaletteGenerationInitialPublishesClearedThenDefinitive(t *testing.T) { | |||||
| 119 | paletteActionPublishFinal, | 118 | paletteActionPublishFinal, | ||
| 120 | }, actionKinds(final)) | 119 | }, actionKinds(final)) | ||
| 121 | got := findAction(t, final, paletteActionPublishFinal).theme | 120 | got := findAction(t, final, paletteActionPublishFinal).theme | ||
| 122 | - | require.True(t, got.TrueColor) | |||
| 123 | require.True(t, got.HasForeground) | 121 | require.True(t, got.HasForeground) | ||
| 124 | require.Equal(t, renderer.RGB{R: 10, G: 11, B: 12}, got.Foreground) | 122 | require.Equal(t, renderer.RGB{R: 10, G: 11, B: 12}, got.Foreground) | ||
| 125 | require.True(t, got.HasBackground) | 123 | require.True(t, got.HasBackground) | ||
M internal/usecase/colorprofile/profile.go +10 -3
| @@ -9,10 +9,17 @@ import ( | |||
| 9 | 9 | ) | |
| 10 | 10 | ||
| 11 | 11 | // Profile returns the renderer profile for c. A terminal without RGB support | |
| 12 | - | // gets indexed colors, so RGB surfaces are quantized instead of dropped. | |
| 12 | + | // gets a reduced palette, so RGB surfaces are quantized instead of dropped. | |
| 13 | + | // Unknown modes fall back to the conservative 256-color profile. | |
| 13 | 14 | func Profile(c terminalcap.ColorCapabilities) ansi.ColorProfile { | |
| 14 | - | if c.RGB() { | |
| 15 | + | switch c.Mode { | |
| 16 | + | case terminalcap.TrueColor: | |
| 15 | 17 | return ansi.ColorProfileTrueColor | |
| 18 | + | case terminalcap.ANSI16: | |
| 19 | + | return ansi.ColorProfileANSI16 | |
| 20 | + | case terminalcap.Monochrome: | |
| 21 | + | return ansi.ColorProfileMonochrome | |
| 22 | + | default: | |
| 23 | + | return ansi.ColorProfileANSI256 | |
| 16 | 24 | } | |
| 17 | - | return ansi.ColorProfileANSI256 | |
| 18 | 25 | } | |
| @@ -9,10 +9,17 @@ import ( | |||||
| 9 | ) | 9 | ) | ||
| 10 | 10 | ||||
| 11 | // Profile returns the renderer profile for c. A terminal without RGB support | 11 | // Profile returns the renderer profile for c. A terminal without RGB support | ||
| 12 | - | // gets indexed colors, so RGB surfaces are quantized instead of dropped. | 12 | + | // gets a reduced palette, so RGB surfaces are quantized instead of dropped. |
| 13 | + | // Unknown modes fall back to the conservative 256-color profile. | |||
| 13 | func Profile(c terminalcap.ColorCapabilities) ansi.ColorProfile { | 14 | func Profile(c terminalcap.ColorCapabilities) ansi.ColorProfile { | ||
| 14 | - | if c.RGB() { | 15 | + | switch c.Mode { |
| 16 | + | case terminalcap.TrueColor: | |||
| 15 | return ansi.ColorProfileTrueColor | 17 | return ansi.ColorProfileTrueColor | ||
| 18 | + | case terminalcap.ANSI16: | |||
| 19 | + | return ansi.ColorProfileANSI16 | |||
| 20 | + | case terminalcap.Monochrome: | |||
| 21 | + | return ansi.ColorProfileMonochrome | |||
| 22 | + | default: | |||
| 23 | + | return ansi.ColorProfileANSI256 | |||
| 16 | } | 24 | } | ||
| 17 | - | return ansi.ColorProfileANSI256 | |||
| 18 | } | 25 | } | ||
M internal/usecase/colorprofile/profile_test.go +4 -0
| @@ -17,6 +17,10 @@ func TestProfile(t *testing.T) { | |||
| 17 | 17 | {name: "zero value is truecolor", in: terminalcap.ColorCapabilities{}, want: ansi.ColorProfileTrueColor}, | |
| 18 | 18 | {name: "truecolor", in: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor}, want: ansi.ColorProfileTrueColor}, | |
| 19 | 19 | {name: "ansi256", in: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256}, want: ansi.ColorProfileANSI256}, | |
| 20 | + | {name: "ansi16", in: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16}, want: ansi.ColorProfileANSI16}, | |
| 21 | + | {name: "monochrome", in: terminalcap.ColorCapabilities{Mode: terminalcap.Monochrome}, want: ansi.ColorProfileMonochrome}, | |
| 22 | + | {name: "forced source does not change mapping", in: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceForced}, want: ansi.ColorProfileANSI16}, | |
| 23 | + | {name: "unknown mode is conservative", in: terminalcap.ColorCapabilities{Mode: terminalcap.ColorMode(99)}, want: ansi.ColorProfileANSI256}, | |
| 20 | 24 | } | |
| 21 | 25 | for _, tt := range tests { | |
| 22 | 26 | t.Run(tt.name, func(t *testing.T) { | |
| @@ -17,6 +17,10 @@ func TestProfile(t *testing.T) { | |||||
| 17 | {name: "zero value is truecolor", in: terminalcap.ColorCapabilities{}, want: ansi.ColorProfileTrueColor}, | 17 | {name: "zero value is truecolor", in: terminalcap.ColorCapabilities{}, want: ansi.ColorProfileTrueColor}, | ||
| 18 | {name: "truecolor", in: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor}, want: ansi.ColorProfileTrueColor}, | 18 | {name: "truecolor", in: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor}, want: ansi.ColorProfileTrueColor}, | ||
| 19 | {name: "ansi256", in: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256}, want: ansi.ColorProfileANSI256}, | 19 | {name: "ansi256", in: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256}, want: ansi.ColorProfileANSI256}, | ||
| 20 | + | {name: "ansi16", in: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16}, want: ansi.ColorProfileANSI16}, | |||
| 21 | + | {name: "monochrome", in: terminalcap.ColorCapabilities{Mode: terminalcap.Monochrome}, want: ansi.ColorProfileMonochrome}, | |||
| 22 | + | {name: "forced source does not change mapping", in: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceForced}, want: ansi.ColorProfileANSI16}, | |||
| 23 | + | {name: "unknown mode is conservative", in: terminalcap.ColorCapabilities{Mode: terminalcap.ColorMode(99)}, want: ansi.ColorProfileANSI256}, | |||
| 20 | } | 24 | } | ||
| 21 | for _, tt := range tests { | 25 | for _, tt := range tests { | ||
| 22 | t.Run(tt.name, func(t *testing.T) { | 26 | t.Run(tt.name, func(t *testing.T) { | ||
M internal/usecase/daemon/client.go +21 -3
| @@ -693,14 +693,33 @@ func (d *Daemon) attachClient(sess *session, tr ports.ServerConnection, sz domai | |||
| 693 | 693 | return ac, nil | |
| 694 | 694 | } | |
| 695 | 695 | ||
| 696 | + | // colorDowngradeNotice reports the toast shown when a declared (not forced) | |
| 697 | + | // terminal renders with fewer than TrueColor colors. A forced mode is a user | |
| 698 | + | // choice and never warrants a notice. | |
| 699 | + | func colorDowngradeNotice(color terminalcap.ColorCapabilities) (string, bool) { | |
| 700 | + | if color.Source != terminalcap.SourceDeclared { | |
| 701 | + | return "", false | |
| 702 | + | } | |
| 703 | + | switch color.Mode { | |
| 704 | + | case terminalcap.ANSI256: | |
| 705 | + | return "TrueColor was not detected; rendering with 256 colors.", true | |
| 706 | + | case terminalcap.ANSI16: | |
| 707 | + | return "TrueColor was not detected; rendering with 16 colors.", true | |
| 708 | + | case terminalcap.Monochrome: | |
| 709 | + | return "TrueColor was not detected; rendering without colors.", true | |
| 710 | + | default: | |
| 711 | + | return "", false | |
| 712 | + | } | |
| 713 | + | } | |
| 714 | + | ||
| 696 | 715 | func (d *Daemon) finishAttachedClient(sess *session, ac *attachedClient, opts attachClientOptions) { | |
| 697 | 716 | d.touchMRU(sess) | |
| 698 | 717 | d.log.Info("client attached", "session", sess.name, "resume", opts.resumeCapable) | |
| 699 | - | if ac.terminalCapabilities.Color.Source == terminalcap.SourceDeclared && !ac.terminalCapabilities.Color.RGB() { | |
| 718 | + | if message, ok := colorDowngradeNotice(ac.terminalCapabilities.Color); ok { | |
| 700 | 719 | d.publishToast(ac, domain.Notification{ | |
| 701 | 720 | Code: domain.NoticeUser, | |
| 702 | 721 | Severity: domain.NoticeWarn, | |
| 703 | - | Message: "TrueColor was not detected; rendering with 256 colors.", | |
| 722 | + | Message: message, | |
| 704 | 723 | Time: d.clock.Now(), | |
| 705 | 724 | Count: 1, | |
| 706 | 725 | SessionID: sess.id, | |
| @@ -947,7 +966,6 @@ func themeFromMessage(msg protocol.Theme) themeui.Theme { | |||
| 947 | 966 | PaletteKnown: msg.PaletteKnown, | |
| 948 | 967 | HasFG: msg.HasForeground, | |
| 949 | 968 | HasBG: msg.HasBackground, | |
| 950 | - | TrueColor: msg.TrueColor, | |
| 951 | 969 | Known: msg.HasForeground && msg.HasBackground, | |
| 952 | 970 | SchemeKnown: msg.SchemeKnown, | |
| 953 | 971 | Light: msg.Light, | |
| @@ -693,14 +693,33 @@ func (d *Daemon) attachClient(sess *session, tr ports.ServerConnection, sz domai | |||||
| 693 | return ac, nil | 693 | return ac, nil | ||
| 694 | } | 694 | } | ||
| 695 | 695 | ||||
| 696 | + | // colorDowngradeNotice reports the toast shown when a declared (not forced) | |||
| 697 | + | // terminal renders with fewer than TrueColor colors. A forced mode is a user | |||
| 698 | + | // choice and never warrants a notice. | |||
| 699 | + | func colorDowngradeNotice(color terminalcap.ColorCapabilities) (string, bool) { | |||
| 700 | + | if color.Source != terminalcap.SourceDeclared { | |||
| 701 | + | return "", false | |||
| 702 | + | } | |||
| 703 | + | switch color.Mode { | |||
| 704 | + | case terminalcap.ANSI256: | |||
| 705 | + | return "TrueColor was not detected; rendering with 256 colors.", true | |||
| 706 | + | case terminalcap.ANSI16: | |||
| 707 | + | return "TrueColor was not detected; rendering with 16 colors.", true | |||
| 708 | + | case terminalcap.Monochrome: | |||
| 709 | + | return "TrueColor was not detected; rendering without colors.", true | |||
| 710 | + | default: | |||
| 711 | + | return "", false | |||
| 712 | + | } | |||
| 713 | + | } | |||
| 714 | + | ||||
| 696 | func (d *Daemon) finishAttachedClient(sess *session, ac *attachedClient, opts attachClientOptions) { | 715 | func (d *Daemon) finishAttachedClient(sess *session, ac *attachedClient, opts attachClientOptions) { | ||
| 697 | d.touchMRU(sess) | 716 | d.touchMRU(sess) | ||
| 698 | d.log.Info("client attached", "session", sess.name, "resume", opts.resumeCapable) | 717 | d.log.Info("client attached", "session", sess.name, "resume", opts.resumeCapable) | ||
| 699 | - | if ac.terminalCapabilities.Color.Source == terminalcap.SourceDeclared && !ac.terminalCapabilities.Color.RGB() { | 718 | + | if message, ok := colorDowngradeNotice(ac.terminalCapabilities.Color); ok { |
| 700 | d.publishToast(ac, domain.Notification{ | 719 | d.publishToast(ac, domain.Notification{ | ||
| 701 | Code: domain.NoticeUser, | 720 | Code: domain.NoticeUser, | ||
| 702 | Severity: domain.NoticeWarn, | 721 | Severity: domain.NoticeWarn, | ||
| 703 | - | Message: "TrueColor was not detected; rendering with 256 colors.", | 722 | + | Message: message, |
| 704 | Time: d.clock.Now(), | 723 | Time: d.clock.Now(), | ||
| 705 | Count: 1, | 724 | Count: 1, | ||
| 706 | SessionID: sess.id, | 725 | SessionID: sess.id, | ||
| @@ -947,7 +966,6 @@ func themeFromMessage(msg protocol.Theme) themeui.Theme { | |||||
| 947 | PaletteKnown: msg.PaletteKnown, | 966 | PaletteKnown: msg.PaletteKnown, | ||
| 948 | HasFG: msg.HasForeground, | 967 | HasFG: msg.HasForeground, | ||
| 949 | HasBG: msg.HasBackground, | 968 | HasBG: msg.HasBackground, | ||
| 950 | - | TrueColor: msg.TrueColor, | |||
| 951 | Known: msg.HasForeground && msg.HasBackground, | 969 | Known: msg.HasForeground && msg.HasBackground, | ||
| 952 | SchemeKnown: msg.SchemeKnown, | 970 | SchemeKnown: msg.SchemeKnown, | ||
| 953 | Light: msg.Light, | 971 | Light: msg.Light, | ||
A internal/usecase/daemon/color_notice_test.go +68 -0
| @@ -0,0 +1,68 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/stretchr/testify/require" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestColorDowngradeNotice(t *testing.T) { | |
| 12 | + | tests := []struct { | |
| 13 | + | name string | |
| 14 | + | color terminalcap.ColorCapabilities | |
| 15 | + | want string | |
| 16 | + | }{ | |
| 17 | + | {name: "declared 256 colors", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256, Source: terminalcap.SourceDeclared}, want: "TrueColor was not detected; rendering with 256 colors."}, | |
| 18 | + | {name: "declared 16 colors", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceDeclared}, want: "TrueColor was not detected; rendering with 16 colors."}, | |
| 19 | + | {name: "declared monochrome", color: terminalcap.ColorCapabilities{Mode: terminalcap.Monochrome, Source: terminalcap.SourceDeclared}, want: "TrueColor was not detected; rendering without colors."}, | |
| 20 | + | {name: "declared truecolor", color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor, Source: terminalcap.SourceDeclared}}, | |
| 21 | + | {name: "forced 256 colors", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256, Source: terminalcap.SourceForced}}, | |
| 22 | + | {name: "forced 16 colors", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceForced}}, | |
| 23 | + | {name: "forced monochrome", color: terminalcap.ColorCapabilities{Mode: terminalcap.Monochrome, Source: terminalcap.SourceForced}}, | |
| 24 | + | {name: "heuristic 256 colors", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256, Source: terminalcap.SourceHeuristic}}, | |
| 25 | + | {name: "unknown 256 colors", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256}}, | |
| 26 | + | {name: "zero value", color: terminalcap.ColorCapabilities{}}, | |
| 27 | + | } | |
| 28 | + | for _, tt := range tests { | |
| 29 | + | t.Run(tt.name, func(t *testing.T) { | |
| 30 | + | got, ok := colorDowngradeNotice(tt.color) | |
| 31 | + | require.Equal(t, tt.want != "", ok) | |
| 32 | + | require.Equal(t, tt.want, got) | |
| 33 | + | }) | |
| 34 | + | } | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // TestFinishAttachedClientColorToast drives the real attach completion path: | |
| 38 | + | // only a declared reduced mode toasts, and a forced mode never does. | |
| 39 | + | func TestFinishAttachedClientColorToast(t *testing.T) { | |
| 40 | + | tests := []struct { | |
| 41 | + | name string | |
| 42 | + | color terminalcap.ColorCapabilities | |
| 43 | + | wantToast string | |
| 44 | + | }{ | |
| 45 | + | {name: "declared 256 colors toasts", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256, Source: terminalcap.SourceDeclared}, wantToast: "TrueColor was not detected; rendering with 256 colors."}, | |
| 46 | + | {name: "declared 16 colors toasts", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceDeclared}, wantToast: "TrueColor was not detected; rendering with 16 colors."}, | |
| 47 | + | {name: "forced 16 colors is silent", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceForced}}, | |
| 48 | + | {name: "declared truecolor is silent", color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor, Source: terminalcap.SourceDeclared}}, | |
| 49 | + | } | |
| 50 | + | for _, tt := range tests { | |
| 51 | + | t.Run(tt.name, func(t *testing.T) { | |
| 52 | + | pty, release := newBlockingPTY(t) | |
| 53 | + | defer release() | |
| 54 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, pty) | |
| 55 | + | ac.terminalCapabilities = terminalcap.Capabilities{Color: tt.color} | |
| 56 | + | ||
| 57 | + | d.finishAttachedClient(sess, ac, attachClientOptions{}) | |
| 58 | + | ||
| 59 | + | toasts, _ := visibleToasts(ac) | |
| 60 | + | if tt.wantToast == "" { | |
| 61 | + | require.Empty(t, toasts) | |
| 62 | + | return | |
| 63 | + | } | |
| 64 | + | require.Len(t, toasts, 1) | |
| 65 | + | require.Equal(t, tt.wantToast, toasts[0].Message) | |
| 66 | + | }) | |
| 67 | + | } | |
| 68 | + | } | |
| @@ -0,0 +1,68 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/stretchr/testify/require" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func TestColorDowngradeNotice(t *testing.T) { | |||
| 12 | + | tests := []struct { | |||
| 13 | + | name string | |||
| 14 | + | color terminalcap.ColorCapabilities | |||
| 15 | + | want string | |||
| 16 | + | }{ | |||
| 17 | + | {name: "declared 256 colors", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256, Source: terminalcap.SourceDeclared}, want: "TrueColor was not detected; rendering with 256 colors."}, | |||
| 18 | + | {name: "declared 16 colors", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceDeclared}, want: "TrueColor was not detected; rendering with 16 colors."}, | |||
| 19 | + | {name: "declared monochrome", color: terminalcap.ColorCapabilities{Mode: terminalcap.Monochrome, Source: terminalcap.SourceDeclared}, want: "TrueColor was not detected; rendering without colors."}, | |||
| 20 | + | {name: "declared truecolor", color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor, Source: terminalcap.SourceDeclared}}, | |||
| 21 | + | {name: "forced 256 colors", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256, Source: terminalcap.SourceForced}}, | |||
| 22 | + | {name: "forced 16 colors", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceForced}}, | |||
| 23 | + | {name: "forced monochrome", color: terminalcap.ColorCapabilities{Mode: terminalcap.Monochrome, Source: terminalcap.SourceForced}}, | |||
| 24 | + | {name: "heuristic 256 colors", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256, Source: terminalcap.SourceHeuristic}}, | |||
| 25 | + | {name: "unknown 256 colors", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256}}, | |||
| 26 | + | {name: "zero value", color: terminalcap.ColorCapabilities{}}, | |||
| 27 | + | } | |||
| 28 | + | for _, tt := range tests { | |||
| 29 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 30 | + | got, ok := colorDowngradeNotice(tt.color) | |||
| 31 | + | require.Equal(t, tt.want != "", ok) | |||
| 32 | + | require.Equal(t, tt.want, got) | |||
| 33 | + | }) | |||
| 34 | + | } | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // TestFinishAttachedClientColorToast drives the real attach completion path: | |||
| 38 | + | // only a declared reduced mode toasts, and a forced mode never does. | |||
| 39 | + | func TestFinishAttachedClientColorToast(t *testing.T) { | |||
| 40 | + | tests := []struct { | |||
| 41 | + | name string | |||
| 42 | + | color terminalcap.ColorCapabilities | |||
| 43 | + | wantToast string | |||
| 44 | + | }{ | |||
| 45 | + | {name: "declared 256 colors toasts", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256, Source: terminalcap.SourceDeclared}, wantToast: "TrueColor was not detected; rendering with 256 colors."}, | |||
| 46 | + | {name: "declared 16 colors toasts", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceDeclared}, wantToast: "TrueColor was not detected; rendering with 16 colors."}, | |||
| 47 | + | {name: "forced 16 colors is silent", color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16, Source: terminalcap.SourceForced}}, | |||
| 48 | + | {name: "declared truecolor is silent", color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor, Source: terminalcap.SourceDeclared}}, | |||
| 49 | + | } | |||
| 50 | + | for _, tt := range tests { | |||
| 51 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 52 | + | pty, release := newBlockingPTY(t) | |||
| 53 | + | defer release() | |||
| 54 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, pty) | |||
| 55 | + | ac.terminalCapabilities = terminalcap.Capabilities{Color: tt.color} | |||
| 56 | + | ||||
| 57 | + | d.finishAttachedClient(sess, ac, attachClientOptions{}) | |||
| 58 | + | ||||
| 59 | + | toasts, _ := visibleToasts(ac) | |||
| 60 | + | if tt.wantToast == "" { | |||
| 61 | + | require.Empty(t, toasts) | |||
| 62 | + | return | |||
| 63 | + | } | |||
| 64 | + | require.Len(t, toasts, 1) | |||
| 65 | + | require.Equal(t, tt.wantToast, toasts[0].Message) | |||
| 66 | + | }) | |||
| 67 | + | } | |||
| 68 | + | } | |||
M internal/usecase/daemon/daemon.go +1 -1
| @@ -1477,7 +1477,7 @@ func (d *Daemon) finishAttach(sess *session, tr ports.ServerConnection, sz domai | |||
| 1477 | 1477 | // otherwise falls back to the session's normal first-tab repair. | |
| 1478 | 1478 | initialTabIndex = preferredTabIndex(sess, h.PreferredTabID) | |
| 1479 | 1479 | } | |
| 1480 | - | terminalCapabilities := terminalcap.Resolve(h.Env, h.TrueColor) | |
| 1480 | + | terminalCapabilities := terminalcap.Resolve(h.Env, h.Color) | |
| 1481 | 1481 | // Kitty graphics are enabled only by the explicit direct-terminal | |
| 1482 | 1482 | // declaration in Hello. Environment values remain useful for color and | |
| 1483 | 1483 | // diagnostics, but cannot authorize terminal-global graphics side effects. | |
| @@ -1477,7 +1477,7 @@ func (d *Daemon) finishAttach(sess *session, tr ports.ServerConnection, sz domai | |||||
| 1477 | // otherwise falls back to the session's normal first-tab repair. | 1477 | // otherwise falls back to the session's normal first-tab repair. | ||
| 1478 | initialTabIndex = preferredTabIndex(sess, h.PreferredTabID) | 1478 | initialTabIndex = preferredTabIndex(sess, h.PreferredTabID) | ||
| 1479 | } | 1479 | } | ||
| 1480 | - | terminalCapabilities := terminalcap.Resolve(h.Env, h.TrueColor) | 1480 | + | terminalCapabilities := terminalcap.Resolve(h.Env, h.Color) |
| 1481 | // Kitty graphics are enabled only by the explicit direct-terminal | 1481 | // Kitty graphics are enabled only by the explicit direct-terminal | ||
| 1482 | // declaration in Hello. Environment values remain useful for color and | 1482 | // declaration in Hello. Environment values remain useful for color and | ||
| 1483 | // diagnostics, but cannot authorize terminal-global graphics side effects. | 1483 | // diagnostics, but cannot authorize terminal-global graphics side effects. | ||
M internal/usecase/daemon/floating_render_test.go +0 -1
| @@ -737,7 +737,6 @@ func benchmarkCapturedRenderState() capturedRenderState { | |||
| 737 | 737 | PaletteKnown: 1<<2 | 1<<4 | 1<<10 | 1<<12 | 1<<14, | |
| 738 | 738 | HasFG: true, | |
| 739 | 739 | HasBG: true, | |
| 740 | - | TrueColor: true, | |
| 741 | 740 | Known: true, | |
| 742 | 741 | SchemeKnown: true, | |
| 743 | 742 | UsePalette: true, | |
| @@ -737,7 +737,6 @@ func benchmarkCapturedRenderState() capturedRenderState { | |||||
| 737 | PaletteKnown: 1<<2 | 1<<4 | 1<<10 | 1<<12 | 1<<14, | 737 | PaletteKnown: 1<<2 | 1<<4 | 1<<10 | 1<<12 | 1<<14, | ||
| 738 | HasFG: true, | 738 | HasFG: true, | ||
| 739 | HasBG: true, | 739 | HasBG: true, | ||
| 740 | - | TrueColor: true, | |||
| 741 | Known: true, | 740 | Known: true, | ||
| 742 | SchemeKnown: true, | 741 | SchemeKnown: true, | ||
| 743 | UsePalette: true, | 742 | UsePalette: true, | ||
M internal/usecase/daemon/overlay_backdrop_test.go +1 -1
| @@ -11,7 +11,7 @@ import ( | |||
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | 13 | func backdropTheme() themeui.Theme { | |
| 14 | - | return themeui.Theme{Known: true, TrueColor: true, HasFG: true, HasBG: true, Foreground: renderer.RGB{R: 220, G: 220, B: 220}, Background: renderer.RGB{R: 10, G: 10, B: 10}} | |
| 14 | + | return themeui.Theme{Known: true, HasFG: true, HasBG: true, Foreground: renderer.RGB{R: 220, G: 220, B: 220}, Background: renderer.RGB{R: 10, G: 10, B: 10}} | |
| 15 | 15 | } | |
| 16 | 16 | ||
| 17 | 17 | func TestApplyOverlayBackdropDimsCompleteFrame(t *testing.T) { | |
| @@ -11,7 +11,7 @@ import ( | |||||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| 13 | func backdropTheme() themeui.Theme { | 13 | func backdropTheme() themeui.Theme { | ||
| 14 | - | return themeui.Theme{Known: true, TrueColor: true, HasFG: true, HasBG: true, Foreground: renderer.RGB{R: 220, G: 220, B: 220}, Background: renderer.RGB{R: 10, G: 10, B: 10}} | 14 | + | return themeui.Theme{Known: true, HasFG: true, HasBG: true, Foreground: renderer.RGB{R: 220, G: 220, B: 220}, Background: renderer.RGB{R: 10, G: 10, B: 10}} |
| 15 | } | 15 | } | ||
| 16 | 16 | ||||
| 17 | func TestApplyOverlayBackdropDimsCompleteFrame(t *testing.T) { | 17 | func TestApplyOverlayBackdropDimsCompleteFrame(t *testing.T) { | ||
M internal/usecase/daemon/palette_test.go +1 -2
| @@ -131,11 +131,10 @@ func TestCaptureOverlayLayersPreservesPaletteDescriptionSurfaceAcrossFallbacks(t | |||
| 131 | 131 | paletteColors[10] = paletteColors[2] | |
| 132 | 132 | accentTheme := themeui.Theme{ | |
| 133 | 133 | Foreground: renderer.RGB{R: 230, G: 230, B: 230}, Background: renderer.RGB{R: 8, G: 9, B: 10}, | |
| 134 | - | HasFG: true, HasBG: true, Known: true, TrueColor: true, UsePalette: true, | |
| 134 | + | HasFG: true, HasBG: true, Known: true, UsePalette: true, | |
| 135 | 135 | Palette: paletteColors, PaletteKnown: 1<<2 | 1<<10, | |
| 136 | 136 | } | |
| 137 | 137 | indexedTheme := accentTheme | |
| 138 | - | indexedTheme.TrueColor = false | |
| 139 | 138 | neutralTheme := accentTheme | |
| 140 | 139 | neutralTheme.PaletteKnown = 0 | |
| 141 | 140 | neutralTheme.SchemeKnown = false | |
| @@ -131,11 +131,10 @@ func TestCaptureOverlayLayersPreservesPaletteDescriptionSurfaceAcrossFallbacks(t | |||||
| 131 | paletteColors[10] = paletteColors[2] | 131 | paletteColors[10] = paletteColors[2] | ||
| 132 | accentTheme := themeui.Theme{ | 132 | accentTheme := themeui.Theme{ | ||
| 133 | Foreground: renderer.RGB{R: 230, G: 230, B: 230}, Background: renderer.RGB{R: 8, G: 9, B: 10}, | 133 | Foreground: renderer.RGB{R: 230, G: 230, B: 230}, Background: renderer.RGB{R: 8, G: 9, B: 10}, | ||
| 134 | - | HasFG: true, HasBG: true, Known: true, TrueColor: true, UsePalette: true, | 134 | + | HasFG: true, HasBG: true, Known: true, UsePalette: true, |
| 135 | Palette: paletteColors, PaletteKnown: 1<<2 | 1<<10, | 135 | Palette: paletteColors, PaletteKnown: 1<<2 | 1<<10, | ||
| 136 | } | 136 | } | ||
| 137 | indexedTheme := accentTheme | 137 | indexedTheme := accentTheme | ||
| 138 | - | indexedTheme.TrueColor = false | |||
| 139 | neutralTheme := accentTheme | 138 | neutralTheme := accentTheme | ||
| 140 | neutralTheme.PaletteKnown = 0 | 139 | neutralTheme.PaletteKnown = 0 | ||
| 141 | neutralTheme.SchemeKnown = false | 140 | neutralTheme.SchemeKnown = false | ||
M internal/usecase/daemon/render_coordinator_test.go +1 -1
| @@ -1221,7 +1221,7 @@ func TestProducerInvalidations(t *testing.T) { | |||
| 1221 | 1221 | file: "client.go", | |
| 1222 | 1222 | name: "client theme application", | |
| 1223 | 1223 | run: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) { | |
| 1224 | - | d.applyTheme(sess, ac, protocol.Theme{TrueColor: true}) | |
| 1224 | + | d.applyTheme(sess, ac, protocol.Theme{SchemeKnown: true}) | |
| 1225 | 1225 | }, | |
| 1226 | 1226 | }, | |
| 1227 | 1227 | { | |
| @@ -1221,7 +1221,7 @@ func TestProducerInvalidations(t *testing.T) { | |||||
| 1221 | file: "client.go", | 1221 | file: "client.go", | ||
| 1222 | name: "client theme application", | 1222 | name: "client theme application", | ||
| 1223 | run: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) { | 1223 | run: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) { | ||
| 1224 | - | d.applyTheme(sess, ac, protocol.Theme{TrueColor: true}) | 1224 | + | d.applyTheme(sess, ac, protocol.Theme{SchemeKnown: true}) |
| 1225 | }, | 1225 | }, | ||
| 1226 | }, | 1226 | }, | ||
| 1227 | { | 1227 | { | ||
M internal/usecase/daemon/render_pipeline_cache_test.go +2 -3
| @@ -94,7 +94,7 @@ func TestComposeFrameClearsFloatingFrameWhenItCloses(t *testing.T) { | |||
| 94 | 94 | } | |
| 95 | 95 | ||
| 96 | 96 | func TestComposeFrameCacheSkipsUndamagedBlitsAndInvalidatesFocusAndLayout(t *testing.T) { | |
| 97 | - | theme := themeui.Theme{Known: true, TrueColor: true, HasFG: true, HasBG: true, Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 10, B: 10}} | |
| 97 | + | theme := themeui.Theme{Known: true, HasFG: true, HasBG: true, Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 10, B: 10}} | |
| 98 | 98 | initial := cachedSplitState("horizontal-left", "left", layout.Horizontal, theme) | |
| 99 | 99 | committed := composeFrame(initial, composeCacheInput{}) | |
| 100 | 100 | require.Equal(t, '│', committed.frame.At(20, 1).Rune) | |
| @@ -170,7 +170,7 @@ func TestLayoutFingerprintWeightChangeInvalidatesGeometryCache(t *testing.T) { | |||
| 170 | 170 | } | |
| 171 | 171 | ||
| 172 | 172 | func TestComposeFrameUsesCachedNeutralStructuralBorder(t *testing.T) { | |
| 173 | - | theme := themeui.Theme{Known: true, TrueColor: true, HasFG: true, HasBG: true, Foreground: renderer.RGB{R: 220, G: 210, B: 200}, Background: renderer.RGB{R: 20, G: 30, B: 40}} | |
| 173 | + | theme := themeui.Theme{Known: true, HasFG: true, HasBG: true, Foreground: renderer.RGB{R: 220, G: 210, B: 200}, Background: renderer.RGB{R: 20, G: 30, B: 40}} | |
| 174 | 174 | neutralBorder := renderer.Style{HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 170, G: 80, B: 30}} | |
| 175 | 175 | expected := themeui.NewDimmer(theme).Dim(neutralBorder) | |
| 176 | 176 | ||
| @@ -197,7 +197,6 @@ func TestComposeFrameStackDrawsTitleBarsAndDimsCollapsed(t *testing.T) { | |||
| 197 | 197 | state := cachedStackTitleState("collapsed", 1, true) | |
| 198 | 198 | state.theme = themeui.Theme{ | |
| 199 | 199 | Known: true, | |
| 200 | - | TrueColor: true, | |
| 201 | 200 | HasFG: true, | |
| 202 | 201 | HasBG: true, | |
| 203 | 202 | Foreground: renderer.RGB{R: 220, G: 210, B: 200}, | |
| @@ -94,7 +94,7 @@ func TestComposeFrameClearsFloatingFrameWhenItCloses(t *testing.T) { | |||||
| 94 | } | 94 | } | ||
| 95 | 95 | ||||
| 96 | func TestComposeFrameCacheSkipsUndamagedBlitsAndInvalidatesFocusAndLayout(t *testing.T) { | 96 | func TestComposeFrameCacheSkipsUndamagedBlitsAndInvalidatesFocusAndLayout(t *testing.T) { | ||
| 97 | - | theme := themeui.Theme{Known: true, TrueColor: true, HasFG: true, HasBG: true, Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 10, B: 10}} | 97 | + | theme := themeui.Theme{Known: true, HasFG: true, HasBG: true, Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 10, B: 10}} |
| 98 | initial := cachedSplitState("horizontal-left", "left", layout.Horizontal, theme) | 98 | initial := cachedSplitState("horizontal-left", "left", layout.Horizontal, theme) | ||
| 99 | committed := composeFrame(initial, composeCacheInput{}) | 99 | committed := composeFrame(initial, composeCacheInput{}) | ||
| 100 | require.Equal(t, '│', committed.frame.At(20, 1).Rune) | 100 | require.Equal(t, '│', committed.frame.At(20, 1).Rune) | ||
| @@ -170,7 +170,7 @@ func TestLayoutFingerprintWeightChangeInvalidatesGeometryCache(t *testing.T) { | |||||
| 170 | } | 170 | } | ||
| 171 | 171 | ||||
| 172 | func TestComposeFrameUsesCachedNeutralStructuralBorder(t *testing.T) { | 172 | func TestComposeFrameUsesCachedNeutralStructuralBorder(t *testing.T) { | ||
| 173 | - | theme := themeui.Theme{Known: true, TrueColor: true, HasFG: true, HasBG: true, Foreground: renderer.RGB{R: 220, G: 210, B: 200}, Background: renderer.RGB{R: 20, G: 30, B: 40}} | 173 | + | theme := themeui.Theme{Known: true, HasFG: true, HasBG: true, Foreground: renderer.RGB{R: 220, G: 210, B: 200}, Background: renderer.RGB{R: 20, G: 30, B: 40}} |
| 174 | neutralBorder := renderer.Style{HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 170, G: 80, B: 30}} | 174 | neutralBorder := renderer.Style{HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 170, G: 80, B: 30}} | ||
| 175 | expected := themeui.NewDimmer(theme).Dim(neutralBorder) | 175 | expected := themeui.NewDimmer(theme).Dim(neutralBorder) | ||
| 176 | 176 | ||||
| @@ -197,7 +197,6 @@ func TestComposeFrameStackDrawsTitleBarsAndDimsCollapsed(t *testing.T) { | |||||
| 197 | state := cachedStackTitleState("collapsed", 1, true) | 197 | state := cachedStackTitleState("collapsed", 1, true) | ||
| 198 | state.theme = themeui.Theme{ | 198 | state.theme = themeui.Theme{ | ||
| 199 | Known: true, | 199 | Known: true, | ||
| 200 | - | TrueColor: true, | |||
| 201 | HasFG: true, | 200 | HasFG: true, | ||
| 202 | HasBG: true, | 201 | HasBG: true, | ||
| 203 | Foreground: renderer.RGB{R: 220, G: 210, B: 200}, | 202 | Foreground: renderer.RGB{R: 220, G: 210, B: 200}, | ||
M internal/usecase/daemon/resume_test.go +2 -1
| @@ -14,6 +14,7 @@ import ( | |||
| 14 | 14 | ||
| 15 | 15 | vt "github.com/bnema/vev-vt" | |
| 16 | 16 | "github.com/bnema/vev/internal/domain" | |
| 17 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 17 | 18 | "github.com/bnema/vev/internal/ports" | |
| 18 | 19 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 19 | 20 | "github.com/bnema/vev/internal/protocol" | |
| @@ -1594,7 +1595,7 @@ func TestResumeParkedRefreshesSessionBoundFuturePTYEnvironment(t *testing.T) { | |||
| 1594 | 1595 | ||
| 1595 | 1596 | resumeHello := helloResumeCapable(protocol.IntentResume, "work", token) | |
| 1596 | 1597 | resumeHello.Env = []string{"SECRET=after", "PAIR=a=b", "SHELL=/usr/bin/fish", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old", "WAYLAND_DISPLAY=wayland-1"} | |
| 1597 | - | resumeHello.TrueColor = true | |
| 1598 | + | resumeHello.Color = terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor, Source: terminalcap.SourceDeclared} | |
| 1598 | 1599 | _, _, ok, err := d.resumeParked(resumeHello, &closeTrackingTransport{}, domain.Size{Cols: 80, Rows: 24}) | |
| 1599 | 1600 | require.NoError(t, err) | |
| 1600 | 1601 | require.True(t, ok) | |
| @@ -14,6 +14,7 @@ import ( | |||||
| 14 | 14 | ||||
| 15 | vt "github.com/bnema/vev-vt" | 15 | vt "github.com/bnema/vev-vt" | ||
| 16 | "github.com/bnema/vev/internal/domain" | 16 | "github.com/bnema/vev/internal/domain" | ||
| 17 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |||
| 17 | "github.com/bnema/vev/internal/ports" | 18 | "github.com/bnema/vev/internal/ports" | ||
| 18 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | 19 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | ||
| 19 | "github.com/bnema/vev/internal/protocol" | 20 | "github.com/bnema/vev/internal/protocol" | ||
| @@ -1594,7 +1595,7 @@ func TestResumeParkedRefreshesSessionBoundFuturePTYEnvironment(t *testing.T) { | |||||
| 1594 | 1595 | ||||
| 1595 | resumeHello := helloResumeCapable(protocol.IntentResume, "work", token) | 1596 | resumeHello := helloResumeCapable(protocol.IntentResume, "work", token) | ||
| 1596 | resumeHello.Env = []string{"SECRET=after", "PAIR=a=b", "SHELL=/usr/bin/fish", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old", "WAYLAND_DISPLAY=wayland-1"} | 1597 | resumeHello.Env = []string{"SECRET=after", "PAIR=a=b", "SHELL=/usr/bin/fish", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old", "WAYLAND_DISPLAY=wayland-1"} | ||
| 1597 | - | resumeHello.TrueColor = true | 1598 | + | resumeHello.Color = terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor, Source: terminalcap.SourceDeclared} |
| 1598 | _, _, ok, err := d.resumeParked(resumeHello, &closeTrackingTransport{}, domain.Size{Cols: 80, Rows: 24}) | 1599 | _, _, ok, err := d.resumeParked(resumeHello, &closeTrackingTransport{}, domain.Size{Cols: 80, Rows: 24}) | ||
| 1599 | require.NoError(t, err) | 1600 | require.NoError(t, err) | ||
| 1600 | require.True(t, ok) | 1601 | require.True(t, ok) | ||
M internal/usecase/daemon/session_test.go +5 -4
| @@ -16,6 +16,7 @@ import ( | |||
| 16 | 16 | "github.com/stretchr/testify/require" | |
| 17 | 17 | ||
| 18 | 18 | "github.com/bnema/vev/internal/domain" | |
| 19 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 19 | 20 | "github.com/bnema/vev/internal/persist" | |
| 20 | 21 | "github.com/bnema/vev/internal/ports" | |
| 21 | 22 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| @@ -1618,7 +1619,7 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1618 | 1619 | tr := newMockServerConnection(t) | |
| 1619 | 1620 | tr.EXPECT().Send(mock.Anything).Return(nil).Maybe() | |
| 1620 | 1621 | tr.EXPECT().Close().Return(nil).Maybe() | |
| 1621 | - | sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr) | |
| 1622 | + | sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz}, tr) | |
| 1622 | 1623 | require.NoError(t, err) | |
| 1623 | 1624 | defer func() { | |
| 1624 | 1625 | _ = d.killSession(sess, protocol.ReasonServerShutdown, false) | |
| @@ -1663,9 +1664,9 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1663 | 1664 | tr2 := newMockServerConnection(t) | |
| 1664 | 1665 | tr2.EXPECT().Send(mock.Anything).Return(nil).Maybe() | |
| 1665 | 1666 | tr2.EXPECT().Close().Return(nil).Maybe() | |
| 1666 | - | sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: false}, tr1) | |
| 1667 | + | sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256, Source: terminalcap.SourceDeclared}}, tr1) | |
| 1667 | 1668 | require.NoError(t, err) | |
| 1668 | - | _, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz, TrueColor: true}, tr2) | |
| 1669 | + | _, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz}, tr2) | |
| 1669 | 1670 | require.NoError(t, err) | |
| 1670 | 1671 | defer func() { | |
| 1671 | 1672 | _ = d.killSession(sess, protocol.ReasonServerShutdown, false) | |
| @@ -1704,7 +1705,7 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 1704 | 1705 | tr := newMockServerConnection(t) | |
| 1705 | 1706 | tr.EXPECT().Send(mock.Anything).Return(nil).Maybe() | |
| 1706 | 1707 | tr.EXPECT().Close().Return(nil).Maybe() | |
| 1707 | - | sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr) | |
| 1708 | + | sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz}, tr) | |
| 1708 | 1709 | require.NoError(t, err) | |
| 1709 | 1710 | // The source coordinator is deliberately made pending so the switch must | |
| 1710 | 1711 | // invalidate it rather than letting its stale callback render for ac. | |
| @@ -16,6 +16,7 @@ import ( | |||||
| 16 | "github.com/stretchr/testify/require" | 16 | "github.com/stretchr/testify/require" | ||
| 17 | 17 | ||||
| 18 | "github.com/bnema/vev/internal/domain" | 18 | "github.com/bnema/vev/internal/domain" | ||
| 19 | + | "github.com/bnema/vev/internal/domain/terminalcap" | |||
| 19 | "github.com/bnema/vev/internal/persist" | 20 | "github.com/bnema/vev/internal/persist" | ||
| 20 | "github.com/bnema/vev/internal/ports" | 21 | "github.com/bnema/vev/internal/ports" | ||
| 21 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | 22 | portsmocks "github.com/bnema/vev/internal/ports/mocks" | ||
| @@ -1618,7 +1619,7 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1618 | tr := newMockServerConnection(t) | 1619 | tr := newMockServerConnection(t) | ||
| 1619 | tr.EXPECT().Send(mock.Anything).Return(nil).Maybe() | 1620 | tr.EXPECT().Send(mock.Anything).Return(nil).Maybe() | ||
| 1620 | tr.EXPECT().Close().Return(nil).Maybe() | 1621 | tr.EXPECT().Close().Return(nil).Maybe() | ||
| 1621 | - | sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr) | 1622 | + | sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz}, tr) |
| 1622 | require.NoError(t, err) | 1623 | require.NoError(t, err) | ||
| 1623 | defer func() { | 1624 | defer func() { | ||
| 1624 | _ = d.killSession(sess, protocol.ReasonServerShutdown, false) | 1625 | _ = d.killSession(sess, protocol.ReasonServerShutdown, false) | ||
| @@ -1663,9 +1664,9 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1663 | tr2 := newMockServerConnection(t) | 1664 | tr2 := newMockServerConnection(t) | ||
| 1664 | tr2.EXPECT().Send(mock.Anything).Return(nil).Maybe() | 1665 | tr2.EXPECT().Send(mock.Anything).Return(nil).Maybe() | ||
| 1665 | tr2.EXPECT().Close().Return(nil).Maybe() | 1666 | tr2.EXPECT().Close().Return(nil).Maybe() | ||
| 1666 | - | sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: false}, tr1) | 1667 | + | sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256, Source: terminalcap.SourceDeclared}}, tr1) |
| 1667 | require.NoError(t, err) | 1668 | require.NoError(t, err) | ||
| 1668 | - | _, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz, TrueColor: true}, tr2) | 1669 | + | _, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz}, tr2) |
| 1669 | require.NoError(t, err) | 1670 | require.NoError(t, err) | ||
| 1670 | defer func() { | 1671 | defer func() { | ||
| 1671 | _ = d.killSession(sess, protocol.ReasonServerShutdown, false) | 1672 | _ = d.killSession(sess, protocol.ReasonServerShutdown, false) | ||
| @@ -1704,7 +1705,7 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 1704 | tr := newMockServerConnection(t) | 1705 | tr := newMockServerConnection(t) | ||
| 1705 | tr.EXPECT().Send(mock.Anything).Return(nil).Maybe() | 1706 | tr.EXPECT().Send(mock.Anything).Return(nil).Maybe() | ||
| 1706 | tr.EXPECT().Close().Return(nil).Maybe() | 1707 | tr.EXPECT().Close().Return(nil).Maybe() | ||
| 1707 | - | sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr) | 1708 | + | sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz}, tr) |
| 1708 | require.NoError(t, err) | 1709 | require.NoError(t, err) | ||
| 1709 | // The source coordinator is deliberately made pending so the switch must | 1710 | // The source coordinator is deliberately made pending so the switch must | ||
| 1710 | // invalidate it rather than letting its stale callback render for ac. | 1711 | // invalidate it rather than letting its stale callback render for ac. | ||
M internal/usecase/daemon/status_test.go +7 -11
| @@ -297,7 +297,6 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) { | |||
| 297 | 297 | HasFG: true, | |
| 298 | 298 | HasBG: true, | |
| 299 | 299 | Known: true, | |
| 300 | - | TrueColor: true, | |
| 301 | 300 | } | |
| 302 | 301 | styles := themeui.NewStyles(theme) | |
| 303 | 302 | require.False(t, styles.TabTitle.Equal(styles.StatusBar), "sanity: muted title style must differ from the base style on a truecolor theme") | |
| @@ -445,7 +444,6 @@ func TestStatusCompositionUsesTruecolorTheme(t *testing.T) { | |||
| 445 | 444 | Background: renderer.RGB{R: 10, G: 20, B: 30}, | |
| 446 | 445 | HasFG: true, | |
| 447 | 446 | HasBG: true, | |
| 448 | - | TrueColor: true, | |
| 449 | 447 | Known: true, | |
| 450 | 448 | }) | |
| 451 | 449 | ||
| @@ -523,7 +521,7 @@ func TestApplyThemeForcedBuiltinThemePropagatesToChromeAndPanes(t *testing.T) { | |||
| 523 | 521 | d.applyTheme(sess, ac, protocol.Theme{ | |
| 524 | 522 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | |
| 525 | 523 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | |
| 526 | - | TrueColor: true, SchemeKnown: true, Light: !tc.want.Light, | |
| 524 | + | SchemeKnown: true, Light: !tc.want.Light, | |
| 527 | 525 | }) | |
| 528 | 526 | ||
| 529 | 527 | require.Equal(t, tc.want, ac.getAppliedTheme().Raw) | |
| @@ -578,7 +576,7 @@ func TestAutoThemeDetachClearsPaneColorScheme(t *testing.T) { | |||
| 578 | 576 | d.applyTheme(sess, ac, protocol.Theme{ | |
| 579 | 577 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | |
| 580 | 578 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | |
| 581 | - | TrueColor: true, SchemeKnown: true, Light: true, | |
| 579 | + | SchemeKnown: true, Light: true, | |
| 582 | 580 | }) | |
| 583 | 581 | assertSessionColorScheme(t, sess, true) | |
| 584 | 582 | ||
| @@ -595,7 +593,7 @@ func TestAttachClientClearsStaleColorSchemeOnReplacement(t *testing.T) { | |||
| 595 | 593 | d.applyTheme(sess, ac, protocol.Theme{ | |
| 596 | 594 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | |
| 597 | 595 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | |
| 598 | - | TrueColor: true, SchemeKnown: true, Light: true, | |
| 596 | + | SchemeKnown: true, Light: true, | |
| 599 | 597 | }) | |
| 600 | 598 | assertSessionColorScheme(t, sess, true) | |
| 601 | 599 | ||
| @@ -614,7 +612,7 @@ func TestForcedThemeDetachPreservesBuiltinOnPanes(t *testing.T) { | |||
| 614 | 612 | d.applyTheme(sess, ac, protocol.Theme{ | |
| 615 | 613 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | |
| 616 | 614 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | |
| 617 | - | TrueColor: true, SchemeKnown: true, Light: true, | |
| 615 | + | SchemeKnown: true, Light: true, | |
| 618 | 616 | }) | |
| 619 | 617 | ||
| 620 | 618 | d.clientGone(sess, ac, ac.transport(), true) | |
| @@ -631,12 +629,11 @@ func TestApplyThemeAutoUnknownDoesNotClobberPaneColorScheme(t *testing.T) { | |||
| 631 | 629 | d.applyTheme(sess, ac, protocol.Theme{ | |
| 632 | 630 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | |
| 633 | 631 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | |
| 634 | - | TrueColor: true, SchemeKnown: true, Light: true, | |
| 632 | + | SchemeKnown: true, Light: true, | |
| 635 | 633 | }) | |
| 636 | 634 | d.applyTheme(sess, ac, protocol.Theme{ | |
| 637 | 635 | HasForeground: true, Foreground: renderer.RGB{R: 7, G: 8, B: 9}, | |
| 638 | 636 | HasBackground: true, Background: renderer.RGB{R: 10, G: 11, B: 12}, | |
| 639 | - | TrueColor: true, | |
| 640 | 637 | }) | |
| 641 | 638 | ||
| 642 | 639 | assertSessionDefaultColors(t, sess, renderer.RGB{R: 7, G: 8, B: 9}, renderer.RGB{R: 10, G: 11, B: 12}) | |
| @@ -1094,7 +1091,7 @@ func TestStatusBarRendersMRUNamesAndInlineBell(t *testing.T) { | |||
| 1094 | 1091 | } | |
| 1095 | 1092 | ||
| 1096 | 1093 | func TestStatusBarCurrentSessionUsesAccentStyle(t *testing.T) { | |
| 1097 | - | theme := themeui.Theme{Foreground: renderer.RGB{R: 220, G: 220, B: 220}, Background: renderer.RGB{R: 10, G: 10, B: 10}, HasFG: true, HasBG: true, TrueColor: true, Known: true} | |
| 1094 | + | theme := themeui.Theme{Foreground: renderer.RGB{R: 220, G: 220, B: 220}, Background: renderer.RGB{R: 10, G: 10, B: 10}, HasFG: true, HasBG: true, Known: true} | |
| 1098 | 1095 | styles := themeui.NewStyles(theme) | |
| 1099 | 1096 | row := make([]renderer.Cell, 16) | |
| 1100 | 1097 | ||
| @@ -1141,7 +1138,7 @@ func TestStatusBarContextualRanksPreserveOriginalRanksAndSelectedAccent(t *testi | |||
| 1141 | 1138 | } | |
| 1142 | 1139 | ||
| 1143 | 1140 | func TestStatusBarMRUGradientTruecolorAndPlainFallback(t *testing.T) { | |
| 1144 | - | theme := themeui.Theme{Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, HasFG: true, HasBG: true, TrueColor: true, Known: true, UsePalette: true} | |
| 1141 | + | theme := themeui.Theme{Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, HasFG: true, HasBG: true, Known: true, UsePalette: true} | |
| 1145 | 1142 | theme.Palette[2] = renderer.RGB{R: 0x7d, G: 0xb5, B: 0xb5} | |
| 1146 | 1143 | theme.Palette[10] = theme.Palette[2] | |
| 1147 | 1144 | theme.PaletteKnown = 1<<2 | 1<<10 | |
| @@ -1344,7 +1341,6 @@ func TestStatusBarsUseCompleteSemanticSurfaces(t *testing.T) { | |||
| 1344 | 1341 | Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, | |
| 1345 | 1342 | HasFG: true, | |
| 1346 | 1343 | HasBG: true, | |
| 1347 | - | TrueColor: true, | |
| 1348 | 1344 | Known: true, | |
| 1349 | 1345 | UsePalette: true, | |
| 1350 | 1346 | } | |
| @@ -297,7 +297,6 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) { | |||||
| 297 | HasFG: true, | 297 | HasFG: true, | ||
| 298 | HasBG: true, | 298 | HasBG: true, | ||
| 299 | Known: true, | 299 | Known: true, | ||
| 300 | - | TrueColor: true, | |||
| 301 | } | 300 | } | ||
| 302 | styles := themeui.NewStyles(theme) | 301 | styles := themeui.NewStyles(theme) | ||
| 303 | require.False(t, styles.TabTitle.Equal(styles.StatusBar), "sanity: muted title style must differ from the base style on a truecolor theme") | 302 | require.False(t, styles.TabTitle.Equal(styles.StatusBar), "sanity: muted title style must differ from the base style on a truecolor theme") | ||
| @@ -445,7 +444,6 @@ func TestStatusCompositionUsesTruecolorTheme(t *testing.T) { | |||||
| 445 | Background: renderer.RGB{R: 10, G: 20, B: 30}, | 444 | Background: renderer.RGB{R: 10, G: 20, B: 30}, | ||
| 446 | HasFG: true, | 445 | HasFG: true, | ||
| 447 | HasBG: true, | 446 | HasBG: true, | ||
| 448 | - | TrueColor: true, | |||
| 449 | Known: true, | 447 | Known: true, | ||
| 450 | }) | 448 | }) | ||
| 451 | 449 | ||||
| @@ -523,7 +521,7 @@ func TestApplyThemeForcedBuiltinThemePropagatesToChromeAndPanes(t *testing.T) { | |||||
| 523 | d.applyTheme(sess, ac, protocol.Theme{ | 521 | d.applyTheme(sess, ac, protocol.Theme{ | ||
| 524 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | 522 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | ||
| 525 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | 523 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | ||
| 526 | - | TrueColor: true, SchemeKnown: true, Light: !tc.want.Light, | 524 | + | SchemeKnown: true, Light: !tc.want.Light, |
| 527 | }) | 525 | }) | ||
| 528 | 526 | ||||
| 529 | require.Equal(t, tc.want, ac.getAppliedTheme().Raw) | 527 | require.Equal(t, tc.want, ac.getAppliedTheme().Raw) | ||
| @@ -578,7 +576,7 @@ func TestAutoThemeDetachClearsPaneColorScheme(t *testing.T) { | |||||
| 578 | d.applyTheme(sess, ac, protocol.Theme{ | 576 | d.applyTheme(sess, ac, protocol.Theme{ | ||
| 579 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | 577 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | ||
| 580 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | 578 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | ||
| 581 | - | TrueColor: true, SchemeKnown: true, Light: true, | 579 | + | SchemeKnown: true, Light: true, |
| 582 | }) | 580 | }) | ||
| 583 | assertSessionColorScheme(t, sess, true) | 581 | assertSessionColorScheme(t, sess, true) | ||
| 584 | 582 | ||||
| @@ -595,7 +593,7 @@ func TestAttachClientClearsStaleColorSchemeOnReplacement(t *testing.T) { | |||||
| 595 | d.applyTheme(sess, ac, protocol.Theme{ | 593 | d.applyTheme(sess, ac, protocol.Theme{ | ||
| 596 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | 594 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | ||
| 597 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | 595 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | ||
| 598 | - | TrueColor: true, SchemeKnown: true, Light: true, | 596 | + | SchemeKnown: true, Light: true, |
| 599 | }) | 597 | }) | ||
| 600 | assertSessionColorScheme(t, sess, true) | 598 | assertSessionColorScheme(t, sess, true) | ||
| 601 | 599 | ||||
| @@ -614,7 +612,7 @@ func TestForcedThemeDetachPreservesBuiltinOnPanes(t *testing.T) { | |||||
| 614 | d.applyTheme(sess, ac, protocol.Theme{ | 612 | d.applyTheme(sess, ac, protocol.Theme{ | ||
| 615 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | 613 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | ||
| 616 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | 614 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | ||
| 617 | - | TrueColor: true, SchemeKnown: true, Light: true, | 615 | + | SchemeKnown: true, Light: true, |
| 618 | }) | 616 | }) | ||
| 619 | 617 | ||||
| 620 | d.clientGone(sess, ac, ac.transport(), true) | 618 | d.clientGone(sess, ac, ac.transport(), true) | ||
| @@ -631,12 +629,11 @@ func TestApplyThemeAutoUnknownDoesNotClobberPaneColorScheme(t *testing.T) { | |||||
| 631 | d.applyTheme(sess, ac, protocol.Theme{ | 629 | d.applyTheme(sess, ac, protocol.Theme{ | ||
| 632 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | 630 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | ||
| 633 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | 631 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | ||
| 634 | - | TrueColor: true, SchemeKnown: true, Light: true, | 632 | + | SchemeKnown: true, Light: true, |
| 635 | }) | 633 | }) | ||
| 636 | d.applyTheme(sess, ac, protocol.Theme{ | 634 | d.applyTheme(sess, ac, protocol.Theme{ | ||
| 637 | HasForeground: true, Foreground: renderer.RGB{R: 7, G: 8, B: 9}, | 635 | HasForeground: true, Foreground: renderer.RGB{R: 7, G: 8, B: 9}, | ||
| 638 | HasBackground: true, Background: renderer.RGB{R: 10, G: 11, B: 12}, | 636 | HasBackground: true, Background: renderer.RGB{R: 10, G: 11, B: 12}, | ||
| 639 | - | TrueColor: true, | |||
| 640 | }) | 637 | }) | ||
| 641 | 638 | ||||
| 642 | assertSessionDefaultColors(t, sess, renderer.RGB{R: 7, G: 8, B: 9}, renderer.RGB{R: 10, G: 11, B: 12}) | 639 | assertSessionDefaultColors(t, sess, renderer.RGB{R: 7, G: 8, B: 9}, renderer.RGB{R: 10, G: 11, B: 12}) | ||
| @@ -1094,7 +1091,7 @@ func TestStatusBarRendersMRUNamesAndInlineBell(t *testing.T) { | |||||
| 1094 | } | 1091 | } | ||
| 1095 | 1092 | ||||
| 1096 | func TestStatusBarCurrentSessionUsesAccentStyle(t *testing.T) { | 1093 | func TestStatusBarCurrentSessionUsesAccentStyle(t *testing.T) { | ||
| 1097 | - | theme := themeui.Theme{Foreground: renderer.RGB{R: 220, G: 220, B: 220}, Background: renderer.RGB{R: 10, G: 10, B: 10}, HasFG: true, HasBG: true, TrueColor: true, Known: true} | 1094 | + | theme := themeui.Theme{Foreground: renderer.RGB{R: 220, G: 220, B: 220}, Background: renderer.RGB{R: 10, G: 10, B: 10}, HasFG: true, HasBG: true, Known: true} |
| 1098 | styles := themeui.NewStyles(theme) | 1095 | styles := themeui.NewStyles(theme) | ||
| 1099 | row := make([]renderer.Cell, 16) | 1096 | row := make([]renderer.Cell, 16) | ||
| 1100 | 1097 | ||||
| @@ -1141,7 +1138,7 @@ func TestStatusBarContextualRanksPreserveOriginalRanksAndSelectedAccent(t *testi | |||||
| 1141 | } | 1138 | } | ||
| 1142 | 1139 | ||||
| 1143 | func TestStatusBarMRUGradientTruecolorAndPlainFallback(t *testing.T) { | 1140 | func TestStatusBarMRUGradientTruecolorAndPlainFallback(t *testing.T) { | ||
| 1144 | - | theme := themeui.Theme{Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, HasFG: true, HasBG: true, TrueColor: true, Known: true, UsePalette: true} | 1141 | + | theme := themeui.Theme{Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, HasFG: true, HasBG: true, Known: true, UsePalette: true} |
| 1145 | theme.Palette[2] = renderer.RGB{R: 0x7d, G: 0xb5, B: 0xb5} | 1142 | theme.Palette[2] = renderer.RGB{R: 0x7d, G: 0xb5, B: 0xb5} | ||
| 1146 | theme.Palette[10] = theme.Palette[2] | 1143 | theme.Palette[10] = theme.Palette[2] | ||
| 1147 | theme.PaletteKnown = 1<<2 | 1<<10 | 1144 | theme.PaletteKnown = 1<<2 | 1<<10 | ||
| @@ -1344,7 +1341,6 @@ func TestStatusBarsUseCompleteSemanticSurfaces(t *testing.T) { | |||||
| 1344 | Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, | 1341 | Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, | ||
| 1345 | HasFG: true, | 1342 | HasFG: true, | ||
| 1346 | HasBG: true, | 1343 | HasBG: true, | ||
| 1347 | - | TrueColor: true, | |||
| 1348 | Known: true, | 1344 | Known: true, | ||
| 1349 | UsePalette: true, | 1345 | UsePalette: true, | ||
| 1350 | } | 1346 | } | ||
M internal/usecase/daemon/theme_cache_test.go +2 -2
| @@ -18,7 +18,7 @@ func cacheTheme() themeui.Theme { | |||
| 18 | 18 | palette[10] = palette[2] | |
| 19 | 19 | return themeui.Theme{ | |
| 20 | 20 | Foreground: renderer.RGB{R: 230, G: 230, B: 230}, Background: renderer.RGB{R: 8, G: 9, B: 10}, | |
| 21 | - | HasFG: true, HasBG: true, Known: true, TrueColor: true, UsePalette: true, | |
| 21 | + | HasFG: true, HasBG: true, Known: true, UsePalette: true, | |
| 22 | 22 | Palette: palette, PaletteKnown: 1<<2 | 1<<10, | |
| 23 | 23 | } | |
| 24 | 24 | } | |
| @@ -26,7 +26,7 @@ func cacheTheme() themeui.Theme { | |||
| 26 | 26 | func TestThemeAccentHotReloadRebuildsAppliedSnapshot(t *testing.T) { | |
| 27 | 27 | d, sess, ac, _ := newManualSessionWithPTYs(t) | |
| 28 | 28 | raw := cacheTheme() | |
| 29 | - | d.applyTheme(sess, ac, protocol.Theme{Foreground: raw.Foreground, Background: raw.Background, HasForeground: true, HasBackground: true, TrueColor: true, Palette: raw.Palette, PaletteKnown: raw.PaletteKnown}) | |
| 29 | + | d.applyTheme(sess, ac, protocol.Theme{Foreground: raw.Foreground, Background: raw.Background, HasForeground: true, HasBackground: true, Palette: raw.Palette, PaletteKnown: raw.PaletteKnown}) | |
| 30 | 30 | before := ac.getAppliedTheme() | |
| 31 | 31 | ||
| 32 | 32 | cfg := domain.Defaults() | |
| @@ -18,7 +18,7 @@ func cacheTheme() themeui.Theme { | |||||
| 18 | palette[10] = palette[2] | 18 | palette[10] = palette[2] | ||
| 19 | return themeui.Theme{ | 19 | return themeui.Theme{ | ||
| 20 | Foreground: renderer.RGB{R: 230, G: 230, B: 230}, Background: renderer.RGB{R: 8, G: 9, B: 10}, | 20 | Foreground: renderer.RGB{R: 230, G: 230, B: 230}, Background: renderer.RGB{R: 8, G: 9, B: 10}, | ||
| 21 | - | HasFG: true, HasBG: true, Known: true, TrueColor: true, UsePalette: true, | 21 | + | HasFG: true, HasBG: true, Known: true, UsePalette: true, |
| 22 | Palette: palette, PaletteKnown: 1<<2 | 1<<10, | 22 | Palette: palette, PaletteKnown: 1<<2 | 1<<10, | ||
| 23 | } | 23 | } | ||
| 24 | } | 24 | } | ||
| @@ -26,7 +26,7 @@ func cacheTheme() themeui.Theme { | |||||
| 26 | func TestThemeAccentHotReloadRebuildsAppliedSnapshot(t *testing.T) { | 26 | func TestThemeAccentHotReloadRebuildsAppliedSnapshot(t *testing.T) { | ||
| 27 | d, sess, ac, _ := newManualSessionWithPTYs(t) | 27 | d, sess, ac, _ := newManualSessionWithPTYs(t) | ||
| 28 | raw := cacheTheme() | 28 | raw := cacheTheme() | ||
| 29 | - | d.applyTheme(sess, ac, protocol.Theme{Foreground: raw.Foreground, Background: raw.Background, HasForeground: true, HasBackground: true, TrueColor: true, Palette: raw.Palette, PaletteKnown: raw.PaletteKnown}) | 29 | + | d.applyTheme(sess, ac, protocol.Theme{Foreground: raw.Foreground, Background: raw.Background, HasForeground: true, HasBackground: true, Palette: raw.Palette, PaletteKnown: raw.PaletteKnown}) |
| 30 | before := ac.getAppliedTheme() | 30 | before := ac.getAppliedTheme() | ||
| 31 | 31 | ||||
| 32 | cfg := domain.Defaults() | 32 | cfg := domain.Defaults() | ||
M internal/usecase/daemon/theme_integration_test.go +2 -4
| @@ -14,8 +14,7 @@ import ( | |||
| 14 | 14 | func lifecycleTheme(foreground, background renderer.RGB, palette map[uint8]renderer.RGB) protocol.Theme { | |
| 15 | 15 | out := protocol.Theme{ | |
| 16 | 16 | Foreground: foreground, Background: background, | |
| 17 | - | HasForeground: true, HasBackground: true, TrueColor: true, | |
| 18 | - | SchemeKnown: true, | |
| 17 | + | HasForeground: true, HasBackground: true, SchemeKnown: true, | |
| 19 | 18 | } | |
| 20 | 19 | for slot, color := range palette { | |
| 21 | 20 | out.Palette[slot] = color | |
| @@ -175,8 +174,7 @@ func TestAccentLifecycleNeutralChromeByteSnapshots(t *testing.T) { | |||
| 175 | 174 | d.ApplyConfig(tt.config) | |
| 176 | 175 | got := chromeStyleSnapshot(t, d.resolveAppliedTheme(themeui.Theme{ | |
| 177 | 176 | Foreground: raw.Foreground, Background: raw.Background, Palette: raw.Palette, PaletteKnown: raw.PaletteKnown, | |
| 178 | - | HasFG: true, HasBG: true, TrueColor: true, | |
| 179 | - | }).Resolved.Styles) | |
| 177 | + | HasFG: true, HasBG: true}).Resolved.Styles) | |
| 180 | 178 | require.Equal(t, []byte(tt.want), got) | |
| 181 | 179 | }) | |
| 182 | 180 | } | |
| @@ -14,8 +14,7 @@ import ( | |||||
| 14 | func lifecycleTheme(foreground, background renderer.RGB, palette map[uint8]renderer.RGB) protocol.Theme { | 14 | func lifecycleTheme(foreground, background renderer.RGB, palette map[uint8]renderer.RGB) protocol.Theme { | ||
| 15 | out := protocol.Theme{ | 15 | out := protocol.Theme{ | ||
| 16 | Foreground: foreground, Background: background, | 16 | Foreground: foreground, Background: background, | ||
| 17 | - | HasForeground: true, HasBackground: true, TrueColor: true, | 17 | + | HasForeground: true, HasBackground: true, SchemeKnown: true, |
| 18 | - | SchemeKnown: true, | |||
| 19 | } | 18 | } | ||
| 20 | for slot, color := range palette { | 19 | for slot, color := range palette { | ||
| 21 | out.Palette[slot] = color | 20 | out.Palette[slot] = color | ||
| @@ -175,8 +174,7 @@ func TestAccentLifecycleNeutralChromeByteSnapshots(t *testing.T) { | |||||
| 175 | d.ApplyConfig(tt.config) | 174 | d.ApplyConfig(tt.config) | ||
| 176 | got := chromeStyleSnapshot(t, d.resolveAppliedTheme(themeui.Theme{ | 175 | got := chromeStyleSnapshot(t, d.resolveAppliedTheme(themeui.Theme{ | ||
| 177 | Foreground: raw.Foreground, Background: raw.Background, Palette: raw.Palette, PaletteKnown: raw.PaletteKnown, | 176 | Foreground: raw.Foreground, Background: raw.Background, Palette: raw.Palette, PaletteKnown: raw.PaletteKnown, | ||
| 178 | - | HasFG: true, HasBG: true, TrueColor: true, | 177 | + | HasFG: true, HasBG: true}).Resolved.Styles) |
| 179 | - | }).Resolved.Styles) | |||
| 180 | require.Equal(t, []byte(tt.want), got) | 178 | require.Equal(t, []byte(tt.want), got) | ||
| 181 | }) | 179 | }) | ||
| 182 | } | 180 | } | ||
M internal/usecase/palette/model_test.go +1 -2
| @@ -503,11 +503,10 @@ func TestPaletteDescriptionKeepsInactiveRowSurfaceAcrossFallbacks(t *testing.T) | |||
| 503 | 503 | paletteColors[10] = paletteColors[2] | |
| 504 | 504 | accentTheme := themeui.Theme{ | |
| 505 | 505 | Foreground: renderer.RGB{R: 230, G: 230, B: 230}, Background: renderer.RGB{R: 8, G: 9, B: 10}, | |
| 506 | - | HasFG: true, HasBG: true, Known: true, TrueColor: true, UsePalette: true, | |
| 506 | + | HasFG: true, HasBG: true, Known: true, UsePalette: true, | |
| 507 | 507 | Palette: paletteColors, PaletteKnown: 1<<2 | 1<<10, | |
| 508 | 508 | } | |
| 509 | 509 | indexedTheme := accentTheme | |
| 510 | - | indexedTheme.TrueColor = false | |
| 511 | 510 | paletteOffTheme := accentTheme | |
| 512 | 511 | paletteOffTheme.UsePalette = false | |
| 513 | 512 | neutralTheme := paletteOffTheme | |
| @@ -503,11 +503,10 @@ func TestPaletteDescriptionKeepsInactiveRowSurfaceAcrossFallbacks(t *testing.T) | |||||
| 503 | paletteColors[10] = paletteColors[2] | 503 | paletteColors[10] = paletteColors[2] | ||
| 504 | accentTheme := themeui.Theme{ | 504 | accentTheme := themeui.Theme{ | ||
| 505 | Foreground: renderer.RGB{R: 230, G: 230, B: 230}, Background: renderer.RGB{R: 8, G: 9, B: 10}, | 505 | Foreground: renderer.RGB{R: 230, G: 230, B: 230}, Background: renderer.RGB{R: 8, G: 9, B: 10}, | ||
| 506 | - | HasFG: true, HasBG: true, Known: true, TrueColor: true, UsePalette: true, | 506 | + | HasFG: true, HasBG: true, Known: true, UsePalette: true, |
| 507 | Palette: paletteColors, PaletteKnown: 1<<2 | 1<<10, | 507 | Palette: paletteColors, PaletteKnown: 1<<2 | 1<<10, | ||
| 508 | } | 508 | } | ||
| 509 | indexedTheme := accentTheme | 509 | indexedTheme := accentTheme | ||
| 510 | - | indexedTheme.TrueColor = false | |||
| 511 | paletteOffTheme := accentTheme | 510 | paletteOffTheme := accentTheme | ||
| 512 | paletteOffTheme.UsePalette = false | 511 | paletteOffTheme.UsePalette = false | ||
| 513 | neutralTheme := paletteOffTheme | 512 | neutralTheme := paletteOffTheme | ||
M internal/usecase/theme/accent_resolve_test.go +1 -3
| @@ -131,7 +131,6 @@ func TestResolveAccentAuto(t *testing.T) { | |||
| 131 | 131 | name: "known colors remain semantic without truecolor output", | |
| 132 | 132 | theme: func() Theme { | |
| 133 | 133 | t := paletteTheme(map[int]renderer.RGB{2: teal, 10: teal}) | |
| 134 | - | t.TrueColor = false | |
| 135 | 134 | return t | |
| 136 | 135 | }(), | |
| 137 | 136 | want: Accent{RGB: teal, Slot: 2, Known: true}, | |
| @@ -206,7 +205,7 @@ func TestResolveAccentExplicitSlot(t *testing.T) { | |||
| 206 | 205 | }, | |
| 207 | 206 | { | |
| 208 | 207 | name: "explicit known slot remains semantic without truecolor output", | |
| 209 | - | theme: func() Theme { t := paletteTheme(map[int]renderer.RGB{3: teal}); t.TrueColor = false; return t }(), | |
| 208 | + | theme: paletteTheme(map[int]renderer.RGB{3: teal}), | |
| 210 | 209 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 3}, | |
| 211 | 210 | want: Accent{RGB: teal, Slot: 3, Known: true}, | |
| 212 | 211 | }, | |
| @@ -273,7 +272,6 @@ func paletteTheme(colors map[int]renderer.RGB) Theme { | |||
| 273 | 272 | Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, | |
| 274 | 273 | HasFG: true, | |
| 275 | 274 | HasBG: true, | |
| 276 | - | TrueColor: true, | |
| 277 | 275 | Known: true, | |
| 278 | 276 | UsePalette: true, | |
| 279 | 277 | } | |
| @@ -131,7 +131,6 @@ func TestResolveAccentAuto(t *testing.T) { | |||||
| 131 | name: "known colors remain semantic without truecolor output", | 131 | name: "known colors remain semantic without truecolor output", | ||
| 132 | theme: func() Theme { | 132 | theme: func() Theme { | ||
| 133 | t := paletteTheme(map[int]renderer.RGB{2: teal, 10: teal}) | 133 | t := paletteTheme(map[int]renderer.RGB{2: teal, 10: teal}) | ||
| 134 | - | t.TrueColor = false | |||
| 135 | return t | 134 | return t | ||
| 136 | }(), | 135 | }(), | ||
| 137 | want: Accent{RGB: teal, Slot: 2, Known: true}, | 136 | want: Accent{RGB: teal, Slot: 2, Known: true}, | ||
| @@ -206,7 +205,7 @@ func TestResolveAccentExplicitSlot(t *testing.T) { | |||||
| 206 | }, | 205 | }, | ||
| 207 | { | 206 | { | ||
| 208 | name: "explicit known slot remains semantic without truecolor output", | 207 | name: "explicit known slot remains semantic without truecolor output", | ||
| 209 | - | theme: func() Theme { t := paletteTheme(map[int]renderer.RGB{3: teal}); t.TrueColor = false; return t }(), | 208 | + | theme: paletteTheme(map[int]renderer.RGB{3: teal}), |
| 210 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 3}, | 209 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 3}, | ||
| 211 | want: Accent{RGB: teal, Slot: 3, Known: true}, | 210 | want: Accent{RGB: teal, Slot: 3, Known: true}, | ||
| 212 | }, | 211 | }, | ||
| @@ -273,7 +272,6 @@ func paletteTheme(colors map[int]renderer.RGB) Theme { | |||||
| 273 | Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, | 272 | Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, | ||
| 274 | HasFG: true, | 273 | HasFG: true, | ||
| 275 | HasBG: true, | 274 | HasBG: true, | ||
| 276 | - | TrueColor: true, | |||
| 277 | Known: true, | 275 | Known: true, | ||
| 278 | UsePalette: true, | 276 | UsePalette: true, | ||
| 279 | } | 277 | } | ||
M internal/usecase/theme/benchmark_test.go +0 -1
| @@ -26,7 +26,6 @@ func benchmarkPaletteTheme() Theme { | |||
| 26 | 26 | PaletteKnown: 1<<2 | 1<<4 | 1<<10 | 1<<12 | 1<<14, | |
| 27 | 27 | HasFG: true, | |
| 28 | 28 | HasBG: true, | |
| 29 | - | TrueColor: true, | |
| 30 | 29 | Known: true, | |
| 31 | 30 | SchemeKnown: true, | |
| 32 | 31 | UsePalette: true, | |
| @@ -26,7 +26,6 @@ func benchmarkPaletteTheme() Theme { | |||||
| 26 | PaletteKnown: 1<<2 | 1<<4 | 1<<10 | 1<<12 | 1<<14, | 26 | PaletteKnown: 1<<2 | 1<<4 | 1<<10 | 1<<12 | 1<<14, | ||
| 27 | HasFG: true, | 27 | HasFG: true, | ||
| 28 | HasBG: true, | 28 | HasBG: true, | ||
| 29 | - | TrueColor: true, | |||
| 30 | Known: true, | 29 | Known: true, | ||
| 31 | SchemeKnown: true, | 30 | SchemeKnown: true, | ||
| 32 | UsePalette: true, | 31 | UsePalette: true, | ||
M internal/usecase/theme/neutral_history_test.go +1 -1
| @@ -8,7 +8,7 @@ import ( | |||
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | 10 | func TestNeutralHistoryGradient(t *testing.T) { | |
| 11 | - | theme := Theme{Foreground: renderer.RGB{R: 216, G: 216, B: 216}, Background: renderer.RGB{R: 16, G: 16, B: 16}, HasFG: true, HasBG: true, Known: true, TrueColor: true} | |
| 11 | + | theme := Theme{Foreground: renderer.RGB{R: 216, G: 216, B: 216}, Background: renderer.RGB{R: 16, G: 16, B: 16}, HasFG: true, HasBG: true, Known: true} | |
| 12 | 12 | styles := NewStyles(theme) | |
| 13 | 13 | require.Equal(t, theme.Foreground, styles.TabActive.BackgroundRGB) | |
| 14 | 14 | for count := 1; count <= 9; count++ { | |
| @@ -8,7 +8,7 @@ import ( | |||||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| 10 | func TestNeutralHistoryGradient(t *testing.T) { | 10 | func TestNeutralHistoryGradient(t *testing.T) { | ||
| 11 | - | theme := Theme{Foreground: renderer.RGB{R: 216, G: 216, B: 216}, Background: renderer.RGB{R: 16, G: 16, B: 16}, HasFG: true, HasBG: true, Known: true, TrueColor: true} | 11 | + | theme := Theme{Foreground: renderer.RGB{R: 216, G: 216, B: 216}, Background: renderer.RGB{R: 16, G: 16, B: 16}, HasFG: true, HasBG: true, Known: true} |
| 12 | styles := NewStyles(theme) | 12 | styles := NewStyles(theme) | ||
| 13 | require.Equal(t, theme.Foreground, styles.TabActive.BackgroundRGB) | 13 | require.Equal(t, theme.Foreground, styles.TabActive.BackgroundRGB) | ||
| 14 | for count := 1; count <= 9; count++ { | 14 | for count := 1; count <= 9; count++ { | ||
M internal/usecase/theme/phase3_review_test.go +3 -4
| @@ -27,7 +27,6 @@ func TestResolveAccentIndexedFallbackTruthTable(t *testing.T) { | |||
| 27 | 27 | name: "explicit known slot without truecolor remains semantic", | |
| 28 | 28 | theme: func() Theme { | |
| 29 | 29 | t := paletteTheme(map[int]renderer.RGB{5: teal}) | |
| 30 | - | t.TrueColor = false | |
| 31 | 30 | return t | |
| 32 | 31 | }(), | |
| 33 | 32 | policy: explicit, | |
| @@ -37,7 +36,7 @@ func TestResolveAccentIndexedFallbackTruthTable(t *testing.T) { | |||
| 37 | 36 | name: "automatic absent osc palette keeps dark scheme indexed blue fallback", | |
| 38 | 37 | theme: Theme{ | |
| 39 | 38 | Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, Background: renderer.RGB{R: 8, G: 9, B: 10}, | |
| 40 | - | HasFG: true, HasBG: true, TrueColor: true, Known: true, SchemeKnown: true, UsePalette: true, | |
| 39 | + | HasFG: true, HasBG: true, Known: true, SchemeKnown: true, UsePalette: true, | |
| 41 | 40 | }, | |
| 42 | 41 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentAuto}, | |
| 43 | 42 | want: Accent{Slot: 12, IndexedOnly: true}, | |
| @@ -45,14 +44,14 @@ func TestResolveAccentIndexedFallbackTruthTable(t *testing.T) { | |||
| 45 | 44 | { | |
| 46 | 45 | name: "automatic absent defaults keeps light scheme indexed blue fallback", | |
| 47 | 46 | theme: Theme{ | |
| 48 | - | TrueColor: true, Known: true, SchemeKnown: true, Light: true, UsePalette: true, | |
| 47 | + | Known: true, SchemeKnown: true, Light: true, UsePalette: true, | |
| 49 | 48 | }, | |
| 50 | 49 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentAuto}, | |
| 51 | 50 | want: Accent{Slot: 4, IndexedOnly: true}, | |
| 52 | 51 | }, | |
| 53 | 52 | { | |
| 54 | 53 | name: "automatic absent palette and unknown scheme stays neutral", | |
| 55 | - | theme: Theme{TrueColor: true, Known: true, UsePalette: true}, | |
| 54 | + | theme: Theme{Known: true, UsePalette: true}, | |
| 56 | 55 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentAuto}, | |
| 57 | 56 | want: Accent{}, | |
| 58 | 57 | }, | |
| @@ -27,7 +27,6 @@ func TestResolveAccentIndexedFallbackTruthTable(t *testing.T) { | |||||
| 27 | name: "explicit known slot without truecolor remains semantic", | 27 | name: "explicit known slot without truecolor remains semantic", | ||
| 28 | theme: func() Theme { | 28 | theme: func() Theme { | ||
| 29 | t := paletteTheme(map[int]renderer.RGB{5: teal}) | 29 | t := paletteTheme(map[int]renderer.RGB{5: teal}) | ||
| 30 | - | t.TrueColor = false | |||
| 31 | return t | 30 | return t | ||
| 32 | }(), | 31 | }(), | ||
| 33 | policy: explicit, | 32 | policy: explicit, | ||
| @@ -37,7 +36,7 @@ func TestResolveAccentIndexedFallbackTruthTable(t *testing.T) { | |||||
| 37 | name: "automatic absent osc palette keeps dark scheme indexed blue fallback", | 36 | name: "automatic absent osc palette keeps dark scheme indexed blue fallback", | ||
| 38 | theme: Theme{ | 37 | theme: Theme{ | ||
| 39 | Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, Background: renderer.RGB{R: 8, G: 9, B: 10}, | 38 | Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, Background: renderer.RGB{R: 8, G: 9, B: 10}, | ||
| 40 | - | HasFG: true, HasBG: true, TrueColor: true, Known: true, SchemeKnown: true, UsePalette: true, | 39 | + | HasFG: true, HasBG: true, Known: true, SchemeKnown: true, UsePalette: true, |
| 41 | }, | 40 | }, | ||
| 42 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentAuto}, | 41 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentAuto}, | ||
| 43 | want: Accent{Slot: 12, IndexedOnly: true}, | 42 | want: Accent{Slot: 12, IndexedOnly: true}, | ||
| @@ -45,14 +44,14 @@ func TestResolveAccentIndexedFallbackTruthTable(t *testing.T) { | |||||
| 45 | { | 44 | { | ||
| 46 | name: "automatic absent defaults keeps light scheme indexed blue fallback", | 45 | name: "automatic absent defaults keeps light scheme indexed blue fallback", | ||
| 47 | theme: Theme{ | 46 | theme: Theme{ | ||
| 48 | - | TrueColor: true, Known: true, SchemeKnown: true, Light: true, UsePalette: true, | 47 | + | Known: true, SchemeKnown: true, Light: true, UsePalette: true, |
| 49 | }, | 48 | }, | ||
| 50 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentAuto}, | 49 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentAuto}, | ||
| 51 | want: Accent{Slot: 4, IndexedOnly: true}, | 50 | want: Accent{Slot: 4, IndexedOnly: true}, | ||
| 52 | }, | 51 | }, | ||
| 53 | { | 52 | { | ||
| 54 | name: "automatic absent palette and unknown scheme stays neutral", | 53 | name: "automatic absent palette and unknown scheme stays neutral", | ||
| 55 | - | theme: Theme{TrueColor: true, Known: true, UsePalette: true}, | 54 | + | theme: Theme{Known: true, UsePalette: true}, |
| 56 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentAuto}, | 55 | policy: domain.ThemeAccent{Mode: domain.ThemeAccentAuto}, | ||
| 57 | want: Accent{}, | 56 | want: Accent{}, | ||
| 58 | }, | 57 | }, | ||
M internal/usecase/theme/ramp_test.go +3 -5
| @@ -54,7 +54,7 @@ func TestBuildRampReducesActiveToHighestSafeWeight(t *testing.T) { | |||
| 54 | 54 | theme := Theme{ | |
| 55 | 55 | Foreground: renderer.RGB{R: 0xe0, G: 0xe0, B: 0xe0}, | |
| 56 | 56 | Background: renderer.RGB{R: 0x59, G: 0x59, B: 0x59}, | |
| 57 | - | HasFG: true, HasBG: true, TrueColor: true, Known: true, UsePalette: true, | |
| 57 | + | HasFG: true, HasBG: true, Known: true, UsePalette: true, | |
| 58 | 58 | } | |
| 59 | 59 | accent := Accent{RGB: renderer.RGB{R: 0x7d, G: 0xb5, B: 0xb5}, Known: true} | |
| 60 | 60 | ramp := BuildRamp(theme, accent) | |
| @@ -155,7 +155,6 @@ func TestResolveBuildsCompleteStylesFromOneAccent(t *testing.T) { | |||
| 155 | 155 | ||
| 156 | 156 | func TestResolveANSI256AndPaletteOffSurfaces(t *testing.T) { | |
| 157 | 157 | theme := rampTheme(false) | |
| 158 | - | theme.TrueColor = false | |
| 159 | 158 | theme.Palette[2] = renderer.RGB{R: 0x7d, G: 0xb5, B: 0xb5} | |
| 160 | 159 | theme.PaletteKnown = 1 << 2 | |
| 161 | 160 | indexed := Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 2}) | |
| @@ -172,7 +171,6 @@ func TestResolveANSI256AndPaletteOffSurfaces(t *testing.T) { | |||
| 172 | 171 | }) | |
| 173 | 172 | } | |
| 174 | 173 | ||
| 175 | - | theme.TrueColor = true | |
| 176 | 174 | theme.UsePalette = false | |
| 177 | 175 | off := Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 2}) | |
| 178 | 176 | require.False(t, off.Accent.Known) | |
| @@ -188,7 +186,7 @@ func TestResolveANSI256AndPaletteOffSurfaces(t *testing.T) { | |||
| 188 | 186 | ||
| 189 | 187 | func rampTheme(light bool) Theme { | |
| 190 | 188 | if light { | |
| 191 | - | return Theme{Foreground: renderer.RGB{R: 0x20, G: 0x20, B: 0x20}, Background: renderer.RGB{R: 0xf8, G: 0xf8, B: 0xf8}, HasFG: true, HasBG: true, TrueColor: true, Known: true, UsePalette: true} | |
| 189 | + | return Theme{Foreground: renderer.RGB{R: 0x20, G: 0x20, B: 0x20}, Background: renderer.RGB{R: 0xf8, G: 0xf8, B: 0xf8}, HasFG: true, HasBG: true, Known: true, UsePalette: true} | |
| 192 | 190 | } | |
| 193 | - | return Theme{Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, HasFG: true, HasBG: true, TrueColor: true, Known: true, UsePalette: true} | |
| 191 | + | return Theme{Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, HasFG: true, HasBG: true, Known: true, UsePalette: true} | |
| 194 | 192 | } | |
| @@ -54,7 +54,7 @@ func TestBuildRampReducesActiveToHighestSafeWeight(t *testing.T) { | |||||
| 54 | theme := Theme{ | 54 | theme := Theme{ | ||
| 55 | Foreground: renderer.RGB{R: 0xe0, G: 0xe0, B: 0xe0}, | 55 | Foreground: renderer.RGB{R: 0xe0, G: 0xe0, B: 0xe0}, | ||
| 56 | Background: renderer.RGB{R: 0x59, G: 0x59, B: 0x59}, | 56 | Background: renderer.RGB{R: 0x59, G: 0x59, B: 0x59}, | ||
| 57 | - | HasFG: true, HasBG: true, TrueColor: true, Known: true, UsePalette: true, | 57 | + | HasFG: true, HasBG: true, Known: true, UsePalette: true, |
| 58 | } | 58 | } | ||
| 59 | accent := Accent{RGB: renderer.RGB{R: 0x7d, G: 0xb5, B: 0xb5}, Known: true} | 59 | accent := Accent{RGB: renderer.RGB{R: 0x7d, G: 0xb5, B: 0xb5}, Known: true} | ||
| 60 | ramp := BuildRamp(theme, accent) | 60 | ramp := BuildRamp(theme, accent) | ||
| @@ -155,7 +155,6 @@ func TestResolveBuildsCompleteStylesFromOneAccent(t *testing.T) { | |||||
| 155 | 155 | ||||
| 156 | func TestResolveANSI256AndPaletteOffSurfaces(t *testing.T) { | 156 | func TestResolveANSI256AndPaletteOffSurfaces(t *testing.T) { | ||
| 157 | theme := rampTheme(false) | 157 | theme := rampTheme(false) | ||
| 158 | - | theme.TrueColor = false | |||
| 159 | theme.Palette[2] = renderer.RGB{R: 0x7d, G: 0xb5, B: 0xb5} | 158 | theme.Palette[2] = renderer.RGB{R: 0x7d, G: 0xb5, B: 0xb5} | ||
| 160 | theme.PaletteKnown = 1 << 2 | 159 | theme.PaletteKnown = 1 << 2 | ||
| 161 | indexed := Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 2}) | 160 | indexed := Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 2}) | ||
| @@ -172,7 +171,6 @@ func TestResolveANSI256AndPaletteOffSurfaces(t *testing.T) { | |||||
| 172 | }) | 171 | }) | ||
| 173 | } | 172 | } | ||
| 174 | 173 | ||||
| 175 | - | theme.TrueColor = true | |||
| 176 | theme.UsePalette = false | 174 | theme.UsePalette = false | ||
| 177 | off := Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 2}) | 175 | off := Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 2}) | ||
| 178 | require.False(t, off.Accent.Known) | 176 | require.False(t, off.Accent.Known) | ||
| @@ -188,7 +186,7 @@ func TestResolveANSI256AndPaletteOffSurfaces(t *testing.T) { | |||||
| 188 | 186 | ||||
| 189 | func rampTheme(light bool) Theme { | 187 | func rampTheme(light bool) Theme { | ||
| 190 | if light { | 188 | if light { | ||
| 191 | - | return Theme{Foreground: renderer.RGB{R: 0x20, G: 0x20, B: 0x20}, Background: renderer.RGB{R: 0xf8, G: 0xf8, B: 0xf8}, HasFG: true, HasBG: true, TrueColor: true, Known: true, UsePalette: true} | 189 | + | return Theme{Foreground: renderer.RGB{R: 0x20, G: 0x20, B: 0x20}, Background: renderer.RGB{R: 0xf8, G: 0xf8, B: 0xf8}, HasFG: true, HasBG: true, Known: true, UsePalette: true} |
| 192 | } | 190 | } | ||
| 193 | - | return Theme{Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, HasFG: true, HasBG: true, TrueColor: true, Known: true, UsePalette: true} | 191 | + | return Theme{Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, Background: renderer.RGB{R: 0x08, G: 0x09, B: 0x0a}, HasFG: true, HasBG: true, Known: true, UsePalette: true} |
| 194 | } | 192 | } | ||
M internal/usecase/theme/theme.go +4 -9
| @@ -19,13 +19,10 @@ type Theme struct { | |||
| 19 | 19 | PaletteKnown uint16 | |
| 20 | 20 | HasFG bool | |
| 21 | 21 | HasBG bool | |
| 22 | - | // TrueColor records output capability for transport compatibility. Semantic | |
| 23 | - | // styles remain RGB; the attachment renderer quantizes them when necessary. | |
| 24 | - | TrueColor bool | |
| 25 | - | Known bool | |
| 26 | - | SchemeKnown bool | |
| 27 | - | Light bool | |
| 28 | - | UsePalette bool | |
| 22 | + | Known bool | |
| 23 | + | SchemeKnown bool | |
| 24 | + | Light bool | |
| 25 | + | UsePalette bool | |
| 29 | 26 | } | |
| 30 | 27 | ||
| 31 | 28 | // PaletteColor returns a palette color only when palette inheritance is | |
| @@ -43,7 +40,6 @@ var ( | |||
| 43 | 40 | Background: renderer.RGB{R: 0x18, G: 0x18, B: 0x18}, | |
| 44 | 41 | HasFG: true, | |
| 45 | 42 | HasBG: true, | |
| 46 | - | TrueColor: true, | |
| 47 | 43 | Known: true, | |
| 48 | 44 | SchemeKnown: true, | |
| 49 | 45 | Light: false, | |
| @@ -53,7 +49,6 @@ var ( | |||
| 53 | 49 | Background: renderer.RGB{R: 0xf8, G: 0xf8, B: 0xf8}, | |
| 54 | 50 | HasFG: true, | |
| 55 | 51 | HasBG: true, | |
| 56 | - | TrueColor: true, | |
| 57 | 52 | Known: true, | |
| 58 | 53 | SchemeKnown: true, | |
| 59 | 54 | Light: true, | |
| @@ -19,13 +19,10 @@ type Theme struct { | |||||
| 19 | PaletteKnown uint16 | 19 | PaletteKnown uint16 | ||
| 20 | HasFG bool | 20 | HasFG bool | ||
| 21 | HasBG bool | 21 | HasBG bool | ||
| 22 | - | // TrueColor records output capability for transport compatibility. Semantic | 22 | + | Known bool |
| 23 | - | // styles remain RGB; the attachment renderer quantizes them when necessary. | 23 | + | SchemeKnown bool |
| 24 | - | TrueColor bool | 24 | + | Light bool |
| 25 | - | Known bool | 25 | + | UsePalette bool |
| 26 | - | SchemeKnown bool | |||
| 27 | - | Light bool | |||
| 28 | - | UsePalette bool | |||
| 29 | } | 26 | } | ||
| 30 | 27 | ||||
| 31 | // PaletteColor returns a palette color only when palette inheritance is | 28 | // PaletteColor returns a palette color only when palette inheritance is | ||
| @@ -43,7 +40,6 @@ var ( | |||||
| 43 | Background: renderer.RGB{R: 0x18, G: 0x18, B: 0x18}, | 40 | Background: renderer.RGB{R: 0x18, G: 0x18, B: 0x18}, | ||
| 44 | HasFG: true, | 41 | HasFG: true, | ||
| 45 | HasBG: true, | 42 | HasBG: true, | ||
| 46 | - | TrueColor: true, | |||
| 47 | Known: true, | 43 | Known: true, | ||
| 48 | SchemeKnown: true, | 44 | SchemeKnown: true, | ||
| 49 | Light: false, | 45 | Light: false, | ||
| @@ -53,7 +49,6 @@ var ( | |||||
| 53 | Background: renderer.RGB{R: 0xf8, G: 0xf8, B: 0xf8}, | 49 | Background: renderer.RGB{R: 0xf8, G: 0xf8, B: 0xf8}, | ||
| 54 | HasFG: true, | 50 | HasFG: true, | ||
| 55 | HasBG: true, | 51 | HasBG: true, | ||
| 56 | - | TrueColor: true, | |||
| 57 | Known: true, | 52 | Known: true, | ||
| 58 | SchemeKnown: true, | 53 | SchemeKnown: true, | ||
| 59 | Light: true, | 54 | Light: true, | ||
M internal/usecase/theme/theme_test.go +7 -9
| @@ -271,8 +271,7 @@ func TestDimmer(t *testing.T) { | |||
| 271 | 271 | theme := Theme{ | |
| 272 | 272 | Foreground: renderer.RGB{R: 240, G: 240, B: 240}, | |
| 273 | 273 | Background: renderer.RGB{R: 10, G: 20, B: 30}, | |
| 274 | - | HasFG: true, HasBG: true, Known: true, TrueColor: true, | |
| 275 | - | } | |
| 274 | + | HasFG: true, HasBG: true, Known: true} | |
| 276 | 275 | dimmer := NewDimmer(theme, WithForegroundDimming(55)) | |
| 277 | 276 | ||
| 278 | 277 | t.Run("fades foreground toward the faded cell background", func(t *testing.T) { | |
| @@ -344,7 +343,7 @@ func TestDimmer(t *testing.T) { | |||
| 344 | 343 | } | |
| 345 | 344 | ||
| 346 | 345 | func TestDimmerDefaults(t *testing.T) { | |
| 347 | - | theme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true, TrueColor: true} | |
| 346 | + | theme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true} | |
| 348 | 347 | tests := []struct { | |
| 349 | 348 | name string | |
| 350 | 349 | in renderer.Style | |
| @@ -439,8 +438,7 @@ func TestDimmerUsesReportedPalette(t *testing.T) { | |||
| 439 | 438 | foreground := renderer.RGB{R: 200, G: 200, B: 200} | |
| 440 | 439 | baseTheme := Theme{ | |
| 441 | 440 | Foreground: foreground, Background: background, | |
| 442 | - | HasFG: true, HasBG: true, Known: true, TrueColor: true, | |
| 443 | - | } | |
| 441 | + | HasFG: true, HasBG: true, Known: true} | |
| 444 | 442 | ||
| 445 | 443 | reportedSlot2 := renderer.RGB{R: 60, G: 180, B: 75} | |
| 446 | 444 | reportedSlot4 := renderer.RGB{R: 30, G: 100, B: 200} | |
| @@ -585,7 +583,7 @@ func TestStyleHelpersFallbackAndThemed(t *testing.T) { | |||
| 585 | 583 | t.Fatalf("selection fallback=%+v want inverse", got) | |
| 586 | 584 | } | |
| 587 | 585 | ||
| 588 | - | theme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true, TrueColor: true} | |
| 586 | + | theme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true} | |
| 589 | 587 | if got, want := Blend(renderer.RGB{R: 0, G: 0, B: 0}, renderer.RGB{R: 100, G: 200, B: 255}, 0.25), (renderer.RGB{R: 25, G: 50, B: 64}); got != want { | |
| 590 | 588 | t.Fatalf("blend=%+v want %+v", got, want) | |
| 591 | 589 | } | |
| @@ -608,7 +606,7 @@ func TestStyleHelpersFallbackAndThemed(t *testing.T) { | |||
| 608 | 606 | } | |
| 609 | 607 | ||
| 610 | 608 | func TestEmphasisStyle(t *testing.T) { | |
| 611 | - | usableTheme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true, TrueColor: true} | |
| 609 | + | usableTheme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true} | |
| 612 | 610 | base := renderer.Style{HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 1, G: 2, B: 3}} | |
| 613 | 611 | ||
| 614 | 612 | tests := []struct { | |
| @@ -646,7 +644,7 @@ func TestEmphasisStyle(t *testing.T) { | |||
| 646 | 644 | } | |
| 647 | 645 | ||
| 648 | 646 | func TestMutedVariantStyle(t *testing.T) { | |
| 649 | - | usableTheme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true, TrueColor: true} | |
| 647 | + | usableTheme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true} | |
| 650 | 648 | rgbBase := renderer.Style{ | |
| 651 | 649 | HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 100, G: 100, B: 100}, | |
| 652 | 650 | HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 0, G: 0, B: 0}, | |
| @@ -686,7 +684,7 @@ func TestMutedVariantStyle(t *testing.T) { | |||
| 686 | 684 | { | |
| 687 | 685 | name: "base without RGB background and theme without background returns base unchanged", | |
| 688 | 686 | base: fgOnlyBase, | |
| 689 | - | theme: Theme{Known: true, TrueColor: true, HasFG: true}, | |
| 687 | + | theme: Theme{Known: true, HasFG: true}, | |
| 690 | 688 | want: fgOnlyBase, | |
| 691 | 689 | }, | |
| 692 | 690 | { | |
| @@ -271,8 +271,7 @@ func TestDimmer(t *testing.T) { | |||||
| 271 | theme := Theme{ | 271 | theme := Theme{ | ||
| 272 | Foreground: renderer.RGB{R: 240, G: 240, B: 240}, | 272 | Foreground: renderer.RGB{R: 240, G: 240, B: 240}, | ||
| 273 | Background: renderer.RGB{R: 10, G: 20, B: 30}, | 273 | Background: renderer.RGB{R: 10, G: 20, B: 30}, | ||
| 274 | - | HasFG: true, HasBG: true, Known: true, TrueColor: true, | 274 | + | HasFG: true, HasBG: true, Known: true} |
| 275 | - | } | |||
| 276 | dimmer := NewDimmer(theme, WithForegroundDimming(55)) | 275 | dimmer := NewDimmer(theme, WithForegroundDimming(55)) | ||
| 277 | 276 | ||||
| 278 | t.Run("fades foreground toward the faded cell background", func(t *testing.T) { | 277 | t.Run("fades foreground toward the faded cell background", func(t *testing.T) { | ||
| @@ -344,7 +343,7 @@ func TestDimmer(t *testing.T) { | |||||
| 344 | } | 343 | } | ||
| 345 | 344 | ||||
| 346 | func TestDimmerDefaults(t *testing.T) { | 345 | func TestDimmerDefaults(t *testing.T) { | ||
| 347 | - | theme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true, TrueColor: true} | 346 | + | theme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true} |
| 348 | tests := []struct { | 347 | tests := []struct { | ||
| 349 | name string | 348 | name string | ||
| 350 | in renderer.Style | 349 | in renderer.Style | ||
| @@ -439,8 +438,7 @@ func TestDimmerUsesReportedPalette(t *testing.T) { | |||||
| 439 | foreground := renderer.RGB{R: 200, G: 200, B: 200} | 438 | foreground := renderer.RGB{R: 200, G: 200, B: 200} | ||
| 440 | baseTheme := Theme{ | 439 | baseTheme := Theme{ | ||
| 441 | Foreground: foreground, Background: background, | 440 | Foreground: foreground, Background: background, | ||
| 442 | - | HasFG: true, HasBG: true, Known: true, TrueColor: true, | 441 | + | HasFG: true, HasBG: true, Known: true} |
| 443 | - | } | |||
| 444 | 442 | ||||
| 445 | reportedSlot2 := renderer.RGB{R: 60, G: 180, B: 75} | 443 | reportedSlot2 := renderer.RGB{R: 60, G: 180, B: 75} | ||
| 446 | reportedSlot4 := renderer.RGB{R: 30, G: 100, B: 200} | 444 | reportedSlot4 := renderer.RGB{R: 30, G: 100, B: 200} | ||
| @@ -585,7 +583,7 @@ func TestStyleHelpersFallbackAndThemed(t *testing.T) { | |||||
| 585 | t.Fatalf("selection fallback=%+v want inverse", got) | 583 | t.Fatalf("selection fallback=%+v want inverse", got) | ||
| 586 | } | 584 | } | ||
| 587 | 585 | ||||
| 588 | - | theme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true, TrueColor: true} | 586 | + | theme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true} |
| 589 | if got, want := Blend(renderer.RGB{R: 0, G: 0, B: 0}, renderer.RGB{R: 100, G: 200, B: 255}, 0.25), (renderer.RGB{R: 25, G: 50, B: 64}); got != want { | 587 | if got, want := Blend(renderer.RGB{R: 0, G: 0, B: 0}, renderer.RGB{R: 100, G: 200, B: 255}, 0.25), (renderer.RGB{R: 25, G: 50, B: 64}); got != want { | ||
| 590 | t.Fatalf("blend=%+v want %+v", got, want) | 588 | t.Fatalf("blend=%+v want %+v", got, want) | ||
| 591 | } | 589 | } | ||
| @@ -608,7 +606,7 @@ func TestStyleHelpersFallbackAndThemed(t *testing.T) { | |||||
| 608 | } | 606 | } | ||
| 609 | 607 | ||||
| 610 | func TestEmphasisStyle(t *testing.T) { | 608 | func TestEmphasisStyle(t *testing.T) { | ||
| 611 | - | usableTheme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true, TrueColor: true} | 609 | + | usableTheme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true} |
| 612 | base := renderer.Style{HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 1, G: 2, B: 3}} | 610 | base := renderer.Style{HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 1, G: 2, B: 3}} | ||
| 613 | 611 | ||||
| 614 | tests := []struct { | 612 | tests := []struct { | ||
| @@ -646,7 +644,7 @@ func TestEmphasisStyle(t *testing.T) { | |||||
| 646 | } | 644 | } | ||
| 647 | 645 | ||||
| 648 | func TestMutedVariantStyle(t *testing.T) { | 646 | func TestMutedVariantStyle(t *testing.T) { | ||
| 649 | - | usableTheme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true, TrueColor: true} | 647 | + | usableTheme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true} |
| 650 | rgbBase := renderer.Style{ | 648 | rgbBase := renderer.Style{ | ||
| 651 | HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 100, G: 100, B: 100}, | 649 | HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 100, G: 100, B: 100}, | ||
| 652 | HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 0, G: 0, B: 0}, | 650 | HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 0, G: 0, B: 0}, | ||
| @@ -686,7 +684,7 @@ func TestMutedVariantStyle(t *testing.T) { | |||||
| 686 | { | 684 | { | ||
| 687 | name: "base without RGB background and theme without background returns base unchanged", | 685 | name: "base without RGB background and theme without background returns base unchanged", | ||
| 688 | base: fgOnlyBase, | 686 | base: fgOnlyBase, | ||
| 689 | - | theme: Theme{Known: true, TrueColor: true, HasFG: true}, | 687 | + | theme: Theme{Known: true, HasFG: true}, |
| 690 | want: fgOnlyBase, | 688 | want: fgOnlyBase, | ||
| 691 | }, | 689 | }, | ||
| 692 | { | 690 | { | ||
M transport_replay_integration_test.go +1 -1
| @@ -60,7 +60,7 @@ func TestThemeGenerationTransportSequences(t *testing.T) { | |||
| 60 | 60 | cleared := protocol.Theme{ | |
| 61 | 61 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | |
| 62 | 62 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | |
| 63 | - | TrueColor: true, SchemeKnown: true, | |
| 63 | + | SchemeKnown: true, | |
| 64 | 64 | } | |
| 65 | 65 | palette := [16]renderer.RGB{2: {R: 125, G: 181, B: 181}, 10: {R: 125, G: 181, B: 181}} | |
| 66 | 66 | definitive := cleared | |
| @@ -60,7 +60,7 @@ func TestThemeGenerationTransportSequences(t *testing.T) { | |||||
| 60 | cleared := protocol.Theme{ | 60 | cleared := protocol.Theme{ | ||
| 61 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | 61 | HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, | ||
| 62 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | 62 | HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, | ||
| 63 | - | TrueColor: true, SchemeKnown: true, | 63 | + | SchemeKnown: true, |
| 64 | } | 64 | } | ||
| 65 | palette := [16]renderer.RGB{2: {R: 125, G: 181, B: 181}, 10: {R: 125, G: 181, B: 181}} | 65 | palette := [16]renderer.RGB{2: {R: 125, G: 181, B: 181}, 10: {R: 125, G: 181, B: 181}} | ||
| 66 | definitive := cleared | 66 | definitive := cleared | ||