feat(terminal): isolate attachment capabilities
- commit
- 385a8c0093eeced89d7ecd4961274902360defc8
- author
- brice · · 2026-08-14 09:52 UTC
- parent
- 5e4da5d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
27 files · +323 -133
M docs/terminal.md +3 -1
| @@ -2,7 +2,9 @@ | |||
| 2 | 2 | ||
| 3 | 3 | ## Color | |
| 4 | 4 | ||
| 5 | - | When the client reports truecolor, new panes get `TERM=xterm-direct` and `COLORTERM=truecolor`; otherwise `TERM=xterm-256color`. The capability travels inside vev's own protocol, so it works over SSH without `SendEnv`/`AcceptEnv`. Panes restored from a snapshot derive the same capability from the daemon's startup environment until a client attaches and refreshes it. | |
| 5 | + | Panes always receive vev's stable compatibility contract: `TERM=xterm-256color`, `COLORTERM=truecolor`, and `TERM_PROGRAM=vev`. This contract does not mirror the terminal attached outside vev. | |
| 6 | + | ||
| 7 | + | Each attachment independently detects its output color support from terminal environment signals. vev preserves RGB pane state and converts it to the xterm 256-color palette only for attachments that do not advertise truecolor. An attachment reporting a 256-color terminal receives a one-time warning before its first paint. Environment signals are advisory—especially through nested multiplexers—and do not advertise Kitty graphics support. | |
| 6 | 8 | ||
| 7 | 9 | ## Environment | |
| 8 | 10 | ||
| @@ -2,7 +2,9 @@ | |||||
| 2 | 2 | ||||
| 3 | ## Color | 3 | ## Color | ||
| 4 | 4 | ||||
| 5 | - | When the client reports truecolor, new panes get `TERM=xterm-direct` and `COLORTERM=truecolor`; otherwise `TERM=xterm-256color`. The capability travels inside vev's own protocol, so it works over SSH without `SendEnv`/`AcceptEnv`. Panes restored from a snapshot derive the same capability from the daemon's startup environment until a client attaches and refreshes it. | 5 | + | Panes always receive vev's stable compatibility contract: `TERM=xterm-256color`, `COLORTERM=truecolor`, and `TERM_PROGRAM=vev`. This contract does not mirror the terminal attached outside vev. |
| 6 | + | ||||
| 7 | + | Each attachment independently detects its output color support from terminal environment signals. vev preserves RGB pane state and converts it to the xterm 256-color palette only for attachments that do not advertise truecolor. An attachment reporting a 256-color terminal receives a one-time warning before its first paint. Environment signals are advisory—especially through nested multiplexers—and do not advertise Kitty graphics support. | |||
| 6 | 8 | ||||
| 7 | ## Environment | 9 | ## Environment | ||
| 8 | 10 | ||||
M internal/app/daemon_integration_test.go +1 -1
| @@ -220,7 +220,7 @@ func assertChildEnvironment(t *testing.T, tr ports.Transport, p *pump, sz domain | |||
| 220 | 220 | "SHELL=" + filepath.Base(wantShell), | |
| 221 | 221 | "XDG_RUNTIME_DIR=" + wantRuntimeDir, | |
| 222 | 222 | "WAYLAND_DISPLAY=" + wantWayland, | |
| 223 | - | "TERM=xterm-direct", | |
| 223 | + | "TERM=xterm-256color", | |
| 224 | 224 | "COLORTERM=truecolor", | |
| 225 | 225 | "TERM_PROGRAM=vev", | |
| 226 | 226 | "VEV_PREFIX=session=environment,tab=", | |
| @@ -220,7 +220,7 @@ func assertChildEnvironment(t *testing.T, tr ports.Transport, p *pump, sz domain | |||||
| 220 | "SHELL=" + filepath.Base(wantShell), | 220 | "SHELL=" + filepath.Base(wantShell), | ||
| 221 | "XDG_RUNTIME_DIR=" + wantRuntimeDir, | 221 | "XDG_RUNTIME_DIR=" + wantRuntimeDir, | ||
| 222 | "WAYLAND_DISPLAY=" + wantWayland, | 222 | "WAYLAND_DISPLAY=" + wantWayland, | ||
| 223 | - | "TERM=xterm-direct", | 223 | + | "TERM=xterm-256color", |
| 224 | "COLORTERM=truecolor", | 224 | "COLORTERM=truecolor", | ||
| 225 | "TERM_PROGRAM=vev", | 225 | "TERM_PROGRAM=vev", | ||
| 226 | "VEV_PREFIX=session=environment,tab=", | 226 | "VEV_PREFIX=session=environment,tab=", | ||
M internal/ports/terminal.go +92 -6
| @@ -17,13 +17,99 @@ type Terminal interface { | |||
| 17 | 17 | Flush() error | |
| 18 | 18 | } | |
| 19 | 19 | ||
| 20 | - | // DetectTrueColor reports whether TERM/COLORTERM advertise direct color support. | |
| 21 | - | func DetectTrueColor(termEnv, colorTerm string) bool { | |
| 22 | - | switch strings.ToLower(strings.TrimSpace(colorTerm)) { | |
| 20 | + | // TerminalColorMode is the color output mode selected for one attachment. | |
| 21 | + | type TerminalColorMode uint8 | |
| 22 | + | ||
| 23 | + | const ( | |
| 24 | + | // TrueColor is the zero value so manually constructed attachments retain the | |
| 25 | + | // historical renderer behavior; live Hello paths always use the detector. | |
| 26 | + | TerminalColorTrueColor TerminalColorMode = iota | |
| 27 | + | TerminalColorIndexed256 | |
| 28 | + | ) | |
| 29 | + | ||
| 30 | + | // TerminalCapabilitySource records how confidently a terminal capability was | |
| 31 | + | // selected. Environment observations are advisory because nested multiplexers | |
| 32 | + | // can retain values from an outer terminal. | |
| 33 | + | type TerminalCapabilitySource uint8 | |
| 34 | + | ||
| 35 | + | const ( | |
| 36 | + | TerminalCapabilityUnknown TerminalCapabilitySource = iota | |
| 37 | + | TerminalCapabilityHeuristic | |
| 38 | + | TerminalCapabilityDeclared | |
| 39 | + | ) | |
| 40 | + | ||
| 41 | + | // TerminalApplication identifies a known terminal application when its | |
| 42 | + | // environment provides a trustworthy origin signal. It does not imply support | |
| 43 | + | // for any application-specific protocol on the current output path. | |
| 44 | + | type TerminalApplication uint8 | |
| 45 | + | ||
| 46 | + | const ( | |
| 47 | + | TerminalApplicationUnknown TerminalApplication = iota | |
| 48 | + | TerminalApplicationKitty | |
| 49 | + | ) | |
| 50 | + | ||
| 51 | + | // TerminalCapabilities describes the output features selected for one client | |
| 52 | + | // attachment. Pane processes must not derive their terminal environment from | |
| 53 | + | // these capabilities. | |
| 54 | + | type TerminalCapabilities struct { | |
| 55 | + | ColorMode TerminalColorMode | |
| 56 | + | ColorSource TerminalCapabilitySource | |
| 57 | + | Application TerminalApplication | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | // TrueColor reports whether this attachment can receive RGB ANSI output. | |
| 61 | + | func (c TerminalCapabilities) TrueColor() bool { | |
| 62 | + | return c.ColorMode == TerminalColorTrueColor | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // DetectTerminalCapabilities derives conservative attachment capabilities from | |
| 66 | + | // a client environment. Missing evidence selects indexed color; it never proves | |
| 67 | + | // the physical terminal cannot display direct color. | |
| 68 | + | func DetectTerminalCapabilities(env []string) TerminalCapabilities { | |
| 69 | + | values := environmentValues(env) | |
| 70 | + | term := strings.ToLower(strings.TrimSpace(values["TERM"])) | |
| 71 | + | colorTerm := strings.ToLower(strings.TrimSpace(values["COLORTERM"])) | |
| 72 | + | caps := TerminalCapabilities{ColorMode: TerminalColorIndexed256} | |
| 73 | + | ||
| 74 | + | if values["KITTY_WINDOW_ID"] != "" || values["KITTY_PID"] != "" || values["KITTY_LISTEN_ON"] != "" { | |
| 75 | + | caps.Application = TerminalApplicationKitty | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | switch colorTerm { | |
| 23 | 79 | case "truecolor", "24bit": | |
| 24 | - | return true | |
| 80 | + | caps.ColorMode = TerminalColorTrueColor | |
| 81 | + | caps.ColorSource = TerminalCapabilityDeclared | |
| 82 | + | return caps | |
| 83 | + | } | |
| 84 | + | if term == "xterm-direct" || strings.HasSuffix(term, "-direct") { | |
| 85 | + | caps.ColorMode = TerminalColorTrueColor | |
| 86 | + | caps.ColorSource = TerminalCapabilityDeclared | |
| 87 | + | return caps | |
| 88 | + | } | |
| 89 | + | if term == "xterm-kitty" && caps.Application == TerminalApplicationKitty { | |
| 90 | + | caps.ColorMode = TerminalColorTrueColor | |
| 91 | + | caps.ColorSource = TerminalCapabilityHeuristic | |
| 92 | + | return caps | |
| 93 | + | } | |
| 94 | + | if strings.Contains(term, "256color") || term == "dumb" { | |
| 95 | + | caps.ColorSource = TerminalCapabilityDeclared | |
| 25 | 96 | } | |
| 97 | + | return caps | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | func environmentValues(env []string) map[string]string { | |
| 101 | + | values := make(map[string]string, len(env)) | |
| 102 | + | for _, entry := range env { | |
| 103 | + | key, value, ok := strings.Cut(entry, "=") | |
| 104 | + | if !ok { | |
| 105 | + | continue | |
| 106 | + | } | |
| 107 | + | values[key] = value | |
| 108 | + | } | |
| 109 | + | return values | |
| 110 | + | } | |
| 26 | 111 | ||
| 27 | - | termEnv = strings.ToLower(strings.TrimSpace(termEnv)) | |
| 28 | - | return termEnv == "xterm-direct" || strings.HasSuffix(termEnv, "-direct") | |
| 112 | + | // DetectTrueColor reports whether TERM/COLORTERM advertise direct color support. | |
| 113 | + | func DetectTrueColor(termEnv, colorTerm string) bool { | |
| 114 | + | return DetectTerminalCapabilities([]string{"TERM=" + termEnv, "COLORTERM=" + colorTerm}).TrueColor() | |
| 29 | 115 | } | |
| @@ -17,13 +17,99 @@ type Terminal interface { | |||||
| 17 | Flush() error | 17 | Flush() error | ||
| 18 | } | 18 | } | ||
| 19 | 19 | ||||
| 20 | - | // DetectTrueColor reports whether TERM/COLORTERM advertise direct color support. | 20 | + | // TerminalColorMode is the color output mode selected for one attachment. |
| 21 | - | func DetectTrueColor(termEnv, colorTerm string) bool { | 21 | + | type TerminalColorMode uint8 |
| 22 | - | switch strings.ToLower(strings.TrimSpace(colorTerm)) { | 22 | + | |
| 23 | + | const ( | |||
| 24 | + | // TrueColor is the zero value so manually constructed attachments retain the | |||
| 25 | + | // historical renderer behavior; live Hello paths always use the detector. | |||
| 26 | + | TerminalColorTrueColor TerminalColorMode = iota | |||
| 27 | + | TerminalColorIndexed256 | |||
| 28 | + | ) | |||
| 29 | + | ||||
| 30 | + | // TerminalCapabilitySource records how confidently a terminal capability was | |||
| 31 | + | // selected. Environment observations are advisory because nested multiplexers | |||
| 32 | + | // can retain values from an outer terminal. | |||
| 33 | + | type TerminalCapabilitySource uint8 | |||
| 34 | + | ||||
| 35 | + | const ( | |||
| 36 | + | TerminalCapabilityUnknown TerminalCapabilitySource = iota | |||
| 37 | + | TerminalCapabilityHeuristic | |||
| 38 | + | TerminalCapabilityDeclared | |||
| 39 | + | ) | |||
| 40 | + | ||||
| 41 | + | // TerminalApplication identifies a known terminal application when its | |||
| 42 | + | // environment provides a trustworthy origin signal. It does not imply support | |||
| 43 | + | // for any application-specific protocol on the current output path. | |||
| 44 | + | type TerminalApplication uint8 | |||
| 45 | + | ||||
| 46 | + | const ( | |||
| 47 | + | TerminalApplicationUnknown TerminalApplication = iota | |||
| 48 | + | TerminalApplicationKitty | |||
| 49 | + | ) | |||
| 50 | + | ||||
| 51 | + | // TerminalCapabilities describes the output features selected for one client | |||
| 52 | + | // attachment. Pane processes must not derive their terminal environment from | |||
| 53 | + | // these capabilities. | |||
| 54 | + | type TerminalCapabilities struct { | |||
| 55 | + | ColorMode TerminalColorMode | |||
| 56 | + | ColorSource TerminalCapabilitySource | |||
| 57 | + | Application TerminalApplication | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | // TrueColor reports whether this attachment can receive RGB ANSI output. | |||
| 61 | + | func (c TerminalCapabilities) TrueColor() bool { | |||
| 62 | + | return c.ColorMode == TerminalColorTrueColor | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // DetectTerminalCapabilities derives conservative attachment capabilities from | |||
| 66 | + | // a client environment. Missing evidence selects indexed color; it never proves | |||
| 67 | + | // the physical terminal cannot display direct color. | |||
| 68 | + | func DetectTerminalCapabilities(env []string) TerminalCapabilities { | |||
| 69 | + | values := environmentValues(env) | |||
| 70 | + | term := strings.ToLower(strings.TrimSpace(values["TERM"])) | |||
| 71 | + | colorTerm := strings.ToLower(strings.TrimSpace(values["COLORTERM"])) | |||
| 72 | + | caps := TerminalCapabilities{ColorMode: TerminalColorIndexed256} | |||
| 73 | + | ||||
| 74 | + | if values["KITTY_WINDOW_ID"] != "" || values["KITTY_PID"] != "" || values["KITTY_LISTEN_ON"] != "" { | |||
| 75 | + | caps.Application = TerminalApplicationKitty | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | switch colorTerm { | |||
| 23 | case "truecolor", "24bit": | 79 | case "truecolor", "24bit": | ||
| 24 | - | return true | 80 | + | caps.ColorMode = TerminalColorTrueColor |
| 81 | + | caps.ColorSource = TerminalCapabilityDeclared | |||
| 82 | + | return caps | |||
| 83 | + | } | |||
| 84 | + | if term == "xterm-direct" || strings.HasSuffix(term, "-direct") { | |||
| 85 | + | caps.ColorMode = TerminalColorTrueColor | |||
| 86 | + | caps.ColorSource = TerminalCapabilityDeclared | |||
| 87 | + | return caps | |||
| 88 | + | } | |||
| 89 | + | if term == "xterm-kitty" && caps.Application == TerminalApplicationKitty { | |||
| 90 | + | caps.ColorMode = TerminalColorTrueColor | |||
| 91 | + | caps.ColorSource = TerminalCapabilityHeuristic | |||
| 92 | + | return caps | |||
| 93 | + | } | |||
| 94 | + | if strings.Contains(term, "256color") || term == "dumb" { | |||
| 95 | + | caps.ColorSource = TerminalCapabilityDeclared | |||
| 25 | } | 96 | } | ||
| 97 | + | return caps | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | func environmentValues(env []string) map[string]string { | |||
| 101 | + | values := make(map[string]string, len(env)) | |||
| 102 | + | for _, entry := range env { | |||
| 103 | + | key, value, ok := strings.Cut(entry, "=") | |||
| 104 | + | if !ok { | |||
| 105 | + | continue | |||
| 106 | + | } | |||
| 107 | + | values[key] = value | |||
| 108 | + | } | |||
| 109 | + | return values | |||
| 110 | + | } | |||
| 26 | 111 | ||||
| 27 | - | termEnv = strings.ToLower(strings.TrimSpace(termEnv)) | 112 | + | // DetectTrueColor reports whether TERM/COLORTERM advertise direct color support. |
| 28 | - | return termEnv == "xterm-direct" || strings.HasSuffix(termEnv, "-direct") | 113 | + | func DetectTrueColor(termEnv, colorTerm string) bool { |
| 114 | + | return DetectTerminalCapabilities([]string{"TERM=" + termEnv, "COLORTERM=" + colorTerm}).TrueColor() | |||
| 29 | } | 115 | } | ||
A internal/ports/terminal_test.go +59 -0
| @@ -0,0 +1,59 @@ | |||
| 1 | + | package ports | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/stretchr/testify/require" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestDetectTerminalCapabilities(t *testing.T) { | |
| 10 | + | tests := []struct { | |
| 11 | + | name string | |
| 12 | + | env []string | |
| 13 | + | wantMode TerminalColorMode | |
| 14 | + | wantSource TerminalCapabilitySource | |
| 15 | + | wantApp TerminalApplication | |
| 16 | + | }{ | |
| 17 | + | { | |
| 18 | + | name: "declared truecolor", | |
| 19 | + | env: []string{"TERM=xterm-256color", "COLORTERM=truecolor"}, | |
| 20 | + | wantMode: TerminalColorTrueColor, | |
| 21 | + | wantSource: TerminalCapabilityDeclared, | |
| 22 | + | }, | |
| 23 | + | { | |
| 24 | + | name: "kitty signals infer truecolor", | |
| 25 | + | env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, | |
| 26 | + | wantMode: TerminalColorTrueColor, | |
| 27 | + | wantSource: TerminalCapabilityHeuristic, | |
| 28 | + | wantApp: TerminalApplicationKitty, | |
| 29 | + | }, | |
| 30 | + | { | |
| 31 | + | name: "256 color terminal remains a constrained attachment", | |
| 32 | + | env: []string{"TERM=xterm-256color"}, | |
| 33 | + | wantMode: TerminalColorIndexed256, | |
| 34 | + | wantSource: TerminalCapabilityDeclared, | |
| 35 | + | }, | |
| 36 | + | { | |
| 37 | + | name: "unknown terminal is conservatively indexed", | |
| 38 | + | env: []string{"TERM=unknown"}, | |
| 39 | + | wantMode: TerminalColorIndexed256, | |
| 40 | + | wantSource: TerminalCapabilityUnknown, | |
| 41 | + | }, | |
| 42 | + | { | |
| 43 | + | name: "kitty environment behind a multiplexer is not graphics evidence", | |
| 44 | + | env: []string{"TERM=tmux-256color", "KITTY_WINDOW_ID=1"}, | |
| 45 | + | wantMode: TerminalColorIndexed256, | |
| 46 | + | wantSource: TerminalCapabilityDeclared, | |
| 47 | + | wantApp: TerminalApplicationKitty, | |
| 48 | + | }, | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | for _, tt := range tests { | |
| 52 | + | t.Run(tt.name, func(t *testing.T) { | |
| 53 | + | got := DetectTerminalCapabilities(tt.env) | |
| 54 | + | require.Equal(t, tt.wantMode, got.ColorMode) | |
| 55 | + | require.Equal(t, tt.wantSource, got.ColorSource) | |
| 56 | + | require.Equal(t, tt.wantApp, got.Application) | |
| 57 | + | }) | |
| 58 | + | } | |
| 59 | + | } | |
| @@ -0,0 +1,59 @@ | |||||
| 1 | + | package ports | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/stretchr/testify/require" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestDetectTerminalCapabilities(t *testing.T) { | |||
| 10 | + | tests := []struct { | |||
| 11 | + | name string | |||
| 12 | + | env []string | |||
| 13 | + | wantMode TerminalColorMode | |||
| 14 | + | wantSource TerminalCapabilitySource | |||
| 15 | + | wantApp TerminalApplication | |||
| 16 | + | }{ | |||
| 17 | + | { | |||
| 18 | + | name: "declared truecolor", | |||
| 19 | + | env: []string{"TERM=xterm-256color", "COLORTERM=truecolor"}, | |||
| 20 | + | wantMode: TerminalColorTrueColor, | |||
| 21 | + | wantSource: TerminalCapabilityDeclared, | |||
| 22 | + | }, | |||
| 23 | + | { | |||
| 24 | + | name: "kitty signals infer truecolor", | |||
| 25 | + | env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, | |||
| 26 | + | wantMode: TerminalColorTrueColor, | |||
| 27 | + | wantSource: TerminalCapabilityHeuristic, | |||
| 28 | + | wantApp: TerminalApplicationKitty, | |||
| 29 | + | }, | |||
| 30 | + | { | |||
| 31 | + | name: "256 color terminal remains a constrained attachment", | |||
| 32 | + | env: []string{"TERM=xterm-256color"}, | |||
| 33 | + | wantMode: TerminalColorIndexed256, | |||
| 34 | + | wantSource: TerminalCapabilityDeclared, | |||
| 35 | + | }, | |||
| 36 | + | { | |||
| 37 | + | name: "unknown terminal is conservatively indexed", | |||
| 38 | + | env: []string{"TERM=unknown"}, | |||
| 39 | + | wantMode: TerminalColorIndexed256, | |||
| 40 | + | wantSource: TerminalCapabilityUnknown, | |||
| 41 | + | }, | |||
| 42 | + | { | |||
| 43 | + | name: "kitty environment behind a multiplexer is not graphics evidence", | |||
| 44 | + | env: []string{"TERM=tmux-256color", "KITTY_WINDOW_ID=1"}, | |||
| 45 | + | wantMode: TerminalColorIndexed256, | |||
| 46 | + | wantSource: TerminalCapabilityDeclared, | |||
| 47 | + | wantApp: TerminalApplicationKitty, | |||
| 48 | + | }, | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | for _, tt := range tests { | |||
| 52 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 53 | + | got := DetectTerminalCapabilities(tt.env) | |||
| 54 | + | require.Equal(t, tt.wantMode, got.ColorMode) | |||
| 55 | + | require.Equal(t, tt.wantSource, got.ColorSource) | |||
| 56 | + | require.Equal(t, tt.wantApp, got.Application) | |||
| 57 | + | }) | |||
| 58 | + | } | |||
| 59 | + | } | |||
M internal/usecase/daemon/attachment_transition_publish.go +0 -1
| @@ -176,7 +176,6 @@ func (d *Daemon) applyTargetStateLocked(publication *attachmentPublication) bool | |||
| 176 | 176 | req.target.activateAttachmentViewLocked(req.next, req.targetTabIndex) | |
| 177 | 177 | } | |
| 178 | 178 | if req.copySourceEnvironment && publication.source != nil && req.target != nil { | |
| 179 | - | req.target.terminal = publication.source.terminal | |
| 180 | 179 | req.target.env = copyEnvironment(publication.source.env) | |
| 181 | 180 | } | |
| 182 | 181 | return req.preserveAttachment | |
| @@ -176,7 +176,6 @@ func (d *Daemon) applyTargetStateLocked(publication *attachmentPublication) bool | |||||
| 176 | req.target.activateAttachmentViewLocked(req.next, req.targetTabIndex) | 176 | req.target.activateAttachmentViewLocked(req.next, req.targetTabIndex) | ||
| 177 | } | 177 | } | ||
| 178 | if req.copySourceEnvironment && publication.source != nil && req.target != nil { | 178 | if req.copySourceEnvironment && publication.source != nil && req.target != nil { | ||
| 179 | - | req.target.terminal = publication.source.terminal | |||
| 180 | req.target.env = copyEnvironment(publication.source.env) | 179 | req.target.env = copyEnvironment(publication.source.env) | ||
| 181 | } | 180 | } | ||
| 182 | return req.preserveAttachment | 181 | return req.preserveAttachment | ||
M internal/usecase/daemon/client.go +17 -0
| @@ -42,6 +42,7 @@ type attachedClient struct { | |||
| 42 | 42 | // remoteOrigin is attachment-specific presentation metadata supplied by a | |
| 43 | 43 | // validated picker handoff; it never participates in routing. | |
| 44 | 44 | remoteOrigin string | |
| 45 | + | terminalCapabilities ports.TerminalCapabilities | |
| 45 | 46 | navigationCapabilities ports.NavigationCapabilities | |
| 46 | 47 | startupOverlay ports.StartupOverlay | |
| 47 | 48 | // connectionGeneration is the wire-facing capability generation. attachmentEffects is | |
| @@ -628,6 +629,8 @@ type attachClientOptions struct { | |||
| 628 | 629 | resumeCapable bool | |
| 629 | 630 | maxOutputInFlight uint8 | |
| 630 | 631 | remoteOrigin string | |
| 632 | + | terminalCapabilities ports.TerminalCapabilities | |
| 633 | + | capabilitiesSet bool | |
| 631 | 634 | navigationCapabilities ports.NavigationCapabilities | |
| 632 | 635 | startupOverlay ports.StartupOverlay | |
| 633 | 636 | } | |
| @@ -657,6 +660,16 @@ func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, | |||
| 657 | 660 | func (d *Daemon) finishAttachedClient(sess *session, ac *attachedClient, opts attachClientOptions) { | |
| 658 | 661 | d.touchMRU(sess) | |
| 659 | 662 | d.log.Info("client attached", "session", sess.name, "resume", opts.resumeCapable) | |
| 663 | + | if ac.terminalCapabilities.ColorSource == ports.TerminalCapabilityDeclared && !ac.terminalCapabilities.TrueColor() { | |
| 664 | + | d.publishToast(ac, domain.Notification{ | |
| 665 | + | Code: domain.NoticeUser, | |
| 666 | + | Severity: domain.NoticeWarn, | |
| 667 | + | Message: "TrueColor was not detected; rendering with 256 colors.", | |
| 668 | + | Time: d.clock.Now(), | |
| 669 | + | Count: 1, | |
| 670 | + | SessionID: sess.id, | |
| 671 | + | }) | |
| 672 | + | } | |
| 660 | 673 | d.applyHostTheme(sess, ac, themeui.Theme{}, true) | |
| 661 | 674 | } | |
| 662 | 675 | ||
| @@ -668,6 +681,9 @@ func (d *Daemon) prepareAttachedClientLocked(tr ports.Transport, sz domain.Size, | |||
| 668 | 681 | if opts.resumeCapable { | |
| 669 | 682 | resumeToken = d.nextResumeTokenLocked() | |
| 670 | 683 | } | |
| 684 | + | if !opts.capabilitiesSet { | |
| 685 | + | opts.terminalCapabilities = ports.TerminalCapabilities{ColorMode: ports.TerminalColorTrueColor} | |
| 686 | + | } | |
| 671 | 687 | output := newOutputStateStream(opts.maxOutputInFlight) | |
| 672 | 688 | ac := &attachedClient{ | |
| 673 | 689 | tr: tr, | |
| @@ -676,6 +692,7 @@ func (d *Daemon) prepareAttachedClientLocked(tr ports.Transport, sz domain.Size, | |||
| 676 | 692 | view: attachmentView{windowRows: sz.Rows, windowSet: true}, | |
| 677 | 693 | clientID: opts.clientID, | |
| 678 | 694 | remoteOrigin: opts.remoteOrigin, | |
| 695 | + | terminalCapabilities: opts.terminalCapabilities, | |
| 679 | 696 | navigationCapabilities: opts.navigationCapabilities, | |
| 680 | 697 | startupOverlay: opts.startupOverlay, | |
| 681 | 698 | resumeCapable: opts.resumeCapable, | |
| @@ -42,6 +42,7 @@ type attachedClient struct { | |||||
| 42 | // remoteOrigin is attachment-specific presentation metadata supplied by a | 42 | // remoteOrigin is attachment-specific presentation metadata supplied by a | ||
| 43 | // validated picker handoff; it never participates in routing. | 43 | // validated picker handoff; it never participates in routing. | ||
| 44 | remoteOrigin string | 44 | remoteOrigin string | ||
| 45 | + | terminalCapabilities ports.TerminalCapabilities | |||
| 45 | navigationCapabilities ports.NavigationCapabilities | 46 | navigationCapabilities ports.NavigationCapabilities | ||
| 46 | startupOverlay ports.StartupOverlay | 47 | startupOverlay ports.StartupOverlay | ||
| 47 | // connectionGeneration is the wire-facing capability generation. attachmentEffects is | 48 | // connectionGeneration is the wire-facing capability generation. attachmentEffects is | ||
| @@ -628,6 +629,8 @@ type attachClientOptions struct { | |||||
| 628 | resumeCapable bool | 629 | resumeCapable bool | ||
| 629 | maxOutputInFlight uint8 | 630 | maxOutputInFlight uint8 | ||
| 630 | remoteOrigin string | 631 | remoteOrigin string | ||
| 632 | + | terminalCapabilities ports.TerminalCapabilities | |||
| 633 | + | capabilitiesSet bool | |||
| 631 | navigationCapabilities ports.NavigationCapabilities | 634 | navigationCapabilities ports.NavigationCapabilities | ||
| 632 | startupOverlay ports.StartupOverlay | 635 | startupOverlay ports.StartupOverlay | ||
| 633 | } | 636 | } | ||
| @@ -657,6 +660,16 @@ func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, | |||||
| 657 | func (d *Daemon) finishAttachedClient(sess *session, ac *attachedClient, opts attachClientOptions) { | 660 | func (d *Daemon) finishAttachedClient(sess *session, ac *attachedClient, opts attachClientOptions) { | ||
| 658 | d.touchMRU(sess) | 661 | d.touchMRU(sess) | ||
| 659 | d.log.Info("client attached", "session", sess.name, "resume", opts.resumeCapable) | 662 | d.log.Info("client attached", "session", sess.name, "resume", opts.resumeCapable) | ||
| 663 | + | if ac.terminalCapabilities.ColorSource == ports.TerminalCapabilityDeclared && !ac.terminalCapabilities.TrueColor() { | |||
| 664 | + | d.publishToast(ac, domain.Notification{ | |||
| 665 | + | Code: domain.NoticeUser, | |||
| 666 | + | Severity: domain.NoticeWarn, | |||
| 667 | + | Message: "TrueColor was not detected; rendering with 256 colors.", | |||
| 668 | + | Time: d.clock.Now(), | |||
| 669 | + | Count: 1, | |||
| 670 | + | SessionID: sess.id, | |||
| 671 | + | }) | |||
| 672 | + | } | |||
| 660 | d.applyHostTheme(sess, ac, themeui.Theme{}, true) | 673 | d.applyHostTheme(sess, ac, themeui.Theme{}, true) | ||
| 661 | } | 674 | } | ||
| 662 | 675 | ||||
| @@ -668,6 +681,9 @@ func (d *Daemon) prepareAttachedClientLocked(tr ports.Transport, sz domain.Size, | |||||
| 668 | if opts.resumeCapable { | 681 | if opts.resumeCapable { | ||
| 669 | resumeToken = d.nextResumeTokenLocked() | 682 | resumeToken = d.nextResumeTokenLocked() | ||
| 670 | } | 683 | } | ||
| 684 | + | if !opts.capabilitiesSet { | |||
| 685 | + | opts.terminalCapabilities = ports.TerminalCapabilities{ColorMode: ports.TerminalColorTrueColor} | |||
| 686 | + | } | |||
| 671 | output := newOutputStateStream(opts.maxOutputInFlight) | 687 | output := newOutputStateStream(opts.maxOutputInFlight) | ||
| 672 | ac := &attachedClient{ | 688 | ac := &attachedClient{ | ||
| 673 | tr: tr, | 689 | tr: tr, | ||
| @@ -676,6 +692,7 @@ func (d *Daemon) prepareAttachedClientLocked(tr ports.Transport, sz domain.Size, | |||||
| 676 | view: attachmentView{windowRows: sz.Rows, windowSet: true}, | 692 | view: attachmentView{windowRows: sz.Rows, windowSet: true}, | ||
| 677 | clientID: opts.clientID, | 693 | clientID: opts.clientID, | ||
| 678 | remoteOrigin: opts.remoteOrigin, | 694 | remoteOrigin: opts.remoteOrigin, | ||
| 695 | + | terminalCapabilities: opts.terminalCapabilities, | |||
| 679 | navigationCapabilities: opts.navigationCapabilities, | 696 | navigationCapabilities: opts.navigationCapabilities, | ||
| 680 | startupOverlay: opts.startupOverlay, | 697 | startupOverlay: opts.startupOverlay, | ||
| 681 | resumeCapable: opts.resumeCapable, | 698 | resumeCapable: opts.resumeCapable, | ||
M internal/usecase/daemon/control.go +2 -2
| @@ -480,7 +480,7 @@ func (e controlExec) CreateSessionNamed(name string) error { | |||
| 480 | 480 | return command.ErrInvalidArguments | |
| 481 | 481 | } | |
| 482 | 482 | e.sess.mu.Lock() | |
| 483 | - | cwd, term, env := e.sess.cwd, e.sess.terminal, copyEnvironment(e.sess.env) | |
| 483 | + | cwd, env := e.sess.cwd, copyEnvironment(e.sess.env) | |
| 484 | 484 | e.sess.mu.Unlock() | |
| 485 | 485 | size := e.sess.fullViewportSize() | |
| 486 | 486 | e.d.mu.Lock() | |
| @@ -491,7 +491,7 @@ func (e controlExec) CreateSessionNamed(name string) error { | |||
| 491 | 491 | if e.d.nameLiveOrStoppedLocked(name) { | |
| 492 | 492 | return errSessionNameInUse | |
| 493 | 493 | } | |
| 494 | - | _, err := e.d.createSessionLocked(name, false, cwd, size, term, env) | |
| 494 | + | _, err := e.d.createSessionLocked(name, false, cwd, size, env) | |
| 495 | 495 | return err | |
| 496 | 496 | } | |
| 497 | 497 | func (e controlExec) CloseTab() error { | |
| @@ -480,7 +480,7 @@ func (e controlExec) CreateSessionNamed(name string) error { | |||||
| 480 | return command.ErrInvalidArguments | 480 | return command.ErrInvalidArguments | ||
| 481 | } | 481 | } | ||
| 482 | e.sess.mu.Lock() | 482 | e.sess.mu.Lock() | ||
| 483 | - | cwd, term, env := e.sess.cwd, e.sess.terminal, copyEnvironment(e.sess.env) | 483 | + | cwd, env := e.sess.cwd, copyEnvironment(e.sess.env) |
| 484 | e.sess.mu.Unlock() | 484 | e.sess.mu.Unlock() | ||
| 485 | size := e.sess.fullViewportSize() | 485 | size := e.sess.fullViewportSize() | ||
| 486 | e.d.mu.Lock() | 486 | e.d.mu.Lock() | ||
| @@ -491,7 +491,7 @@ func (e controlExec) CreateSessionNamed(name string) error { | |||||
| 491 | if e.d.nameLiveOrStoppedLocked(name) { | 491 | if e.d.nameLiveOrStoppedLocked(name) { | ||
| 492 | return errSessionNameInUse | 492 | return errSessionNameInUse | ||
| 493 | } | 493 | } | ||
| 494 | - | _, err := e.d.createSessionLocked(name, false, cwd, size, term, env) | 494 | + | _, err := e.d.createSessionLocked(name, false, cwd, size, env) |
| 495 | return err | 495 | return err | ||
| 496 | } | 496 | } | ||
| 497 | func (e controlExec) CloseTab() error { | 497 | func (e controlExec) CloseTab() error { | ||
M internal/usecase/daemon/control_test.go +0 -2
| @@ -649,7 +649,6 @@ func TestHandleCommandNewSessionInheritsHeadlessIdentityAndViewport(t *testing.T | |||
| 649 | 649 | source.mu.Lock() | |
| 650 | 650 | source.cwd = "/tmp/work" | |
| 651 | 651 | source.env = []string{"INHERITED=yes"} | |
| 652 | - | source.terminal = terminalEnv{TrueColor: true} | |
| 653 | 652 | source.tabs[0].size = domain.Size{Cols: 118, Rows: 38} | |
| 654 | 653 | source.mu.Unlock() | |
| 655 | 654 | ||
| @@ -664,7 +663,6 @@ func TestHandleCommandNewSessionInheritsHeadlessIdentityAndViewport(t *testing.T | |||
| 664 | 663 | require.Empty(t, created.snapshotAttachmentsLocked()) | |
| 665 | 664 | require.Equal(t, "/tmp/work", created.cwd) | |
| 666 | 665 | require.Equal(t, []string{"INHERITED=yes"}, created.env) | |
| 667 | - | require.True(t, created.terminal.TrueColor) | |
| 668 | 666 | tb := created.tabs[0] | |
| 669 | 667 | created.mu.Unlock() | |
| 670 | 668 | tb.mu.Lock() | |
| @@ -649,7 +649,6 @@ func TestHandleCommandNewSessionInheritsHeadlessIdentityAndViewport(t *testing.T | |||||
| 649 | source.mu.Lock() | 649 | source.mu.Lock() | ||
| 650 | source.cwd = "/tmp/work" | 650 | source.cwd = "/tmp/work" | ||
| 651 | source.env = []string{"INHERITED=yes"} | 651 | source.env = []string{"INHERITED=yes"} | ||
| 652 | - | source.terminal = terminalEnv{TrueColor: true} | |||
| 653 | source.tabs[0].size = domain.Size{Cols: 118, Rows: 38} | 652 | source.tabs[0].size = domain.Size{Cols: 118, Rows: 38} | ||
| 654 | source.mu.Unlock() | 653 | source.mu.Unlock() | ||
| 655 | 654 | ||||
| @@ -664,7 +663,6 @@ func TestHandleCommandNewSessionInheritsHeadlessIdentityAndViewport(t *testing.T | |||||
| 664 | require.Empty(t, created.snapshotAttachmentsLocked()) | 663 | require.Empty(t, created.snapshotAttachmentsLocked()) | ||
| 665 | require.Equal(t, "/tmp/work", created.cwd) | 664 | require.Equal(t, "/tmp/work", created.cwd) | ||
| 666 | require.Equal(t, []string{"INHERITED=yes"}, created.env) | 665 | require.Equal(t, []string{"INHERITED=yes"}, created.env) | ||
| 667 | - | require.True(t, created.terminal.TrueColor) | |||
| 668 | tb := created.tabs[0] | 666 | tb := created.tabs[0] | ||
| 669 | created.mu.Unlock() | 667 | created.mu.Unlock() | ||
| 670 | tb.mu.Lock() | 668 | tb.mu.Lock() | ||
M internal/usecase/daemon/daemon.go +14 -9
| @@ -1249,7 +1249,7 @@ func (e *protoErr) Error() string { return e.text } | |||
| 1249 | 1249 | // unlocking d.mu before returning, including every error path. It publishes | |
| 1250 | 1250 | // terminal and role state before releasing d.mu, then defers coordinator | |
| 1251 | 1251 | // cleanup so obsolete workers never delay the new handshake. | |
| 1252 | - | func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, term terminalEnv, h ports.Hello) (*attachedClient, error) { | |
| 1252 | + | func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, h ports.Hello) (*attachedClient, error) { | |
| 1253 | 1253 | initialTabIndex := -1 | |
| 1254 | 1254 | if h.RemoteTarget != nil { | |
| 1255 | 1255 | var ok bool | |
| @@ -1276,8 +1276,12 @@ func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, | |||
| 1276 | 1276 | if h.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned { | |
| 1277 | 1277 | sess.env = copyEnvironment(h.Env) | |
| 1278 | 1278 | } | |
| 1279 | - | sess.terminal = term | |
| 1280 | 1279 | sess.mu.Unlock() | |
| 1280 | + | terminalCapabilities := ports.DetectTerminalCapabilities(h.Env) | |
| 1281 | + | if h.TrueColor && !terminalCapabilities.TrueColor() { | |
| 1282 | + | terminalCapabilities.ColorMode = ports.TerminalColorTrueColor | |
| 1283 | + | terminalCapabilities.ColorSource = ports.TerminalCapabilityDeclared | |
| 1284 | + | } | |
| 1281 | 1285 | opts := attachClientOptions{ | |
| 1282 | 1286 | clientID: h.ClientID, | |
| 1283 | 1287 | resumeCapable: true, | |
| @@ -1285,6 +1289,8 @@ func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, | |||
| 1285 | 1289 | navigationCapabilities: h.NavigationCapabilities, | |
| 1286 | 1290 | startupOverlay: h.StartupOverlay, | |
| 1287 | 1291 | remoteOrigin: h.RemoteOrigin, | |
| 1292 | + | terminalCapabilities: terminalCapabilities, | |
| 1293 | + | capabilitiesSet: true, | |
| 1288 | 1294 | } | |
| 1289 | 1295 | if opts.remoteOrigin == "" && h.RemoteTarget != nil { | |
| 1290 | 1296 | opts.remoteOrigin = h.RemoteTarget.DisplayOrigin | |
| @@ -1420,7 +1426,6 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||
| 1420 | 1426 | if !sz.Valid() { | |
| 1421 | 1427 | return nil, nil, &protoErr{ports.ErrInternal, "invalid terminal size"} | |
| 1422 | 1428 | } | |
| 1423 | - | term := terminalEnv{TrueColor: h.TrueColor} | |
| 1424 | 1429 | if h.Intent == ports.IntentResume && h.ResumeToken == 0 { | |
| 1425 | 1430 | return nil, nil, &protoErr{ports.ErrNoSuchSession, "resume token is required"} | |
| 1426 | 1431 | } | |
| @@ -1503,12 +1508,12 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||
| 1503 | 1508 | switch h.Intent { | |
| 1504 | 1509 | case ports.IntentEphemeral: | |
| 1505 | 1510 | name := d.allocEphemeralNameLocked() | |
| 1506 | - | sess, err := d.createSessionLockedWithMode(name, true, h.Cwd, sz, term, h.Env) | |
| 1511 | + | sess, err := d.createSessionLockedWithMode(name, true, h.Cwd, sz, h.Env) | |
| 1507 | 1512 | if err != nil { | |
| 1508 | 1513 | d.mu.Unlock() | |
| 1509 | 1514 | return nil, nil, err | |
| 1510 | 1515 | } | |
| 1511 | - | ac, err := d.finishRouteAttach(sess, tr, sz, term, h, true, true) | |
| 1516 | + | ac, err := d.finishRouteAttach(sess, tr, sz, h, true, true) | |
| 1512 | 1517 | return sess, ac, err | |
| 1513 | 1518 | ||
| 1514 | 1519 | case ports.IntentNew: | |
| @@ -1524,12 +1529,12 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||
| 1524 | 1529 | d.mu.Unlock() | |
| 1525 | 1530 | return nil, nil, &protoErr{ports.ErrNameTaken, "session name already in use: " + h.Name} | |
| 1526 | 1531 | } | |
| 1527 | - | sess, err := d.createSessionLockedWithMode(h.Name, false, h.Cwd, sz, term, h.Env) | |
| 1532 | + | sess, err := d.createSessionLockedWithMode(h.Name, false, h.Cwd, sz, h.Env) | |
| 1528 | 1533 | if err != nil { | |
| 1529 | 1534 | d.mu.Unlock() | |
| 1530 | 1535 | return nil, nil, err | |
| 1531 | 1536 | } | |
| 1532 | - | ac, err := d.finishRouteAttach(sess, tr, sz, term, h, true, true) | |
| 1537 | + | ac, err := d.finishRouteAttach(sess, tr, sz, h, true, true) | |
| 1533 | 1538 | return sess, ac, err | |
| 1534 | 1539 | ||
| 1535 | 1540 | case ports.IntentAttach: | |
| @@ -1547,14 +1552,14 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||
| 1547 | 1552 | env = copyEnvironment(d.baseEnv) | |
| 1548 | 1553 | } | |
| 1549 | 1554 | var err error | |
| 1550 | - | sess, err = d.createSessionLockedWithMode(h.Name, false, cwd, sz, term, env, stopped.tabNames) | |
| 1555 | + | sess, err = d.createSessionLockedWithMode(h.Name, false, cwd, sz, env, stopped.tabNames) | |
| 1551 | 1556 | if err != nil { | |
| 1552 | 1557 | d.mu.Unlock() | |
| 1553 | 1558 | return nil, nil, err | |
| 1554 | 1559 | } | |
| 1555 | 1560 | created = true | |
| 1556 | 1561 | } | |
| 1557 | - | ac, err := d.finishRouteAttach(sess, tr, sz, term, h, created, false) | |
| 1562 | + | ac, err := d.finishRouteAttach(sess, tr, sz, h, created, false) | |
| 1558 | 1563 | return sess, ac, err | |
| 1559 | 1564 | ||
| 1560 | 1565 | default: | |
| @@ -1249,7 +1249,7 @@ func (e *protoErr) Error() string { return e.text } | |||||
| 1249 | // unlocking d.mu before returning, including every error path. It publishes | 1249 | // unlocking d.mu before returning, including every error path. It publishes | ||
| 1250 | // terminal and role state before releasing d.mu, then defers coordinator | 1250 | // terminal and role state before releasing d.mu, then defers coordinator | ||
| 1251 | // cleanup so obsolete workers never delay the new handshake. | 1251 | // cleanup so obsolete workers never delay the new handshake. | ||
| 1252 | - | func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, term terminalEnv, h ports.Hello) (*attachedClient, error) { | 1252 | + | func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, h ports.Hello) (*attachedClient, error) { |
| 1253 | initialTabIndex := -1 | 1253 | initialTabIndex := -1 | ||
| 1254 | if h.RemoteTarget != nil { | 1254 | if h.RemoteTarget != nil { | ||
| 1255 | var ok bool | 1255 | var ok bool | ||
| @@ -1276,8 +1276,12 @@ func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, | |||||
| 1276 | if h.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned { | 1276 | if h.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned { | ||
| 1277 | sess.env = copyEnvironment(h.Env) | 1277 | sess.env = copyEnvironment(h.Env) | ||
| 1278 | } | 1278 | } | ||
| 1279 | - | sess.terminal = term | |||
| 1280 | sess.mu.Unlock() | 1279 | sess.mu.Unlock() | ||
| 1280 | + | terminalCapabilities := ports.DetectTerminalCapabilities(h.Env) | |||
| 1281 | + | if h.TrueColor && !terminalCapabilities.TrueColor() { | |||
| 1282 | + | terminalCapabilities.ColorMode = ports.TerminalColorTrueColor | |||
| 1283 | + | terminalCapabilities.ColorSource = ports.TerminalCapabilityDeclared | |||
| 1284 | + | } | |||
| 1281 | opts := attachClientOptions{ | 1285 | opts := attachClientOptions{ | ||
| 1282 | clientID: h.ClientID, | 1286 | clientID: h.ClientID, | ||
| 1283 | resumeCapable: true, | 1287 | resumeCapable: true, | ||
| @@ -1285,6 +1289,8 @@ func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, | |||||
| 1285 | navigationCapabilities: h.NavigationCapabilities, | 1289 | navigationCapabilities: h.NavigationCapabilities, | ||
| 1286 | startupOverlay: h.StartupOverlay, | 1290 | startupOverlay: h.StartupOverlay, | ||
| 1287 | remoteOrigin: h.RemoteOrigin, | 1291 | remoteOrigin: h.RemoteOrigin, | ||
| 1292 | + | terminalCapabilities: terminalCapabilities, | |||
| 1293 | + | capabilitiesSet: true, | |||
| 1288 | } | 1294 | } | ||
| 1289 | if opts.remoteOrigin == "" && h.RemoteTarget != nil { | 1295 | if opts.remoteOrigin == "" && h.RemoteTarget != nil { | ||
| 1290 | opts.remoteOrigin = h.RemoteTarget.DisplayOrigin | 1296 | opts.remoteOrigin = h.RemoteTarget.DisplayOrigin | ||
| @@ -1420,7 +1426,6 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||||
| 1420 | if !sz.Valid() { | 1426 | if !sz.Valid() { | ||
| 1421 | return nil, nil, &protoErr{ports.ErrInternal, "invalid terminal size"} | 1427 | return nil, nil, &protoErr{ports.ErrInternal, "invalid terminal size"} | ||
| 1422 | } | 1428 | } | ||
| 1423 | - | term := terminalEnv{TrueColor: h.TrueColor} | |||
| 1424 | if h.Intent == ports.IntentResume && h.ResumeToken == 0 { | 1429 | if h.Intent == ports.IntentResume && h.ResumeToken == 0 { | ||
| 1425 | return nil, nil, &protoErr{ports.ErrNoSuchSession, "resume token is required"} | 1430 | return nil, nil, &protoErr{ports.ErrNoSuchSession, "resume token is required"} | ||
| 1426 | } | 1431 | } | ||
| @@ -1503,12 +1508,12 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||||
| 1503 | switch h.Intent { | 1508 | switch h.Intent { | ||
| 1504 | case ports.IntentEphemeral: | 1509 | case ports.IntentEphemeral: | ||
| 1505 | name := d.allocEphemeralNameLocked() | 1510 | name := d.allocEphemeralNameLocked() | ||
| 1506 | - | sess, err := d.createSessionLockedWithMode(name, true, h.Cwd, sz, term, h.Env) | 1511 | + | sess, err := d.createSessionLockedWithMode(name, true, h.Cwd, sz, h.Env) |
| 1507 | if err != nil { | 1512 | if err != nil { | ||
| 1508 | d.mu.Unlock() | 1513 | d.mu.Unlock() | ||
| 1509 | return nil, nil, err | 1514 | return nil, nil, err | ||
| 1510 | } | 1515 | } | ||
| 1511 | - | ac, err := d.finishRouteAttach(sess, tr, sz, term, h, true, true) | 1516 | + | ac, err := d.finishRouteAttach(sess, tr, sz, h, true, true) |
| 1512 | return sess, ac, err | 1517 | return sess, ac, err | ||
| 1513 | 1518 | ||||
| 1514 | case ports.IntentNew: | 1519 | case ports.IntentNew: | ||
| @@ -1524,12 +1529,12 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||||
| 1524 | d.mu.Unlock() | 1529 | d.mu.Unlock() | ||
| 1525 | return nil, nil, &protoErr{ports.ErrNameTaken, "session name already in use: " + h.Name} | 1530 | return nil, nil, &protoErr{ports.ErrNameTaken, "session name already in use: " + h.Name} | ||
| 1526 | } | 1531 | } | ||
| 1527 | - | sess, err := d.createSessionLockedWithMode(h.Name, false, h.Cwd, sz, term, h.Env) | 1532 | + | sess, err := d.createSessionLockedWithMode(h.Name, false, h.Cwd, sz, h.Env) |
| 1528 | if err != nil { | 1533 | if err != nil { | ||
| 1529 | d.mu.Unlock() | 1534 | d.mu.Unlock() | ||
| 1530 | return nil, nil, err | 1535 | return nil, nil, err | ||
| 1531 | } | 1536 | } | ||
| 1532 | - | ac, err := d.finishRouteAttach(sess, tr, sz, term, h, true, true) | 1537 | + | ac, err := d.finishRouteAttach(sess, tr, sz, h, true, true) |
| 1533 | return sess, ac, err | 1538 | return sess, ac, err | ||
| 1534 | 1539 | ||||
| 1535 | case ports.IntentAttach: | 1540 | case ports.IntentAttach: | ||
| @@ -1547,14 +1552,14 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||||
| 1547 | env = copyEnvironment(d.baseEnv) | 1552 | env = copyEnvironment(d.baseEnv) | ||
| 1548 | } | 1553 | } | ||
| 1549 | var err error | 1554 | var err error | ||
| 1550 | - | sess, err = d.createSessionLockedWithMode(h.Name, false, cwd, sz, term, env, stopped.tabNames) | 1555 | + | sess, err = d.createSessionLockedWithMode(h.Name, false, cwd, sz, env, stopped.tabNames) |
| 1551 | if err != nil { | 1556 | if err != nil { | ||
| 1552 | d.mu.Unlock() | 1557 | d.mu.Unlock() | ||
| 1553 | return nil, nil, err | 1558 | return nil, nil, err | ||
| 1554 | } | 1559 | } | ||
| 1555 | created = true | 1560 | created = true | ||
| 1556 | } | 1561 | } | ||
| 1557 | - | ac, err := d.finishRouteAttach(sess, tr, sz, term, h, created, false) | 1562 | + | ac, err := d.finishRouteAttach(sess, tr, sz, h, created, false) |
| 1558 | return sess, ac, err | 1563 | return sess, ac, err | ||
| 1559 | 1564 | ||||
| 1560 | default: | 1565 | default: | ||
M internal/usecase/daemon/daemon_test.go +6 -2
| @@ -68,10 +68,14 @@ func newTestDaemon(t testing.TB, ptys ports.PTYFactory, clk ports.Clock) *Daemon | |||
| 68 | 68 | return newTestDaemonWithCleanup(t, ptys, clk, true) | |
| 69 | 69 | } | |
| 70 | 70 | ||
| 71 | - | func createSessionForTest(d *Daemon, name string, ephemeral bool, cwd string, sz domain.Size, term terminalEnv, env []string, restoredTabNames ...[]string) (*session, error) { | |
| 71 | + | type terminalEnv struct { | |
| 72 | + | TrueColor bool | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func createSessionForTest(d *Daemon, name string, ephemeral bool, cwd string, sz domain.Size, _ terminalEnv, env []string, restoredTabNames ...[]string) (*session, error) { | |
| 72 | 76 | d.mu.Lock() | |
| 73 | 77 | defer d.mu.Unlock() | |
| 74 | - | return d.createSessionLocked(name, ephemeral, cwd, sz, term, env, restoredTabNames...) | |
| 78 | + | return d.createSessionLocked(name, ephemeral, cwd, sz, env, restoredTabNames...) | |
| 75 | 79 | } | |
| 76 | 80 | ||
| 77 | 81 | func newTestDaemonWithCleanup(t testing.TB, ptys ports.PTYFactory, clk ports.Clock, registerCleanup bool) *Daemon { | |
| @@ -68,10 +68,14 @@ func newTestDaemon(t testing.TB, ptys ports.PTYFactory, clk ports.Clock) *Daemon | |||||
| 68 | return newTestDaemonWithCleanup(t, ptys, clk, true) | 68 | return newTestDaemonWithCleanup(t, ptys, clk, true) | ||
| 69 | } | 69 | } | ||
| 70 | 70 | ||||
| 71 | - | func createSessionForTest(d *Daemon, name string, ephemeral bool, cwd string, sz domain.Size, term terminalEnv, env []string, restoredTabNames ...[]string) (*session, error) { | 71 | + | type terminalEnv struct { |
| 72 | + | TrueColor bool | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func createSessionForTest(d *Daemon, name string, ephemeral bool, cwd string, sz domain.Size, _ terminalEnv, env []string, restoredTabNames ...[]string) (*session, error) { | |||
| 72 | d.mu.Lock() | 76 | d.mu.Lock() | ||
| 73 | defer d.mu.Unlock() | 77 | defer d.mu.Unlock() | ||
| 74 | - | return d.createSessionLocked(name, ephemeral, cwd, sz, term, env, restoredTabNames...) | 78 | + | return d.createSessionLocked(name, ephemeral, cwd, sz, env, restoredTabNames...) |
| 75 | } | 79 | } | ||
| 76 | 80 | ||||
| 77 | func newTestDaemonWithCleanup(t testing.TB, ptys ports.PTYFactory, clk ports.Clock, registerCleanup bool) *Daemon { | 81 | func newTestDaemonWithCleanup(t testing.TB, ptys ports.PTYFactory, clk ports.Clock, registerCleanup bool) *Daemon { | ||
M internal/usecase/daemon/floating.go +2 -2
| @@ -350,7 +350,7 @@ func (d *Daemon) launchFloating(sess *session, tb *tab, cfg domain.FloatingConfi | |||
| 350 | 350 | ||
| 351 | 351 | func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.FloatingConfig, userOpen bool) (floatingLaunchSpec, error) { | |
| 352 | 352 | sess.mu.Lock() | |
| 353 | - | name, cwd, term, sessCtx := sess.name, sess.cwd, sess.terminal, sess.ctx | |
| 353 | + | name, cwd, sessCtx := sess.name, sess.cwd, sess.ctx | |
| 354 | 354 | env := copyEnvironment(sess.env) | |
| 355 | 355 | sess.mu.Unlock() | |
| 356 | 356 | tb.mu.Lock() | |
| @@ -387,7 +387,7 @@ func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.Floati | |||
| 387 | 387 | size: size, | |
| 388 | 388 | geometry: geometry, | |
| 389 | 389 | paneStableID: paneStableID, | |
| 390 | - | env: childEnvFrom(env, name, tabStableID, paneStableID, term), | |
| 390 | + | env: childEnvFrom(env, name, tabStableID, paneStableID), | |
| 391 | 391 | command: launch.command, | |
| 392 | 392 | args: launch.args, | |
| 393 | 393 | fallback: floatingCommandFallback(cfg.Command, launch.command), | |
| @@ -350,7 +350,7 @@ func (d *Daemon) launchFloating(sess *session, tb *tab, cfg domain.FloatingConfi | |||||
| 350 | 350 | ||||
| 351 | func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.FloatingConfig, userOpen bool) (floatingLaunchSpec, error) { | 351 | func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.FloatingConfig, userOpen bool) (floatingLaunchSpec, error) { | ||
| 352 | sess.mu.Lock() | 352 | sess.mu.Lock() | ||
| 353 | - | name, cwd, term, sessCtx := sess.name, sess.cwd, sess.terminal, sess.ctx | 353 | + | name, cwd, sessCtx := sess.name, sess.cwd, sess.ctx |
| 354 | env := copyEnvironment(sess.env) | 354 | env := copyEnvironment(sess.env) | ||
| 355 | sess.mu.Unlock() | 355 | sess.mu.Unlock() | ||
| 356 | tb.mu.Lock() | 356 | tb.mu.Lock() | ||
| @@ -387,7 +387,7 @@ func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.Floati | |||||
| 387 | size: size, | 387 | size: size, | ||
| 388 | geometry: geometry, | 388 | geometry: geometry, | ||
| 389 | paneStableID: paneStableID, | 389 | paneStableID: paneStableID, | ||
| 390 | - | env: childEnvFrom(env, name, tabStableID, paneStableID, term), | 390 | + | env: childEnvFrom(env, name, tabStableID, paneStableID), |
| 391 | command: launch.command, | 391 | command: launch.command, | ||
| 392 | args: launch.args, | 392 | args: launch.args, | ||
| 393 | fallback: floatingCommandFallback(cfg.Command, launch.command), | 393 | fallback: floatingCommandFallback(cfg.Command, launch.command), | ||
M internal/usecase/daemon/floating_test.go +2 -2
| @@ -172,7 +172,7 @@ func TestFloatingLifecycleCapturesLaunchBeforeOpenAndDoesNotHoldTabLock(t *testi | |||
| 172 | 172 | cwd := t.TempDir() | |
| 173 | 173 | tb := newTabWithStableID("t_stable", "p_normal", newBlockingPanePTY(t), domain.Size{Cols: 100, Rows: 40}) | |
| 174 | 174 | tb.ctx, tb.cancel = context.WithCancel(t.Context()) | |
| 175 | - | sess := &session{sessionCore: sessionCore{name: "work"}, cwd: cwd, terminal: terminalEnv{TrueColor: true}, env: []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/bin/custom-shell", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old"}, tabs: []*tab{tb}, ctx: t.Context()} | |
| 175 | + | sess := &session{sessionCore: sessionCore{name: "work"}, cwd: cwd, env: []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/bin/custom-shell", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old"}, tabs: []*tab{tb}, ctx: t.Context()} | |
| 176 | 176 | d.ApplyConfig(domain.Config{Theme: domain.ThemeDark, Floating: domain.FloatingConfig{Command: "btop --utf", Width: 50, Height: 50}}) | |
| 177 | 177 | d.ensureFloatingWarm(sess, tb) | |
| 178 | 178 | // Open has started while this goroutine owns tab.mu: an external factory | |
| @@ -196,7 +196,7 @@ func TestFloatingLifecycleCapturesLaunchBeforeOpenAndDoesNotHoldTabLock(t *testi | |||
| 196 | 196 | floating := tb.floating.pane | |
| 197 | 197 | tb.mu.Unlock() | |
| 198 | 198 | require.NotNil(t, floating) | |
| 199 | - | require.Equal(t, []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/bin/custom-shell", "TERM=xterm-direct", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + tb.stableID + ",pane=" + floating.stableID}, openCall.env) | |
| 199 | + | require.Equal(t, []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/bin/custom-shell", "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + tb.stableID + ",pane=" + floating.stableID}, openCall.env) | |
| 200 | 200 | assertPaneDefaultColors(t, floating, themeui.BuiltinDark.Foreground, themeui.BuiltinDark.Background) | |
| 201 | 201 | assertPaneColorScheme(t, floating, false) | |
| 202 | 202 | require.Equal(t, "/bin/custom-shell", openCall.command) | |
| @@ -172,7 +172,7 @@ func TestFloatingLifecycleCapturesLaunchBeforeOpenAndDoesNotHoldTabLock(t *testi | |||||
| 172 | cwd := t.TempDir() | 172 | cwd := t.TempDir() | ||
| 173 | tb := newTabWithStableID("t_stable", "p_normal", newBlockingPanePTY(t), domain.Size{Cols: 100, Rows: 40}) | 173 | tb := newTabWithStableID("t_stable", "p_normal", newBlockingPanePTY(t), domain.Size{Cols: 100, Rows: 40}) | ||
| 174 | tb.ctx, tb.cancel = context.WithCancel(t.Context()) | 174 | tb.ctx, tb.cancel = context.WithCancel(t.Context()) | ||
| 175 | - | sess := &session{sessionCore: sessionCore{name: "work"}, cwd: cwd, terminal: terminalEnv{TrueColor: true}, env: []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/bin/custom-shell", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old"}, tabs: []*tab{tb}, ctx: t.Context()} | 175 | + | sess := &session{sessionCore: sessionCore{name: "work"}, cwd: cwd, env: []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/bin/custom-shell", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old"}, tabs: []*tab{tb}, ctx: t.Context()} |
| 176 | d.ApplyConfig(domain.Config{Theme: domain.ThemeDark, Floating: domain.FloatingConfig{Command: "btop --utf", Width: 50, Height: 50}}) | 176 | d.ApplyConfig(domain.Config{Theme: domain.ThemeDark, Floating: domain.FloatingConfig{Command: "btop --utf", Width: 50, Height: 50}}) | ||
| 177 | d.ensureFloatingWarm(sess, tb) | 177 | d.ensureFloatingWarm(sess, tb) | ||
| 178 | // Open has started while this goroutine owns tab.mu: an external factory | 178 | // Open has started while this goroutine owns tab.mu: an external factory | ||
| @@ -196,7 +196,7 @@ func TestFloatingLifecycleCapturesLaunchBeforeOpenAndDoesNotHoldTabLock(t *testi | |||||
| 196 | floating := tb.floating.pane | 196 | floating := tb.floating.pane | ||
| 197 | tb.mu.Unlock() | 197 | tb.mu.Unlock() | ||
| 198 | require.NotNil(t, floating) | 198 | require.NotNil(t, floating) | ||
| 199 | - | require.Equal(t, []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/bin/custom-shell", "TERM=xterm-direct", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + tb.stableID + ",pane=" + floating.stableID}, openCall.env) | 199 | + | require.Equal(t, []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/bin/custom-shell", "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + tb.stableID + ",pane=" + floating.stableID}, openCall.env) |
| 200 | assertPaneDefaultColors(t, floating, themeui.BuiltinDark.Foreground, themeui.BuiltinDark.Background) | 200 | assertPaneDefaultColors(t, floating, themeui.BuiltinDark.Foreground, themeui.BuiltinDark.Background) | ||
| 201 | assertPaneColorScheme(t, floating, false) | 201 | assertPaneColorScheme(t, floating, false) | ||
| 202 | require.Equal(t, "/bin/custom-shell", openCall.command) | 202 | require.Equal(t, "/bin/custom-shell", openCall.command) | ||
M internal/usecase/daemon/pane_actions.go +1 -2
| @@ -48,7 +48,6 @@ func (d *Daemon) spawnPaneOpAt( | |||
| 48 | 48 | ||
| 49 | 49 | sess.mu.Lock() | |
| 50 | 50 | name, cwd := sess.name, sess.cwd | |
| 51 | - | term := sess.terminal | |
| 52 | 51 | env := copyEnvironment(sess.env) | |
| 53 | 52 | sess.mu.Unlock() | |
| 54 | 53 | ||
| @@ -89,7 +88,7 @@ func (d *Daemon) spawnPaneOpAt( | |||
| 89 | 88 | } | |
| 90 | 89 | launch := d.shellLaunch(env) | |
| 91 | 90 | lifetime := d.newPaneProcessLifetime(tb.ctx) | |
| 92 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID, term), cwd, rectSize(newRect)) | |
| 91 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, rectSize(newRect)) | |
| 93 | 92 | if err != nil { | |
| 94 | 93 | lifetime.abort() | |
| 95 | 94 | if pty != nil { | |
| @@ -48,7 +48,6 @@ func (d *Daemon) spawnPaneOpAt( | |||||
| 48 | 48 | ||||
| 49 | sess.mu.Lock() | 49 | sess.mu.Lock() | ||
| 50 | name, cwd := sess.name, sess.cwd | 50 | name, cwd := sess.name, sess.cwd | ||
| 51 | - | term := sess.terminal | |||
| 52 | env := copyEnvironment(sess.env) | 51 | env := copyEnvironment(sess.env) | ||
| 53 | sess.mu.Unlock() | 52 | sess.mu.Unlock() | ||
| 54 | 53 | ||||
| @@ -89,7 +88,7 @@ func (d *Daemon) spawnPaneOpAt( | |||||
| 89 | } | 88 | } | ||
| 90 | launch := d.shellLaunch(env) | 89 | launch := d.shellLaunch(env) | ||
| 91 | lifetime := d.newPaneProcessLifetime(tb.ctx) | 90 | lifetime := d.newPaneProcessLifetime(tb.ctx) | ||
| 92 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID, term), cwd, rectSize(newRect)) | 91 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, rectSize(newRect)) |
| 93 | if err != nil { | 92 | if err != nil { | ||
| 94 | lifetime.abort() | 93 | lifetime.abort() | ||
| 95 | if pty != nil { | 94 | if pty != nil { | ||
M internal/usecase/daemon/pane_actions_test.go +2 -9
| @@ -961,23 +961,16 @@ func blockingRead(t *testing.T) func([]byte) (int, error) { | |||
| 961 | 961 | func TestSplitPaneUsesExactAuthoritativeEnvironment(t *testing.T) { | |
| 962 | 962 | for _, tt := range []struct { | |
| 963 | 963 | name string | |
| 964 | - | term terminalEnv | |
| 965 | 964 | want []string | |
| 966 | 965 | }{ | |
| 967 | 966 | { | |
| 968 | - | name: "truecolor", | |
| 969 | - | term: terminalEnv{TrueColor: true}, | |
| 970 | - | want: []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/usr/bin/fish", "TERM=xterm-direct", "COLORTERM=truecolor", "TERM_PROGRAM=vev"}, | |
| 971 | - | }, | |
| 972 | - | { | |
| 973 | - | name: "no truecolor", | |
| 974 | - | want: []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/usr/bin/fish", "TERM=xterm-256color", "TERM_PROGRAM=vev"}, | |
| 967 | + | name: "fixed pane terminal", | |
| 968 | + | want: []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/usr/bin/fish", "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev"}, | |
| 975 | 969 | }, | |
| 976 | 970 | } { | |
| 977 | 971 | t.Run(tt.name, func(t *testing.T) { | |
| 978 | 972 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10}) | |
| 979 | 973 | d.shellOverride = false | |
| 980 | - | sess.terminal = tt.term | |
| 981 | 974 | sess.env = []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/usr/bin/fish", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old"} | |
| 982 | 975 | newPTY := portsmocks.NewMockPTY(t) | |
| 983 | 976 | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | |
| @@ -961,23 +961,16 @@ func blockingRead(t *testing.T) func([]byte) (int, error) { | |||||
| 961 | func TestSplitPaneUsesExactAuthoritativeEnvironment(t *testing.T) { | 961 | func TestSplitPaneUsesExactAuthoritativeEnvironment(t *testing.T) { | ||
| 962 | for _, tt := range []struct { | 962 | for _, tt := range []struct { | ||
| 963 | name string | 963 | name string | ||
| 964 | - | term terminalEnv | |||
| 965 | want []string | 964 | want []string | ||
| 966 | }{ | 965 | }{ | ||
| 967 | { | 966 | { | ||
| 968 | - | name: "truecolor", | 967 | + | name: "fixed pane terminal", |
| 969 | - | term: terminalEnv{TrueColor: true}, | 968 | + | want: []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/usr/bin/fish", "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev"}, |
| 970 | - | want: []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/usr/bin/fish", "TERM=xterm-direct", "COLORTERM=truecolor", "TERM_PROGRAM=vev"}, | |||
| 971 | - | }, | |||
| 972 | - | { | |||
| 973 | - | name: "no truecolor", | |||
| 974 | - | want: []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/usr/bin/fish", "TERM=xterm-256color", "TERM_PROGRAM=vev"}, | |||
| 975 | }, | 969 | }, | ||
| 976 | } { | 970 | } { | ||
| 977 | t.Run(tt.name, func(t *testing.T) { | 971 | t.Run(tt.name, func(t *testing.T) { | ||
| 978 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10}) | 972 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10}) | ||
| 979 | d.shellOverride = false | 973 | d.shellOverride = false | ||
| 980 | - | sess.terminal = tt.term | |||
| 981 | sess.env = []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/usr/bin/fish", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old"} | 974 | sess.env = []string{"ORDINARY=preserved", "DUP=first", "DUP=second", "PAIR=a=b", "SHELL=/bin/first", "SHELL=/usr/bin/fish", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old"} | ||
| 982 | newPTY := portsmocks.NewMockPTY(t) | 975 | newPTY := portsmocks.NewMockPTY(t) | ||
| 983 | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | 976 | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | ||
M internal/usecase/daemon/picker.go +3 -4
| @@ -1282,9 +1282,9 @@ func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, | |||
| 1282 | 1282 | return nil, errAttachmentTransition | |
| 1283 | 1283 | } | |
| 1284 | 1284 | from.mu.Lock() | |
| 1285 | - | cwd, term, env := d.dirOrHome(current.cwd), from.terminal, copyEnvironment(from.env) | |
| 1285 | + | cwd, env := d.dirOrHome(current.cwd), copyEnvironment(from.env) | |
| 1286 | 1286 | from.mu.Unlock() | |
| 1287 | - | created, createErr := d.createSessionLocked(target.Name, false, cwd, ac.sizeSnapshot(), term, env, current.tabNames) | |
| 1287 | + | created, createErr := d.createSessionLocked(target.Name, false, cwd, ac.sizeSnapshot(), env, current.tabNames) | |
| 1288 | 1288 | targetSess = created | |
| 1289 | 1289 | return created, createErr | |
| 1290 | 1290 | }, | |
| @@ -1308,11 +1308,10 @@ func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, | |||
| 1308 | 1308 | from.mu.Unlock() | |
| 1309 | 1309 | return nil, attachmentTransitionResult{}, false, nil | |
| 1310 | 1310 | } | |
| 1311 | - | term := from.terminal | |
| 1312 | 1311 | env := copyEnvironment(from.env) | |
| 1313 | 1312 | from.mu.Unlock() | |
| 1314 | 1313 | cwd := d.dirOrHome(stopped.cwd) | |
| 1315 | - | targetSess, err := d.createSessionLocked(target.Name, false, cwd, ac.sizeSnapshot(), term, env, stopped.tabNames) | |
| 1314 | + | targetSess, err := d.createSessionLocked(target.Name, false, cwd, ac.sizeSnapshot(), env, stopped.tabNames) | |
| 1316 | 1315 | if err != nil { | |
| 1317 | 1316 | d.log.Warn("resuming stopped session failed", "err", err, "session", target.Name) | |
| 1318 | 1317 | return nil, attachmentTransitionResult{}, false, err | |
| @@ -1282,9 +1282,9 @@ func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, | |||||
| 1282 | return nil, errAttachmentTransition | 1282 | return nil, errAttachmentTransition | ||
| 1283 | } | 1283 | } | ||
| 1284 | from.mu.Lock() | 1284 | from.mu.Lock() | ||
| 1285 | - | cwd, term, env := d.dirOrHome(current.cwd), from.terminal, copyEnvironment(from.env) | 1285 | + | cwd, env := d.dirOrHome(current.cwd), copyEnvironment(from.env) |
| 1286 | from.mu.Unlock() | 1286 | from.mu.Unlock() | ||
| 1287 | - | created, createErr := d.createSessionLocked(target.Name, false, cwd, ac.sizeSnapshot(), term, env, current.tabNames) | 1287 | + | created, createErr := d.createSessionLocked(target.Name, false, cwd, ac.sizeSnapshot(), env, current.tabNames) |
| 1288 | targetSess = created | 1288 | targetSess = created | ||
| 1289 | return created, createErr | 1289 | return created, createErr | ||
| 1290 | }, | 1290 | }, | ||
| @@ -1308,11 +1308,10 @@ func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, | |||||
| 1308 | from.mu.Unlock() | 1308 | from.mu.Unlock() | ||
| 1309 | return nil, attachmentTransitionResult{}, false, nil | 1309 | return nil, attachmentTransitionResult{}, false, nil | ||
| 1310 | } | 1310 | } | ||
| 1311 | - | term := from.terminal | |||
| 1312 | env := copyEnvironment(from.env) | 1311 | env := copyEnvironment(from.env) | ||
| 1313 | from.mu.Unlock() | 1312 | from.mu.Unlock() | ||
| 1314 | cwd := d.dirOrHome(stopped.cwd) | 1313 | cwd := d.dirOrHome(stopped.cwd) | ||
| 1315 | - | targetSess, err := d.createSessionLocked(target.Name, false, cwd, ac.sizeSnapshot(), term, env, stopped.tabNames) | 1314 | + | targetSess, err := d.createSessionLocked(target.Name, false, cwd, ac.sizeSnapshot(), env, stopped.tabNames) |
| 1316 | if err != nil { | 1315 | if err != nil { | ||
| 1317 | d.log.Warn("resuming stopped session failed", "err", err, "session", target.Name) | 1316 | d.log.Warn("resuming stopped session failed", "err", err, "session", target.Name) | ||
| 1318 | return nil, attachmentTransitionResult{}, false, err | 1317 | return nil, attachmentTransitionResult{}, false, err | ||
M internal/usecase/daemon/picker_test.go +1 -4
| @@ -1415,11 +1415,8 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 1415 | 1415 | d.resumeStoppedAndSwitch(sess, ac, picker.Target{Name: "old", Stopped: true}) | |
| 1416 | 1416 | got := ac.currentSession() | |
| 1417 | 1417 | require.NotNil(t, got) | |
| 1418 | - | got.mu.Lock() | |
| 1419 | - | require.True(t, got.terminal.TrueColor) | |
| 1420 | - | got.mu.Unlock() | |
| 1421 | 1418 | require.Len(t, opens, 2) | |
| 1422 | - | require.Contains(t, opens[1], "TERM=xterm-direct") | |
| 1419 | + | require.Contains(t, opens[1], "TERM=xterm-256color") | |
| 1423 | 1420 | require.Contains(t, opens[1], "COLORTERM=truecolor") | |
| 1424 | 1421 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | |
| 1425 | 1422 | require.Eventually(t, func() bool { | |
| @@ -1415,11 +1415,8 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 1415 | d.resumeStoppedAndSwitch(sess, ac, picker.Target{Name: "old", Stopped: true}) | 1415 | d.resumeStoppedAndSwitch(sess, ac, picker.Target{Name: "old", Stopped: true}) | ||
| 1416 | got := ac.currentSession() | 1416 | got := ac.currentSession() | ||
| 1417 | require.NotNil(t, got) | 1417 | require.NotNil(t, got) | ||
| 1418 | - | got.mu.Lock() | |||
| 1419 | - | require.True(t, got.terminal.TrueColor) | |||
| 1420 | - | got.mu.Unlock() | |||
| 1421 | require.Len(t, opens, 2) | 1418 | require.Len(t, opens, 2) | ||
| 1422 | - | require.Contains(t, opens[1], "TERM=xterm-direct") | 1419 | + | require.Contains(t, opens[1], "TERM=xterm-256color") |
| 1423 | require.Contains(t, opens[1], "COLORTERM=truecolor") | 1420 | require.Contains(t, opens[1], "COLORTERM=truecolor") | ||
| 1424 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | 1421 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | ||
| 1425 | require.Eventually(t, func() bool { | 1422 | require.Eventually(t, func() bool { | ||
M internal/usecase/daemon/remote_attach.go +5 -5
| @@ -48,7 +48,7 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello | |||
| 48 | 48 | d.mu.Unlock() | |
| 49 | 49 | return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote tab no longer exists"} | |
| 50 | 50 | } | |
| 51 | - | ac, err := d.finishRouteAttach(live, tr, h.Size, terminalEnv{TrueColor: h.TrueColor}, h, false, false) | |
| 51 | + | ac, err := d.finishRouteAttach(live, tr, h.Size, h, false, false) | |
| 52 | 52 | return live, ac, err | |
| 53 | 53 | } | |
| 54 | 54 | ||
| @@ -71,12 +71,12 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello | |||
| 71 | 71 | // trusted for this branch. | |
| 72 | 72 | env := copyEnvironment(d.baseEnv) | |
| 73 | 73 | cwd := d.dirOrHome(stopped.cwd) | |
| 74 | - | sess, err := d.createSessionLockedWithMode(target.SessionName, false, cwd, h.Size, terminalEnv{TrueColor: h.TrueColor}, env, stopped.tabNames) | |
| 74 | + | sess, err := d.createSessionLockedWithMode(target.SessionName, false, cwd, h.Size, env, stopped.tabNames) | |
| 75 | 75 | if err != nil { | |
| 76 | 76 | d.mu.Unlock() | |
| 77 | 77 | return nil, nil, err | |
| 78 | 78 | } | |
| 79 | - | ac, err := d.finishRouteAttach(sess, tr, h.Size, terminalEnv{TrueColor: h.TrueColor}, h, true, false) | |
| 79 | + | ac, err := d.finishRouteAttach(sess, tr, h.Size, h, true, false) | |
| 80 | 80 | return sess, ac, err | |
| 81 | 81 | } | |
| 82 | 82 | ||
| @@ -113,8 +113,8 @@ func (d *Daemon) sendCommittedRouteIdentityForAttachment(token attachmentConnect | |||
| 113 | 113 | return token.sendControl(ports.Frame{Type: ports.MsgCommittedRouteIdentity, Payload: payload}) | |
| 114 | 114 | } | |
| 115 | 115 | ||
| 116 | - | func (d *Daemon) finishRouteAttach(sess *session, tr ports.Transport, sz domain.Size, term terminalEnv, h ports.Hello, routeCreated, purge bool) (*attachedClient, error) { | |
| 117 | - | ac, err := d.finishAttach(sess, tr, sz, term, h) | |
| 116 | + | func (d *Daemon) finishRouteAttach(sess *session, tr ports.Transport, sz domain.Size, h ports.Hello, routeCreated, purge bool) (*attachedClient, error) { | |
| 117 | + | ac, err := d.finishAttach(sess, tr, sz, h) | |
| 118 | 118 | if err != nil && routeCreated { | |
| 119 | 119 | if cleanupErr := d.killSession(sess, ports.ReasonSessionKilled, purge); cleanupErr != nil { | |
| 120 | 120 | err = errors.Join(err, cleanupErr) | |
| @@ -48,7 +48,7 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello | |||||
| 48 | d.mu.Unlock() | 48 | d.mu.Unlock() | ||
| 49 | return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote tab no longer exists"} | 49 | return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote tab no longer exists"} | ||
| 50 | } | 50 | } | ||
| 51 | - | ac, err := d.finishRouteAttach(live, tr, h.Size, terminalEnv{TrueColor: h.TrueColor}, h, false, false) | 51 | + | ac, err := d.finishRouteAttach(live, tr, h.Size, h, false, false) |
| 52 | return live, ac, err | 52 | return live, ac, err | ||
| 53 | } | 53 | } | ||
| 54 | 54 | ||||
| @@ -71,12 +71,12 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello | |||||
| 71 | // trusted for this branch. | 71 | // trusted for this branch. | ||
| 72 | env := copyEnvironment(d.baseEnv) | 72 | env := copyEnvironment(d.baseEnv) | ||
| 73 | cwd := d.dirOrHome(stopped.cwd) | 73 | cwd := d.dirOrHome(stopped.cwd) | ||
| 74 | - | sess, err := d.createSessionLockedWithMode(target.SessionName, false, cwd, h.Size, terminalEnv{TrueColor: h.TrueColor}, env, stopped.tabNames) | 74 | + | sess, err := d.createSessionLockedWithMode(target.SessionName, false, cwd, h.Size, env, stopped.tabNames) |
| 75 | if err != nil { | 75 | if err != nil { | ||
| 76 | d.mu.Unlock() | 76 | d.mu.Unlock() | ||
| 77 | return nil, nil, err | 77 | return nil, nil, err | ||
| 78 | } | 78 | } | ||
| 79 | - | ac, err := d.finishRouteAttach(sess, tr, h.Size, terminalEnv{TrueColor: h.TrueColor}, h, true, false) | 79 | + | ac, err := d.finishRouteAttach(sess, tr, h.Size, h, true, false) |
| 80 | return sess, ac, err | 80 | return sess, ac, err | ||
| 81 | } | 81 | } | ||
| 82 | 82 | ||||
| @@ -113,8 +113,8 @@ func (d *Daemon) sendCommittedRouteIdentityForAttachment(token attachmentConnect | |||||
| 113 | return token.sendControl(ports.Frame{Type: ports.MsgCommittedRouteIdentity, Payload: payload}) | 113 | return token.sendControl(ports.Frame{Type: ports.MsgCommittedRouteIdentity, Payload: payload}) | ||
| 114 | } | 114 | } | ||
| 115 | 115 | ||||
| 116 | - | func (d *Daemon) finishRouteAttach(sess *session, tr ports.Transport, sz domain.Size, term terminalEnv, h ports.Hello, routeCreated, purge bool) (*attachedClient, error) { | 116 | + | func (d *Daemon) finishRouteAttach(sess *session, tr ports.Transport, sz domain.Size, h ports.Hello, routeCreated, purge bool) (*attachedClient, error) { |
| 117 | - | ac, err := d.finishAttach(sess, tr, sz, term, h) | 117 | + | ac, err := d.finishAttach(sess, tr, sz, h) |
| 118 | if err != nil && routeCreated { | 118 | if err != nil && routeCreated { | ||
| 119 | if cleanupErr := d.killSession(sess, ports.ReasonSessionKilled, purge); cleanupErr != nil { | 119 | if cleanupErr := d.killSession(sess, ports.ReasonSessionKilled, purge); cleanupErr != nil { | ||
| 120 | err = errors.Join(err, cleanupErr) | 120 | err = errors.Join(err, cleanupErr) | ||
M internal/usecase/daemon/remote_route_test.go +1 -1
| @@ -40,7 +40,7 @@ func TestFinishRouteAttachRollsBackCreatedSession(t *testing.T) { | |||
| 40 | 40 | // The caller must hold d.mu on entry; finishRouteAttach releases it on | |
| 41 | 41 | // both success and error paths before returning. | |
| 42 | 42 | d.mu.Lock() | |
| 43 | - | _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, terminalEnv{}, hello, true, true) | |
| 43 | + | _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, hello, true, true) | |
| 44 | 44 | var protocol *protoErr | |
| 45 | 45 | require.ErrorAs(t, err, &protocol) | |
| 46 | 46 | require.Equal(t, ports.ErrNoSuchTarget, protocol.code) | |
| @@ -40,7 +40,7 @@ func TestFinishRouteAttachRollsBackCreatedSession(t *testing.T) { | |||||
| 40 | // The caller must hold d.mu on entry; finishRouteAttach releases it on | 40 | // The caller must hold d.mu on entry; finishRouteAttach releases it on | ||
| 41 | // both success and error paths before returning. | 41 | // both success and error paths before returning. | ||
| 42 | d.mu.Lock() | 42 | d.mu.Lock() | ||
| 43 | - | _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, terminalEnv{}, hello, true, true) | 43 | + | _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, hello, true, true) |
| 44 | var protocol *protoErr | 44 | var protocol *protoErr | ||
| 45 | require.ErrorAs(t, err, &protocol) | 45 | require.ErrorAs(t, err, &protocol) | ||
| 46 | require.Equal(t, ports.ErrNoSuchTarget, protocol.code) | 46 | require.Equal(t, ports.ErrNoSuchTarget, protocol.code) | ||
M internal/usecase/daemon/render_pipeline.go +1 -0
| @@ -468,6 +468,7 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe | |||
| 468 | 468 | } | |
| 469 | 469 | } | |
| 470 | 470 | endDiff := marks.span(ports.RuntimeDiffStart, ports.RuntimeDiffEnd, 0) | |
| 471 | + | composed.frame = adaptFrameColors(composed.frame, ac.terminalCapabilities.TrueColor()) | |
| 471 | 472 | var ( | |
| 472 | 473 | preparedANSI *preparedOutput | |
| 473 | 474 | err error | |
| @@ -468,6 +468,7 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe | |||||
| 468 | } | 468 | } | ||
| 469 | } | 469 | } | ||
| 470 | endDiff := marks.span(ports.RuntimeDiffStart, ports.RuntimeDiffEnd, 0) | 470 | endDiff := marks.span(ports.RuntimeDiffStart, ports.RuntimeDiffEnd, 0) | ||
| 471 | + | composed.frame = adaptFrameColors(composed.frame, ac.terminalCapabilities.TrueColor()) | |||
| 471 | var ( | 472 | var ( | ||
| 472 | preparedANSI *preparedOutput | 473 | preparedANSI *preparedOutput | ||
| 473 | err error | 474 | err error | ||
M internal/usecase/daemon/resume.go +0 -1
| @@ -639,7 +639,6 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain | |||
| 639 | 639 | if h.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned { | |
| 640 | 640 | sess.env = copyEnvironment(h.Env) | |
| 641 | 641 | } | |
| 642 | - | sess.terminal = terminalEnv{TrueColor: h.TrueColor} | |
| 643 | 642 | sess.mu.Unlock() | |
| 644 | 643 | preferredTabIndex := preferredTabIndex(sess, h.PreferredTabID) | |
| 645 | 644 | // Resume preparation used sendMu -> d.mu, but freeze/drain must run with | |
| @@ -639,7 +639,6 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain | |||||
| 639 | if h.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned { | 639 | if h.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned { | ||
| 640 | sess.env = copyEnvironment(h.Env) | 640 | sess.env = copyEnvironment(h.Env) | ||
| 641 | } | 641 | } | ||
| 642 | - | sess.terminal = terminalEnv{TrueColor: h.TrueColor} | |||
| 643 | sess.mu.Unlock() | 642 | sess.mu.Unlock() | ||
| 644 | preferredTabIndex := preferredTabIndex(sess, h.PreferredTabID) | 643 | preferredTabIndex := preferredTabIndex(sess, h.PreferredTabID) | ||
| 645 | // Resume preparation used sendMu -> d.mu, but freeze/drain must run with | 644 | // Resume preparation used sendMu -> d.mu, but freeze/drain must run with | ||
M internal/usecase/daemon/resume_test.go +1 -2
| @@ -1607,11 +1607,10 @@ func TestResumeParkedReplacesFuturePTYEnvironment(t *testing.T) { | |||
| 1607 | 1607 | sess.mu.Lock() | |
| 1608 | 1608 | future := sess.tabs[1].focusedPane() | |
| 1609 | 1609 | futureTabID, futurePaneID := sess.tabs[1].stableID, future.stableID | |
| 1610 | - | require.True(t, sess.terminal.TrueColor) | |
| 1611 | 1610 | require.Equal(t, resumeHello.Env, sess.env) | |
| 1612 | 1611 | sess.mu.Unlock() | |
| 1613 | 1612 | require.Equal(t, []string{"/bin/sh", "/usr/bin/fish"}, commands) | |
| 1614 | - | require.Equal(t, []string{"SECRET=after", "PAIR=a=b", "SHELL=/usr/bin/fish", "TERM=xterm-direct", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + futureTabID + ",pane=" + futurePaneID}, envs[1]) | |
| 1613 | + | require.Equal(t, []string{"SECRET=after", "PAIR=a=b", "SHELL=/usr/bin/fish", "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + futureTabID + ",pane=" + futurePaneID}, envs[1]) | |
| 1615 | 1614 | } | |
| 1616 | 1615 | ||
| 1617 | 1616 | func TestResumeParkedDaemonOwnedPreservesSessionEnvironment(t *testing.T) { | |
| @@ -1607,11 +1607,10 @@ func TestResumeParkedReplacesFuturePTYEnvironment(t *testing.T) { | |||||
| 1607 | sess.mu.Lock() | 1607 | sess.mu.Lock() | ||
| 1608 | future := sess.tabs[1].focusedPane() | 1608 | future := sess.tabs[1].focusedPane() | ||
| 1609 | futureTabID, futurePaneID := sess.tabs[1].stableID, future.stableID | 1609 | futureTabID, futurePaneID := sess.tabs[1].stableID, future.stableID | ||
| 1610 | - | require.True(t, sess.terminal.TrueColor) | |||
| 1611 | require.Equal(t, resumeHello.Env, sess.env) | 1610 | require.Equal(t, resumeHello.Env, sess.env) | ||
| 1612 | sess.mu.Unlock() | 1611 | sess.mu.Unlock() | ||
| 1613 | require.Equal(t, []string{"/bin/sh", "/usr/bin/fish"}, commands) | 1612 | require.Equal(t, []string{"/bin/sh", "/usr/bin/fish"}, commands) | ||
| 1614 | - | require.Equal(t, []string{"SECRET=after", "PAIR=a=b", "SHELL=/usr/bin/fish", "TERM=xterm-direct", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + futureTabID + ",pane=" + futurePaneID}, envs[1]) | 1613 | + | require.Equal(t, []string{"SECRET=after", "PAIR=a=b", "SHELL=/usr/bin/fish", "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + futureTabID + ",pane=" + futurePaneID}, envs[1]) |
| 1615 | } | 1614 | } | ||
| 1616 | 1615 | ||||
| 1617 | func TestResumeParkedDaemonOwnedPreservesSessionEnvironment(t *testing.T) { | 1616 | func TestResumeParkedDaemonOwnedPreservesSessionEnvironment(t *testing.T) { | ||
M internal/usecase/daemon/session.go +16 -45
| @@ -25,10 +25,6 @@ var ( | |||
| 25 | 25 | errSessionNameInUse = errors.New("name already in use") | |
| 26 | 26 | ) | |
| 27 | 27 | ||
| 28 | - | type terminalEnv struct { | |
| 29 | - | TrueColor bool | |
| 30 | - | } | |
| 31 | - | ||
| 32 | 28 | type session struct { | |
| 33 | 29 | sessionCore | |
| 34 | 30 | ||
| @@ -62,7 +58,6 @@ type session struct { | |||
| 62 | 58 | clipboardQueue []clipboardForward | |
| 63 | 59 | clipboardWorkerRunning bool | |
| 64 | 60 | cwd string | |
| 65 | - | terminal terminalEnv | |
| 66 | 61 | // env is the authoritative immutable environment snapshot for future PTY children. | |
| 67 | 62 | // It is guarded by mu and is always copied on ingress and egress. | |
| 68 | 63 | env []string | |
| @@ -187,11 +182,11 @@ func (d *Daemon) touchMRU(entry *session) { | |||
| 187 | 182 | entry.mu.Unlock() | |
| 188 | 183 | } | |
| 189 | 184 | ||
| 190 | - | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz domain.Size, term terminalEnv, env []string, restoredTabNames ...[]string) (*session, error) { | |
| 191 | - | return d.createSessionLockedWithMode(name, ephemeral, cwd, sz, term, env, restoredTabNames...) | |
| 185 | + | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz domain.Size, env []string, restoredTabNames ...[]string) (*session, error) { | |
| 186 | + | return d.createSessionLockedWithMode(name, ephemeral, cwd, sz, env, restoredTabNames...) | |
| 192 | 187 | } | |
| 193 | 188 | ||
| 194 | - | func (d *Daemon) createSessionLockedWithMode(name string, ephemeral bool, cwd string, sz domain.Size, term terminalEnv, env []string, restoredTabNames ...[]string) (*session, error) { | |
| 189 | + | func (d *Daemon) createSessionLockedWithMode(name string, ephemeral bool, cwd string, sz domain.Size, env []string, restoredTabNames ...[]string) (*session, error) { | |
| 195 | 190 | env = copyEnvironment(env) | |
| 196 | 191 | if _, reserved := d.creating[name]; reserved { | |
| 197 | 192 | return nil, errSessionNameInUse | |
| @@ -272,7 +267,7 @@ func (d *Daemon) createSessionLockedWithMode(name string, ephemeral bool, cwd st | |||
| 272 | 267 | } | |
| 273 | 268 | launch := d.shellLaunch(env) | |
| 274 | 269 | lifetime := d.newPaneProcessLifetime(d.serveCtx) | |
| 275 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID, term), cwd, tbSize) | |
| 270 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, tbSize) | |
| 276 | 271 | if err != nil { | |
| 277 | 272 | lifetime.abort() | |
| 278 | 273 | if pty != nil { | |
| @@ -321,7 +316,6 @@ func (d *Daemon) createSessionLockedWithMode(name string, ephemeral bool, cwd st | |||
| 321 | 316 | cancel: cancel, | |
| 322 | 317 | tabs: tabs, | |
| 323 | 318 | cwd: cwd, | |
| 324 | - | terminal: term, | |
| 325 | 319 | env: env, | |
| 326 | 320 | snapshotWake: d.snapshotWake, | |
| 327 | 321 | } | |
| @@ -430,7 +424,6 @@ func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name | |||
| 430 | 424 | } | |
| 431 | 425 | from.mu.Lock() | |
| 432 | 426 | cwd := from.cwd | |
| 433 | - | term := from.terminal | |
| 434 | 427 | env := copyEnvironment(from.env) | |
| 435 | 428 | _, attached := from.attachments[ac] | |
| 436 | 429 | from.mu.Unlock() | |
| @@ -439,7 +432,7 @@ func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name | |||
| 439 | 432 | return errors.New("client detached") | |
| 440 | 433 | } | |
| 441 | 434 | ||
| 442 | - | newSess, err := d.createSessionLockedWithMode(name, false, cwd, sz, term, env) | |
| 435 | + | newSess, err := d.createSessionLockedWithMode(name, false, cwd, sz, env) | |
| 443 | 436 | d.mu.Unlock() | |
| 444 | 437 | if err != nil { | |
| 445 | 438 | return err | |
| @@ -500,10 +493,10 @@ func (d *Daemon) createSessionAndSwitchForAttachment(token attachmentConnectionT | |||
| 500 | 493 | return nil, errSessionNameInUse | |
| 501 | 494 | } | |
| 502 | 495 | source.mu.Lock() | |
| 503 | - | cwd, term, env := source.cwd, source.terminal, copyEnvironment(source.env) | |
| 496 | + | cwd, env := source.cwd, copyEnvironment(source.env) | |
| 504 | 497 | source.mu.Unlock() | |
| 505 | 498 | var createErr error | |
| 506 | - | created, createErr = d.createSessionLockedWithMode(name, false, cwd, token.ac.sizeSnapshot(), term, env) | |
| 499 | + | created, createErr = d.createSessionLockedWithMode(name, false, cwd, token.ac.sizeSnapshot(), env) | |
| 507 | 500 | return created, createErr | |
| 508 | 501 | }, | |
| 509 | 502 | }) | |
| @@ -529,7 +522,7 @@ func (d *Daemon) createEphemeralSessionAndSwitch(from *session, ac *attachedClie | |||
| 529 | 522 | return errors.New("daemon is shutting down") | |
| 530 | 523 | } | |
| 531 | 524 | from.mu.Lock() | |
| 532 | - | cwd, term, env := from.cwd, from.terminal, copyEnvironment(from.env) | |
| 525 | + | cwd, env := from.cwd, copyEnvironment(from.env) | |
| 533 | 526 | _, attached := from.attachments[ac] | |
| 534 | 527 | from.mu.Unlock() | |
| 535 | 528 | if !attached { | |
| @@ -537,7 +530,7 @@ func (d *Daemon) createEphemeralSessionAndSwitch(from *session, ac *attachedClie | |||
| 537 | 530 | return errors.New("client detached") | |
| 538 | 531 | } | |
| 539 | 532 | name := d.allocEphemeralNameLocked() | |
| 540 | - | newSess, err := d.createSessionLockedWithMode(name, true, cwd, sz, term, env) | |
| 533 | + | newSess, err := d.createSessionLockedWithMode(name, true, cwd, sz, env) | |
| 541 | 534 | d.mu.Unlock() | |
| 542 | 535 | if err != nil { | |
| 543 | 536 | return err | |
| @@ -575,11 +568,11 @@ func (d *Daemon) createEphemeralSessionAndSwitchForAttachment(token attachmentCo | |||
| 575 | 568 | } | |
| 576 | 569 | source := token.sess | |
| 577 | 570 | source.mu.Lock() | |
| 578 | - | cwd, term, env := source.cwd, source.terminal, copyEnvironment(source.env) | |
| 571 | + | cwd, env := source.cwd, copyEnvironment(source.env) | |
| 579 | 572 | source.mu.Unlock() | |
| 580 | 573 | name := d.allocEphemeralNameLocked() | |
| 581 | 574 | var createErr error | |
| 582 | - | created, createErr = d.createSessionLockedWithMode(name, true, cwd, token.ac.sizeSnapshot(), term, env) | |
| 575 | + | created, createErr = d.createSessionLockedWithMode(name, true, cwd, token.ac.sizeSnapshot(), env) | |
| 583 | 576 | return created, createErr | |
| 584 | 577 | }, | |
| 585 | 578 | }) | |
| @@ -604,7 +597,6 @@ func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, _ dom | |||
| 604 | 597 | sess.mu.Lock() | |
| 605 | 598 | name := sess.name | |
| 606 | 599 | cwd := sess.cwd | |
| 607 | - | term := sess.terminal | |
| 608 | 600 | env := copyEnvironment(sess.env) | |
| 609 | 601 | attachments := sess.snapshotAttachmentsLocked() | |
| 610 | 602 | sess.mu.Unlock() | |
| @@ -615,7 +607,7 @@ func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, _ dom | |||
| 615 | 607 | } | |
| 616 | 608 | launch := d.shellLaunch(env) | |
| 617 | 609 | lifetime := d.newPaneProcessLifetime(sess.ctx) | |
| 618 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID, term), cwd, tbSize) | |
| 610 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, tbSize) | |
| 619 | 611 | if err != nil { | |
| 620 | 612 | lifetime.abort() | |
| 621 | 613 | if pty != nil { | |
| @@ -1881,8 +1873,8 @@ func (d *Daemon) refreshSessionCwd(sess *session) { | |||
| 1881 | 1873 | ||
| 1882 | 1874 | // childEnv retains the daemon-environment helper for daemon-local legacy callers. | |
| 1883 | 1875 | // Interactive PTY launch paths use childEnvFrom with their session snapshot. | |
| 1884 | - | func (d *Daemon) childEnv(name, tabStableID, paneStableID string, term terminalEnv) []string { | |
| 1885 | - | return childEnvFrom(d.baseEnv, name, tabStableID, paneStableID, term) | |
| 1876 | + | func (d *Daemon) childEnv(name, tabStableID, paneStableID string) []string { | |
| 1877 | + | return childEnvFrom(d.baseEnv, name, tabStableID, paneStableID) | |
| 1886 | 1878 | } | |
| 1887 | 1879 | ||
| 1888 | 1880 | func copyEnvironment(env []string) []string { | |
| @@ -1894,23 +1886,6 @@ func environmentEntry(entry string) (name, value string, ok bool) { | |||
| 1894 | 1886 | return name, value, ok | |
| 1895 | 1887 | } | |
| 1896 | 1888 | ||
| 1897 | - | func terminalEnvFromEnvironment(env []string) terminalEnv { | |
| 1898 | - | var term, colorTerm string | |
| 1899 | - | for _, entry := range env { | |
| 1900 | - | name, value, ok := environmentEntry(entry) | |
| 1901 | - | if !ok { | |
| 1902 | - | continue | |
| 1903 | - | } | |
| 1904 | - | switch name { | |
| 1905 | - | case "TERM": | |
| 1906 | - | term = value | |
| 1907 | - | case "COLORTERM": | |
| 1908 | - | colorTerm = value | |
| 1909 | - | } | |
| 1910 | - | } | |
| 1911 | - | return terminalEnv{TrueColor: ports.DetectTrueColor(term, colorTerm)} | |
| 1912 | - | } | |
| 1913 | - | ||
| 1914 | 1889 | const defaultShellCommand = "/bin/sh" | |
| 1915 | 1890 | const defaultShellTitle = "sh" | |
| 1916 | 1891 | ||
| @@ -1956,7 +1931,7 @@ func shellFromEnvironment(env []string) string { | |||
| 1956 | 1931 | ||
| 1957 | 1932 | // childEnvFrom preserves the supplied environment byte-for-byte and in order, | |
| 1958 | 1933 | // except for exact reserved variable names which vev owns. | |
| 1959 | - | func childEnvFrom(env []string, name, tabStableID, paneStableID string, term terminalEnv) []string { | |
| 1934 | + | func childEnvFrom(env []string, name, tabStableID, paneStableID string) []string { | |
| 1960 | 1935 | out := make([]string, 0, len(env)+4) | |
| 1961 | 1936 | for _, entry := range env { | |
| 1962 | 1937 | key, _, ok := environmentEntry(entry) | |
| @@ -1965,11 +1940,7 @@ func childEnvFrom(env []string, name, tabStableID, paneStableID string, term ter | |||
| 1965 | 1940 | } | |
| 1966 | 1941 | out = append(out, entry) | |
| 1967 | 1942 | } | |
| 1968 | - | if term.TrueColor { | |
| 1969 | - | out = append(out, "TERM=xterm-direct", "COLORTERM=truecolor") | |
| 1970 | - | } else { | |
| 1971 | - | out = append(out, "TERM=xterm-256color") | |
| 1972 | - | } | |
| 1943 | + | out = append(out, "TERM=xterm-256color", "COLORTERM=truecolor") | |
| 1973 | 1944 | return append(out, | |
| 1974 | 1945 | "TERM_PROGRAM=vev", | |
| 1975 | 1946 | "VEV=session="+escapeVEVComponent(name)+",tab="+tabStableID+",pane="+paneStableID, | |
| @@ -25,10 +25,6 @@ var ( | |||||
| 25 | errSessionNameInUse = errors.New("name already in use") | 25 | errSessionNameInUse = errors.New("name already in use") | ||
| 26 | ) | 26 | ) | ||
| 27 | 27 | ||||
| 28 | - | type terminalEnv struct { | |||
| 29 | - | TrueColor bool | |||
| 30 | - | } | |||
| 31 | - | ||||
| 32 | type session struct { | 28 | type session struct { | ||
| 33 | sessionCore | 29 | sessionCore | ||
| 34 | 30 | ||||
| @@ -62,7 +58,6 @@ type session struct { | |||||
| 62 | clipboardQueue []clipboardForward | 58 | clipboardQueue []clipboardForward | ||
| 63 | clipboardWorkerRunning bool | 59 | clipboardWorkerRunning bool | ||
| 64 | cwd string | 60 | cwd string | ||
| 65 | - | terminal terminalEnv | |||
| 66 | // env is the authoritative immutable environment snapshot for future PTY children. | 61 | // env is the authoritative immutable environment snapshot for future PTY children. | ||
| 67 | // It is guarded by mu and is always copied on ingress and egress. | 62 | // It is guarded by mu and is always copied on ingress and egress. | ||
| 68 | env []string | 63 | env []string | ||
| @@ -187,11 +182,11 @@ func (d *Daemon) touchMRU(entry *session) { | |||||
| 187 | entry.mu.Unlock() | 182 | entry.mu.Unlock() | ||
| 188 | } | 183 | } | ||
| 189 | 184 | ||||
| 190 | - | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz domain.Size, term terminalEnv, env []string, restoredTabNames ...[]string) (*session, error) { | 185 | + | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz domain.Size, env []string, restoredTabNames ...[]string) (*session, error) { |
| 191 | - | return d.createSessionLockedWithMode(name, ephemeral, cwd, sz, term, env, restoredTabNames...) | 186 | + | return d.createSessionLockedWithMode(name, ephemeral, cwd, sz, env, restoredTabNames...) |
| 192 | } | 187 | } | ||
| 193 | 188 | ||||
| 194 | - | func (d *Daemon) createSessionLockedWithMode(name string, ephemeral bool, cwd string, sz domain.Size, term terminalEnv, env []string, restoredTabNames ...[]string) (*session, error) { | 189 | + | func (d *Daemon) createSessionLockedWithMode(name string, ephemeral bool, cwd string, sz domain.Size, env []string, restoredTabNames ...[]string) (*session, error) { |
| 195 | env = copyEnvironment(env) | 190 | env = copyEnvironment(env) | ||
| 196 | if _, reserved := d.creating[name]; reserved { | 191 | if _, reserved := d.creating[name]; reserved { | ||
| 197 | return nil, errSessionNameInUse | 192 | return nil, errSessionNameInUse | ||
| @@ -272,7 +267,7 @@ func (d *Daemon) createSessionLockedWithMode(name string, ephemeral bool, cwd st | |||||
| 272 | } | 267 | } | ||
| 273 | launch := d.shellLaunch(env) | 268 | launch := d.shellLaunch(env) | ||
| 274 | lifetime := d.newPaneProcessLifetime(d.serveCtx) | 269 | lifetime := d.newPaneProcessLifetime(d.serveCtx) | ||
| 275 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID, term), cwd, tbSize) | 270 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, tbSize) |
| 276 | if err != nil { | 271 | if err != nil { | ||
| 277 | lifetime.abort() | 272 | lifetime.abort() | ||
| 278 | if pty != nil { | 273 | if pty != nil { | ||
| @@ -321,7 +316,6 @@ func (d *Daemon) createSessionLockedWithMode(name string, ephemeral bool, cwd st | |||||
| 321 | cancel: cancel, | 316 | cancel: cancel, | ||
| 322 | tabs: tabs, | 317 | tabs: tabs, | ||
| 323 | cwd: cwd, | 318 | cwd: cwd, | ||
| 324 | - | terminal: term, | |||
| 325 | env: env, | 319 | env: env, | ||
| 326 | snapshotWake: d.snapshotWake, | 320 | snapshotWake: d.snapshotWake, | ||
| 327 | } | 321 | } | ||
| @@ -430,7 +424,6 @@ func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name | |||||
| 430 | } | 424 | } | ||
| 431 | from.mu.Lock() | 425 | from.mu.Lock() | ||
| 432 | cwd := from.cwd | 426 | cwd := from.cwd | ||
| 433 | - | term := from.terminal | |||
| 434 | env := copyEnvironment(from.env) | 427 | env := copyEnvironment(from.env) | ||
| 435 | _, attached := from.attachments[ac] | 428 | _, attached := from.attachments[ac] | ||
| 436 | from.mu.Unlock() | 429 | from.mu.Unlock() | ||
| @@ -439,7 +432,7 @@ func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name | |||||
| 439 | return errors.New("client detached") | 432 | return errors.New("client detached") | ||
| 440 | } | 433 | } | ||
| 441 | 434 | ||||
| 442 | - | newSess, err := d.createSessionLockedWithMode(name, false, cwd, sz, term, env) | 435 | + | newSess, err := d.createSessionLockedWithMode(name, false, cwd, sz, env) |
| 443 | d.mu.Unlock() | 436 | d.mu.Unlock() | ||
| 444 | if err != nil { | 437 | if err != nil { | ||
| 445 | return err | 438 | return err | ||
| @@ -500,10 +493,10 @@ func (d *Daemon) createSessionAndSwitchForAttachment(token attachmentConnectionT | |||||
| 500 | return nil, errSessionNameInUse | 493 | return nil, errSessionNameInUse | ||
| 501 | } | 494 | } | ||
| 502 | source.mu.Lock() | 495 | source.mu.Lock() | ||
| 503 | - | cwd, term, env := source.cwd, source.terminal, copyEnvironment(source.env) | 496 | + | cwd, env := source.cwd, copyEnvironment(source.env) |
| 504 | source.mu.Unlock() | 497 | source.mu.Unlock() | ||
| 505 | var createErr error | 498 | var createErr error | ||
| 506 | - | created, createErr = d.createSessionLockedWithMode(name, false, cwd, token.ac.sizeSnapshot(), term, env) | 499 | + | created, createErr = d.createSessionLockedWithMode(name, false, cwd, token.ac.sizeSnapshot(), env) |
| 507 | return created, createErr | 500 | return created, createErr | ||
| 508 | }, | 501 | }, | ||
| 509 | }) | 502 | }) | ||
| @@ -529,7 +522,7 @@ func (d *Daemon) createEphemeralSessionAndSwitch(from *session, ac *attachedClie | |||||
| 529 | return errors.New("daemon is shutting down") | 522 | return errors.New("daemon is shutting down") | ||
| 530 | } | 523 | } | ||
| 531 | from.mu.Lock() | 524 | from.mu.Lock() | ||
| 532 | - | cwd, term, env := from.cwd, from.terminal, copyEnvironment(from.env) | 525 | + | cwd, env := from.cwd, copyEnvironment(from.env) |
| 533 | _, attached := from.attachments[ac] | 526 | _, attached := from.attachments[ac] | ||
| 534 | from.mu.Unlock() | 527 | from.mu.Unlock() | ||
| 535 | if !attached { | 528 | if !attached { | ||
| @@ -537,7 +530,7 @@ func (d *Daemon) createEphemeralSessionAndSwitch(from *session, ac *attachedClie | |||||
| 537 | return errors.New("client detached") | 530 | return errors.New("client detached") | ||
| 538 | } | 531 | } | ||
| 539 | name := d.allocEphemeralNameLocked() | 532 | name := d.allocEphemeralNameLocked() | ||
| 540 | - | newSess, err := d.createSessionLockedWithMode(name, true, cwd, sz, term, env) | 533 | + | newSess, err := d.createSessionLockedWithMode(name, true, cwd, sz, env) |
| 541 | d.mu.Unlock() | 534 | d.mu.Unlock() | ||
| 542 | if err != nil { | 535 | if err != nil { | ||
| 543 | return err | 536 | return err | ||
| @@ -575,11 +568,11 @@ func (d *Daemon) createEphemeralSessionAndSwitchForAttachment(token attachmentCo | |||||
| 575 | } | 568 | } | ||
| 576 | source := token.sess | 569 | source := token.sess | ||
| 577 | source.mu.Lock() | 570 | source.mu.Lock() | ||
| 578 | - | cwd, term, env := source.cwd, source.terminal, copyEnvironment(source.env) | 571 | + | cwd, env := source.cwd, copyEnvironment(source.env) |
| 579 | source.mu.Unlock() | 572 | source.mu.Unlock() | ||
| 580 | name := d.allocEphemeralNameLocked() | 573 | name := d.allocEphemeralNameLocked() | ||
| 581 | var createErr error | 574 | var createErr error | ||
| 582 | - | created, createErr = d.createSessionLockedWithMode(name, true, cwd, token.ac.sizeSnapshot(), term, env) | 575 | + | created, createErr = d.createSessionLockedWithMode(name, true, cwd, token.ac.sizeSnapshot(), env) |
| 583 | return created, createErr | 576 | return created, createErr | ||
| 584 | }, | 577 | }, | ||
| 585 | }) | 578 | }) | ||
| @@ -604,7 +597,6 @@ func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, _ dom | |||||
| 604 | sess.mu.Lock() | 597 | sess.mu.Lock() | ||
| 605 | name := sess.name | 598 | name := sess.name | ||
| 606 | cwd := sess.cwd | 599 | cwd := sess.cwd | ||
| 607 | - | term := sess.terminal | |||
| 608 | env := copyEnvironment(sess.env) | 600 | env := copyEnvironment(sess.env) | ||
| 609 | attachments := sess.snapshotAttachmentsLocked() | 601 | attachments := sess.snapshotAttachmentsLocked() | ||
| 610 | sess.mu.Unlock() | 602 | sess.mu.Unlock() | ||
| @@ -615,7 +607,7 @@ func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, _ dom | |||||
| 615 | } | 607 | } | ||
| 616 | launch := d.shellLaunch(env) | 608 | launch := d.shellLaunch(env) | ||
| 617 | lifetime := d.newPaneProcessLifetime(sess.ctx) | 609 | lifetime := d.newPaneProcessLifetime(sess.ctx) | ||
| 618 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID, term), cwd, tbSize) | 610 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, tbSize) |
| 619 | if err != nil { | 611 | if err != nil { | ||
| 620 | lifetime.abort() | 612 | lifetime.abort() | ||
| 621 | if pty != nil { | 613 | if pty != nil { | ||
| @@ -1881,8 +1873,8 @@ func (d *Daemon) refreshSessionCwd(sess *session) { | |||||
| 1881 | 1873 | ||||
| 1882 | // childEnv retains the daemon-environment helper for daemon-local legacy callers. | 1874 | // childEnv retains the daemon-environment helper for daemon-local legacy callers. | ||
| 1883 | // Interactive PTY launch paths use childEnvFrom with their session snapshot. | 1875 | // Interactive PTY launch paths use childEnvFrom with their session snapshot. | ||
| 1884 | - | func (d *Daemon) childEnv(name, tabStableID, paneStableID string, term terminalEnv) []string { | 1876 | + | func (d *Daemon) childEnv(name, tabStableID, paneStableID string) []string { |
| 1885 | - | return childEnvFrom(d.baseEnv, name, tabStableID, paneStableID, term) | 1877 | + | return childEnvFrom(d.baseEnv, name, tabStableID, paneStableID) |
| 1886 | } | 1878 | } | ||
| 1887 | 1879 | ||||
| 1888 | func copyEnvironment(env []string) []string { | 1880 | func copyEnvironment(env []string) []string { | ||
| @@ -1894,23 +1886,6 @@ func environmentEntry(entry string) (name, value string, ok bool) { | |||||
| 1894 | return name, value, ok | 1886 | return name, value, ok | ||
| 1895 | } | 1887 | } | ||
| 1896 | 1888 | ||||
| 1897 | - | func terminalEnvFromEnvironment(env []string) terminalEnv { | |||
| 1898 | - | var term, colorTerm string | |||
| 1899 | - | for _, entry := range env { | |||
| 1900 | - | name, value, ok := environmentEntry(entry) | |||
| 1901 | - | if !ok { | |||
| 1902 | - | continue | |||
| 1903 | - | } | |||
| 1904 | - | switch name { | |||
| 1905 | - | case "TERM": | |||
| 1906 | - | term = value | |||
| 1907 | - | case "COLORTERM": | |||
| 1908 | - | colorTerm = value | |||
| 1909 | - | } | |||
| 1910 | - | } | |||
| 1911 | - | return terminalEnv{TrueColor: ports.DetectTrueColor(term, colorTerm)} | |||
| 1912 | - | } | |||
| 1913 | - | ||||
| 1914 | const defaultShellCommand = "/bin/sh" | 1889 | const defaultShellCommand = "/bin/sh" | ||
| 1915 | const defaultShellTitle = "sh" | 1890 | const defaultShellTitle = "sh" | ||
| 1916 | 1891 | ||||
| @@ -1956,7 +1931,7 @@ func shellFromEnvironment(env []string) string { | |||||
| 1956 | 1931 | ||||
| 1957 | // childEnvFrom preserves the supplied environment byte-for-byte and in order, | 1932 | // childEnvFrom preserves the supplied environment byte-for-byte and in order, | ||
| 1958 | // except for exact reserved variable names which vev owns. | 1933 | // except for exact reserved variable names which vev owns. | ||
| 1959 | - | func childEnvFrom(env []string, name, tabStableID, paneStableID string, term terminalEnv) []string { | 1934 | + | func childEnvFrom(env []string, name, tabStableID, paneStableID string) []string { |
| 1960 | out := make([]string, 0, len(env)+4) | 1935 | out := make([]string, 0, len(env)+4) | ||
| 1961 | for _, entry := range env { | 1936 | for _, entry := range env { | ||
| 1962 | key, _, ok := environmentEntry(entry) | 1937 | key, _, ok := environmentEntry(entry) | ||
| @@ -1965,11 +1940,7 @@ func childEnvFrom(env []string, name, tabStableID, paneStableID string, term ter | |||||
| 1965 | } | 1940 | } | ||
| 1966 | out = append(out, entry) | 1941 | out = append(out, entry) | ||
| 1967 | } | 1942 | } | ||
| 1968 | - | if term.TrueColor { | 1943 | + | out = append(out, "TERM=xterm-256color", "COLORTERM=truecolor") |
| 1969 | - | out = append(out, "TERM=xterm-direct", "COLORTERM=truecolor") | |||
| 1970 | - | } else { | |||
| 1971 | - | out = append(out, "TERM=xterm-256color") | |||
| 1972 | - | } | |||
| 1973 | return append(out, | 1944 | return append(out, | ||
| 1974 | "TERM_PROGRAM=vev", | 1945 | "TERM_PROGRAM=vev", | ||
| 1975 | "VEV=session="+escapeVEVComponent(name)+",tab="+tabStableID+",pane="+paneStableID, | 1946 | "VEV=session="+escapeVEVComponent(name)+",tab="+tabStableID+",pane="+paneStableID, | ||
M internal/usecase/daemon/session_test.go +16 -18
| @@ -1239,7 +1239,7 @@ func TestPickerStoppedTargetKillPurges(t *testing.T) { | |||
| 1239 | 1239 | func TestChildEnvEscapesLegacySessionName(t *testing.T) { | |
| 1240 | 1240 | d := newTestDaemon(t, nil, stubClock{}) | |
| 1241 | 1241 | ||
| 1242 | - | got := d.childEnv("legacy,name=value", "t_alpha", "p_beta", terminalEnv{TrueColor: true}) | |
| 1242 | + | got := d.childEnv("legacy,name=value", "t_alpha", "p_beta") | |
| 1243 | 1243 | ||
| 1244 | 1244 | require.Contains(t, got, "VEV=session=legacy%2Cname%3Dvalue,tab=t_alpha,pane=p_beta") | |
| 1245 | 1245 | } | |
| @@ -1285,7 +1285,7 @@ func TestNewSessionAssignsStableIDsAndChildEnv(t *testing.T) { | |||
| 1285 | 1285 | require.NotContains(t, gotEnv, "COLORTERM=old") | |
| 1286 | 1286 | require.NotContains(t, gotEnv, "TERM_PROGRAM=old") | |
| 1287 | 1287 | require.NotContains(t, gotEnv, "VEV=old") | |
| 1288 | - | require.Contains(t, gotEnv, "TERM=xterm-direct") | |
| 1288 | + | require.Contains(t, gotEnv, "TERM=xterm-256color") | |
| 1289 | 1289 | require.Contains(t, gotEnv, "COLORTERM=truecolor") | |
| 1290 | 1290 | require.Contains(t, gotEnv, "TERM_PROGRAM=vev") | |
| 1291 | 1291 | require.Contains(t, gotEnv, "VEV=session=work,tab="+tabStableID+",pane="+paneStableID) | |
| @@ -1385,7 +1385,7 @@ func TestChildEnvTrueColorCapability(t *testing.T) { | |||
| 1385 | 1385 | term: terminalEnv{TrueColor: true}, | |
| 1386 | 1386 | wantContain: []string{ | |
| 1387 | 1387 | "KEEP=1", | |
| 1388 | - | "TERM=xterm-direct", | |
| 1388 | + | "TERM=xterm-256color", | |
| 1389 | 1389 | "COLORTERM=truecolor", | |
| 1390 | 1390 | "TERM_PROGRAM=vev", | |
| 1391 | 1391 | "VEV=session=work,tab=t_alpha,pane=p_beta", | |
| @@ -1393,11 +1393,10 @@ func TestChildEnvTrueColorCapability(t *testing.T) { | |||
| 1393 | 1393 | wantNotContain: []string{"TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old"}, | |
| 1394 | 1394 | }, | |
| 1395 | 1395 | { | |
| 1396 | - | name: "omits truecolor when unsupported", | |
| 1396 | + | name: "uses the fixed pane contract", | |
| 1397 | 1397 | baseEnv: []string{"COLORTERM=old"}, | |
| 1398 | - | term: terminalEnv{}, | |
| 1399 | - | wantContain: []string{"TERM=xterm-256color", "TERM_PROGRAM=vev"}, | |
| 1400 | - | wantNotContain: []string{"TERM=xterm-direct", "COLORTERM=truecolor"}, | |
| 1398 | + | wantContain: []string{"TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev"}, | |
| 1399 | + | wantNotContain: []string{"TERM=xterm-direct"}, | |
| 1401 | 1400 | }, | |
| 1402 | 1401 | } | |
| 1403 | 1402 | ||
| @@ -1406,7 +1405,7 @@ func TestChildEnvTrueColorCapability(t *testing.T) { | |||
| 1406 | 1405 | d := newTestDaemon(t, nil, stubClock{}) | |
| 1407 | 1406 | d.baseEnv = tt.baseEnv | |
| 1408 | 1407 | ||
| 1409 | - | got := d.childEnv("work", "t_alpha", "p_beta", tt.term) | |
| 1408 | + | got := d.childEnv("work", "t_alpha", "p_beta") | |
| 1410 | 1409 | ||
| 1411 | 1410 | for _, want := range tt.wantContain { | |
| 1412 | 1411 | require.Contains(t, got, want) | |
| @@ -1452,11 +1451,11 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1452 | 1451 | d.sessWg.Wait() | |
| 1453 | 1452 | }() | |
| 1454 | 1453 | ||
| 1455 | - | require.Contains(t, opens[0], "TERM=xterm-direct") | |
| 1454 | + | require.Contains(t, opens[0], "TERM=xterm-256color") | |
| 1456 | 1455 | require.Contains(t, opens[0], "COLORTERM=truecolor") | |
| 1457 | 1456 | require.Contains(t, opens[0], "TERM_PROGRAM=vev") | |
| 1458 | 1457 | require.NoError(t, d.createTab(sess, ac.size)) | |
| 1459 | - | require.Contains(t, opens[1], "TERM=xterm-direct") | |
| 1458 | + | require.Contains(t, opens[1], "TERM=xterm-256color") | |
| 1460 | 1459 | require.Contains(t, opens[1], "COLORTERM=truecolor") | |
| 1461 | 1460 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | |
| 1462 | 1461 | } | |
| @@ -1500,9 +1499,9 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1500 | 1499 | }() | |
| 1501 | 1500 | ||
| 1502 | 1501 | require.Contains(t, opens[0], "TERM=xterm-256color") | |
| 1503 | - | require.NotContains(t, opens[0], "COLORTERM=truecolor") | |
| 1502 | + | require.Contains(t, opens[0], "COLORTERM=truecolor") | |
| 1504 | 1503 | require.NoError(t, d.createTab(sess, ac.size)) | |
| 1505 | - | require.Contains(t, opens[1], "TERM=xterm-direct") | |
| 1504 | + | require.Contains(t, opens[1], "TERM=xterm-256color") | |
| 1506 | 1505 | require.Contains(t, opens[1], "COLORTERM=truecolor") | |
| 1507 | 1506 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | |
| 1508 | 1507 | } | |
| @@ -1562,10 +1561,9 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 1562 | 1561 | require.Equal(t, uint64(1), ac.output.next-ac.output.acked, "only the destination first paint may follow the rebase") | |
| 1563 | 1562 | ac.sendMu.Unlock() | |
| 1564 | 1563 | got.mu.Lock() | |
| 1565 | - | require.True(t, got.terminal.TrueColor) | |
| 1566 | 1564 | got.mu.Unlock() | |
| 1567 | 1565 | require.Len(t, opens, 2) | |
| 1568 | - | require.Contains(t, opens[1], "TERM=xterm-direct") | |
| 1566 | + | require.Contains(t, opens[1], "TERM=xterm-256color") | |
| 1569 | 1567 | require.Contains(t, opens[1], "COLORTERM=truecolor") | |
| 1570 | 1568 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | |
| 1571 | 1569 | _ = d.killSession(got, ports.ReasonSessionKilled, false) | |
| @@ -1617,20 +1615,20 @@ func TestAttachEnvironmentReplacesFuturePTYInputs(t *testing.T) { | |||
| 1617 | 1615 | }() | |
| 1618 | 1616 | ||
| 1619 | 1617 | require.Equal(t, "/usr/bin/fish", commands[0]) | |
| 1620 | - | require.Equal(t, []string{"SECRET=first", "TERM_PROGRAM_extra=keep", "SHELL=/usr/bin/fish", "A=a=b", "TERM=xterm-256color", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + sess.tabs[0].stableID + ",pane=" + sess.tabs[0].panes["pane-1"].stableID}, envs[0]) | |
| 1618 | + | require.Equal(t, []string{"SECRET=first", "TERM_PROGRAM_extra=keep", "SHELL=/usr/bin/fish", "A=a=b", "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + sess.tabs[0].stableID + ",pane=" + sess.tabs[0].panes["pane-1"].stableID}, envs[0]) | |
| 1621 | 1619 | require.Equal(t, "fish", sess.tabs[0].focusedPaneTitle(false), "the initial tab title identifies the shell requested by its creator") | |
| 1622 | 1620 | require.NoError(t, d.createTab(sess, ac.size)) | |
| 1623 | 1621 | require.Equal(t, "/bin/bash", commands[1]) | |
| 1624 | - | require.Equal(t, []string{"SECRET=second", "TERM_PROGRAM_extra=keep", "SHELL=/bin/bash", "A=a=b", "TERM=xterm-256color", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + sess.tabs[1].stableID + ",pane=" + sess.tabs[1].panes["pane-1"].stableID}, envs[1]) | |
| 1622 | + | require.Equal(t, []string{"SECRET=second", "TERM_PROGRAM_extra=keep", "SHELL=/bin/bash", "A=a=b", "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + sess.tabs[1].stableID + ",pane=" + sess.tabs[1].panes["pane-1"].stableID}, envs[1]) | |
| 1625 | 1623 | require.Equal(t, "bash", sess.tabs[1].focusedPaneTitle(false), "later tabs use the most recently attached shell") | |
| 1626 | 1624 | } | |
| 1627 | 1625 | ||
| 1628 | 1626 | func TestChildEnvFromPreservesNonReservedEntriesVerbatimAndInOrder(t *testing.T) { | |
| 1629 | 1627 | env := []string{"SECRET=a=b=c", "TERM_PROGRAM_extra=keep", "TERM", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old", "EMPTY="} | |
| 1630 | - | got := childEnvFrom(env, "work", "tab", "pane", terminalEnv{}) | |
| 1628 | + | got := childEnvFrom(env, "work", "tab", "pane") | |
| 1631 | 1629 | require.Equal(t, []string{ | |
| 1632 | 1630 | "SECRET=a=b=c", "TERM_PROGRAM_extra=keep", "TERM", "EMPTY=", | |
| 1633 | - | "TERM=xterm-256color", "TERM_PROGRAM=vev", "VEV=session=work,tab=tab,pane=pane", | |
| 1631 | + | "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=tab,pane=pane", | |
| 1634 | 1632 | }, got) | |
| 1635 | 1633 | } | |
| 1636 | 1634 | ||
| @@ -1239,7 +1239,7 @@ func TestPickerStoppedTargetKillPurges(t *testing.T) { | |||||
| 1239 | func TestChildEnvEscapesLegacySessionName(t *testing.T) { | 1239 | func TestChildEnvEscapesLegacySessionName(t *testing.T) { | ||
| 1240 | d := newTestDaemon(t, nil, stubClock{}) | 1240 | d := newTestDaemon(t, nil, stubClock{}) | ||
| 1241 | 1241 | ||||
| 1242 | - | got := d.childEnv("legacy,name=value", "t_alpha", "p_beta", terminalEnv{TrueColor: true}) | 1242 | + | got := d.childEnv("legacy,name=value", "t_alpha", "p_beta") |
| 1243 | 1243 | ||||
| 1244 | require.Contains(t, got, "VEV=session=legacy%2Cname%3Dvalue,tab=t_alpha,pane=p_beta") | 1244 | require.Contains(t, got, "VEV=session=legacy%2Cname%3Dvalue,tab=t_alpha,pane=p_beta") | ||
| 1245 | } | 1245 | } | ||
| @@ -1285,7 +1285,7 @@ func TestNewSessionAssignsStableIDsAndChildEnv(t *testing.T) { | |||||
| 1285 | require.NotContains(t, gotEnv, "COLORTERM=old") | 1285 | require.NotContains(t, gotEnv, "COLORTERM=old") | ||
| 1286 | require.NotContains(t, gotEnv, "TERM_PROGRAM=old") | 1286 | require.NotContains(t, gotEnv, "TERM_PROGRAM=old") | ||
| 1287 | require.NotContains(t, gotEnv, "VEV=old") | 1287 | require.NotContains(t, gotEnv, "VEV=old") | ||
| 1288 | - | require.Contains(t, gotEnv, "TERM=xterm-direct") | 1288 | + | require.Contains(t, gotEnv, "TERM=xterm-256color") |
| 1289 | require.Contains(t, gotEnv, "COLORTERM=truecolor") | 1289 | require.Contains(t, gotEnv, "COLORTERM=truecolor") | ||
| 1290 | require.Contains(t, gotEnv, "TERM_PROGRAM=vev") | 1290 | require.Contains(t, gotEnv, "TERM_PROGRAM=vev") | ||
| 1291 | require.Contains(t, gotEnv, "VEV=session=work,tab="+tabStableID+",pane="+paneStableID) | 1291 | require.Contains(t, gotEnv, "VEV=session=work,tab="+tabStableID+",pane="+paneStableID) | ||
| @@ -1385,7 +1385,7 @@ func TestChildEnvTrueColorCapability(t *testing.T) { | |||||
| 1385 | term: terminalEnv{TrueColor: true}, | 1385 | term: terminalEnv{TrueColor: true}, | ||
| 1386 | wantContain: []string{ | 1386 | wantContain: []string{ | ||
| 1387 | "KEEP=1", | 1387 | "KEEP=1", | ||
| 1388 | - | "TERM=xterm-direct", | 1388 | + | "TERM=xterm-256color", |
| 1389 | "COLORTERM=truecolor", | 1389 | "COLORTERM=truecolor", | ||
| 1390 | "TERM_PROGRAM=vev", | 1390 | "TERM_PROGRAM=vev", | ||
| 1391 | "VEV=session=work,tab=t_alpha,pane=p_beta", | 1391 | "VEV=session=work,tab=t_alpha,pane=p_beta", | ||
| @@ -1393,11 +1393,10 @@ func TestChildEnvTrueColorCapability(t *testing.T) { | |||||
| 1393 | wantNotContain: []string{"TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old"}, | 1393 | wantNotContain: []string{"TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old"}, | ||
| 1394 | }, | 1394 | }, | ||
| 1395 | { | 1395 | { | ||
| 1396 | - | name: "omits truecolor when unsupported", | 1396 | + | name: "uses the fixed pane contract", |
| 1397 | baseEnv: []string{"COLORTERM=old"}, | 1397 | baseEnv: []string{"COLORTERM=old"}, | ||
| 1398 | - | term: terminalEnv{}, | 1398 | + | wantContain: []string{"TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev"}, |
| 1399 | - | wantContain: []string{"TERM=xterm-256color", "TERM_PROGRAM=vev"}, | 1399 | + | wantNotContain: []string{"TERM=xterm-direct"}, |
| 1400 | - | wantNotContain: []string{"TERM=xterm-direct", "COLORTERM=truecolor"}, | |||
| 1401 | }, | 1400 | }, | ||
| 1402 | } | 1401 | } | ||
| 1403 | 1402 | ||||
| @@ -1406,7 +1405,7 @@ func TestChildEnvTrueColorCapability(t *testing.T) { | |||||
| 1406 | d := newTestDaemon(t, nil, stubClock{}) | 1405 | d := newTestDaemon(t, nil, stubClock{}) | ||
| 1407 | d.baseEnv = tt.baseEnv | 1406 | d.baseEnv = tt.baseEnv | ||
| 1408 | 1407 | ||||
| 1409 | - | got := d.childEnv("work", "t_alpha", "p_beta", tt.term) | 1408 | + | got := d.childEnv("work", "t_alpha", "p_beta") |
| 1410 | 1409 | ||||
| 1411 | for _, want := range tt.wantContain { | 1410 | for _, want := range tt.wantContain { | ||
| 1412 | require.Contains(t, got, want) | 1411 | require.Contains(t, got, want) | ||
| @@ -1452,11 +1451,11 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1452 | d.sessWg.Wait() | 1451 | d.sessWg.Wait() | ||
| 1453 | }() | 1452 | }() | ||
| 1454 | 1453 | ||||
| 1455 | - | require.Contains(t, opens[0], "TERM=xterm-direct") | 1454 | + | require.Contains(t, opens[0], "TERM=xterm-256color") |
| 1456 | require.Contains(t, opens[0], "COLORTERM=truecolor") | 1455 | require.Contains(t, opens[0], "COLORTERM=truecolor") | ||
| 1457 | require.Contains(t, opens[0], "TERM_PROGRAM=vev") | 1456 | require.Contains(t, opens[0], "TERM_PROGRAM=vev") | ||
| 1458 | require.NoError(t, d.createTab(sess, ac.size)) | 1457 | require.NoError(t, d.createTab(sess, ac.size)) | ||
| 1459 | - | require.Contains(t, opens[1], "TERM=xterm-direct") | 1458 | + | require.Contains(t, opens[1], "TERM=xterm-256color") |
| 1460 | require.Contains(t, opens[1], "COLORTERM=truecolor") | 1459 | require.Contains(t, opens[1], "COLORTERM=truecolor") | ||
| 1461 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | 1460 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | ||
| 1462 | } | 1461 | } | ||
| @@ -1500,9 +1499,9 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1500 | }() | 1499 | }() | ||
| 1501 | 1500 | ||||
| 1502 | require.Contains(t, opens[0], "TERM=xterm-256color") | 1501 | require.Contains(t, opens[0], "TERM=xterm-256color") | ||
| 1503 | - | require.NotContains(t, opens[0], "COLORTERM=truecolor") | 1502 | + | require.Contains(t, opens[0], "COLORTERM=truecolor") |
| 1504 | require.NoError(t, d.createTab(sess, ac.size)) | 1503 | require.NoError(t, d.createTab(sess, ac.size)) | ||
| 1505 | - | require.Contains(t, opens[1], "TERM=xterm-direct") | 1504 | + | require.Contains(t, opens[1], "TERM=xterm-256color") |
| 1506 | require.Contains(t, opens[1], "COLORTERM=truecolor") | 1505 | require.Contains(t, opens[1], "COLORTERM=truecolor") | ||
| 1507 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | 1506 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | ||
| 1508 | } | 1507 | } | ||
| @@ -1562,10 +1561,9 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 1562 | require.Equal(t, uint64(1), ac.output.next-ac.output.acked, "only the destination first paint may follow the rebase") | 1561 | require.Equal(t, uint64(1), ac.output.next-ac.output.acked, "only the destination first paint may follow the rebase") | ||
| 1563 | ac.sendMu.Unlock() | 1562 | ac.sendMu.Unlock() | ||
| 1564 | got.mu.Lock() | 1563 | got.mu.Lock() | ||
| 1565 | - | require.True(t, got.terminal.TrueColor) | |||
| 1566 | got.mu.Unlock() | 1564 | got.mu.Unlock() | ||
| 1567 | require.Len(t, opens, 2) | 1565 | require.Len(t, opens, 2) | ||
| 1568 | - | require.Contains(t, opens[1], "TERM=xterm-direct") | 1566 | + | require.Contains(t, opens[1], "TERM=xterm-256color") |
| 1569 | require.Contains(t, opens[1], "COLORTERM=truecolor") | 1567 | require.Contains(t, opens[1], "COLORTERM=truecolor") | ||
| 1570 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | 1568 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | ||
| 1571 | _ = d.killSession(got, ports.ReasonSessionKilled, false) | 1569 | _ = d.killSession(got, ports.ReasonSessionKilled, false) | ||
| @@ -1617,20 +1615,20 @@ func TestAttachEnvironmentReplacesFuturePTYInputs(t *testing.T) { | |||||
| 1617 | }() | 1615 | }() | ||
| 1618 | 1616 | ||||
| 1619 | require.Equal(t, "/usr/bin/fish", commands[0]) | 1617 | require.Equal(t, "/usr/bin/fish", commands[0]) | ||
| 1620 | - | require.Equal(t, []string{"SECRET=first", "TERM_PROGRAM_extra=keep", "SHELL=/usr/bin/fish", "A=a=b", "TERM=xterm-256color", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + sess.tabs[0].stableID + ",pane=" + sess.tabs[0].panes["pane-1"].stableID}, envs[0]) | 1618 | + | require.Equal(t, []string{"SECRET=first", "TERM_PROGRAM_extra=keep", "SHELL=/usr/bin/fish", "A=a=b", "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + sess.tabs[0].stableID + ",pane=" + sess.tabs[0].panes["pane-1"].stableID}, envs[0]) |
| 1621 | require.Equal(t, "fish", sess.tabs[0].focusedPaneTitle(false), "the initial tab title identifies the shell requested by its creator") | 1619 | require.Equal(t, "fish", sess.tabs[0].focusedPaneTitle(false), "the initial tab title identifies the shell requested by its creator") | ||
| 1622 | require.NoError(t, d.createTab(sess, ac.size)) | 1620 | require.NoError(t, d.createTab(sess, ac.size)) | ||
| 1623 | require.Equal(t, "/bin/bash", commands[1]) | 1621 | require.Equal(t, "/bin/bash", commands[1]) | ||
| 1624 | - | require.Equal(t, []string{"SECRET=second", "TERM_PROGRAM_extra=keep", "SHELL=/bin/bash", "A=a=b", "TERM=xterm-256color", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + sess.tabs[1].stableID + ",pane=" + sess.tabs[1].panes["pane-1"].stableID}, envs[1]) | 1622 | + | require.Equal(t, []string{"SECRET=second", "TERM_PROGRAM_extra=keep", "SHELL=/bin/bash", "A=a=b", "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=" + sess.tabs[1].stableID + ",pane=" + sess.tabs[1].panes["pane-1"].stableID}, envs[1]) |
| 1625 | require.Equal(t, "bash", sess.tabs[1].focusedPaneTitle(false), "later tabs use the most recently attached shell") | 1623 | require.Equal(t, "bash", sess.tabs[1].focusedPaneTitle(false), "later tabs use the most recently attached shell") | ||
| 1626 | } | 1624 | } | ||
| 1627 | 1625 | ||||
| 1628 | func TestChildEnvFromPreservesNonReservedEntriesVerbatimAndInOrder(t *testing.T) { | 1626 | func TestChildEnvFromPreservesNonReservedEntriesVerbatimAndInOrder(t *testing.T) { | ||
| 1629 | env := []string{"SECRET=a=b=c", "TERM_PROGRAM_extra=keep", "TERM", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old", "EMPTY="} | 1627 | env := []string{"SECRET=a=b=c", "TERM_PROGRAM_extra=keep", "TERM", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old", "EMPTY="} | ||
| 1630 | - | got := childEnvFrom(env, "work", "tab", "pane", terminalEnv{}) | 1628 | + | got := childEnvFrom(env, "work", "tab", "pane") |
| 1631 | require.Equal(t, []string{ | 1629 | require.Equal(t, []string{ | ||
| 1632 | "SECRET=a=b=c", "TERM_PROGRAM_extra=keep", "TERM", "EMPTY=", | 1630 | "SECRET=a=b=c", "TERM_PROGRAM_extra=keep", "TERM", "EMPTY=", | ||
| 1633 | - | "TERM=xterm-256color", "TERM_PROGRAM=vev", "VEV=session=work,tab=tab,pane=pane", | 1631 | + | "TERM=xterm-256color", "COLORTERM=truecolor", "TERM_PROGRAM=vev", "VEV=session=work,tab=tab,pane=pane", |
| 1634 | }, got) | 1632 | }, got) | ||
| 1635 | } | 1633 | } | ||
| 1636 | 1634 | ||||
M internal/usecase/daemon/snapshot_restore.go +10 -11
| @@ -142,7 +142,7 @@ func (d *Daemon) restoreSession(ctx context.Context, snap snapcodec.Session, rep | |||
| 142 | 142 | } | |
| 143 | 143 | }() | |
| 144 | 144 | ||
| 145 | - | sess := d.newRestoredSession(snap, sctx, cancel, opened, terminalEnvFromEnvironment(d.baseEnv)) | |
| 145 | + | sess := d.newRestoredSession(snap, sctx, cancel, opened) | |
| 146 | 146 | // The loaded manifest is the repository head for this name. Future dirty | |
| 147 | 147 | // checkpoints must continue from it rather than reuse generation one. | |
| 148 | 148 | sess.snapshotPublishedGeneration = repositoryGeneration | |
| @@ -165,10 +165,9 @@ func (d *Daemon) restoredSessionAlreadyExists(name string) bool { | |||
| 165 | 165 | } | |
| 166 | 166 | ||
| 167 | 167 | func (d *Daemon) restoreSnapshotTabs(ctx, sctx context.Context, snap snapcodec.Session) ([]*tab, error) { | |
| 168 | - | // Restore runs before client Hello. Use the daemon's startup environment, | |
| 169 | - | // including its terminal capability, until an attach supplies a fresher one. | |
| 168 | + | // Restore runs before client Hello. Pane terminal identity is stable and does | |
| 169 | + | // not depend on the daemon's startup environment. | |
| 170 | 170 | restoreEnv := copyEnvironment(d.baseEnv) | |
| 171 | - | restoreTerm := terminalEnvFromEnvironment(restoreEnv) | |
| 172 | 171 | allowlist := d.restoreProcessAllowlistSnapshot() | |
| 173 | 172 | stoppedTabNames := d.restoredSessionTabNames(snap.Name) | |
| 174 | 173 | opened := make([]*tab, 0, len(snap.Tabs)) | |
| @@ -181,7 +180,7 @@ func (d *Daemon) restoreSnapshotTabs(ctx, sctx context.Context, snap snapcodec.S | |||
| 181 | 180 | if index < len(stoppedTabNames) { | |
| 182 | 181 | tabName = stoppedTabNames[index] | |
| 183 | 182 | } | |
| 184 | - | tb, err := d.restoreSnapshotTab(ctx, sctx, snap.Name, tabName, tabSnap, restoreEnv, restoreTerm, allowlist) | |
| 183 | + | tb, err := d.restoreSnapshotTab(ctx, sctx, snap.Name, tabName, tabSnap, restoreEnv, allowlist) | |
| 185 | 184 | if err != nil { | |
| 186 | 185 | closeRestoredTabs(opened) | |
| 187 | 186 | return nil, err | |
| @@ -205,7 +204,7 @@ func (d *Daemon) restoredSessionTabNames(name string) []string { | |||
| 205 | 204 | return append([]string(nil), d.stopped[name].tabNames...) | |
| 206 | 205 | } | |
| 207 | 206 | ||
| 208 | - | func (d *Daemon) restoreSnapshotTab(ctx, sctx context.Context, sessionName, tabName string, tabSnap snapcodec.Tab, restoreEnv []string, restoreTerm terminalEnv, allowlist map[string]struct{}) (*tab, error) { | |
| 207 | + | func (d *Daemon) restoreSnapshotTab(ctx, sctx context.Context, sessionName, tabName string, tabSnap snapcodec.Tab, restoreEnv []string, allowlist map[string]struct{}) (*tab, error) { | |
| 209 | 208 | tbSize := domain.Size{Cols: int(tabSnap.Cols), Rows: int(tabSnap.Rows)} | |
| 210 | 209 | if !tbSize.Valid() { | |
| 211 | 210 | return nil, fmt.Errorf("snapshot: invalid tab size") | |
| @@ -241,7 +240,7 @@ func (d *Daemon) restoreSnapshotTab(ctx, sctx context.Context, sessionName, tabN | |||
| 241 | 240 | tb.closeAllPanes() | |
| 242 | 241 | return nil, fmt.Errorf("snapshot: missing pane placement") | |
| 243 | 242 | } | |
| 244 | - | p, err := d.restoreSnapshotPane(ctx, sessionName, tabStableID, paneSnap, contentRect, tbSize, restoreEnv, restoreTerm, allowlist, tb.ctx) | |
| 243 | + | p, err := d.restoreSnapshotPane(ctx, sessionName, tabStableID, paneSnap, contentRect, tbSize, restoreEnv, allowlist, tb.ctx) | |
| 245 | 244 | if err != nil { | |
| 246 | 245 | tb.closeAllPanes() | |
| 247 | 246 | return nil, err | |
| @@ -251,7 +250,7 @@ func (d *Daemon) restoreSnapshotTab(ctx, sctx context.Context, sessionName, tabN | |||
| 251 | 250 | return tb, nil | |
| 252 | 251 | } | |
| 253 | 252 | ||
| 254 | - | func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStableID string, paneSnap snapcodec.Pane, contentRect domain.Rect, tabSize domain.Size, restoreEnv []string, restoreTerm terminalEnv, allowlist map[string]struct{}, tabCtx context.Context) (*pane, error) { | |
| 253 | + | func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStableID string, paneSnap snapcodec.Pane, contentRect domain.Rect, tabSize domain.Size, restoreEnv []string, allowlist map[string]struct{}, tabCtx context.Context) (*pane, error) { | |
| 255 | 254 | paneStableID := paneSnap.StableID | |
| 256 | 255 | if paneStableID == "" { | |
| 257 | 256 | var err error | |
| @@ -262,7 +261,7 @@ func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStable | |||
| 262 | 261 | } | |
| 263 | 262 | launch := d.shellLaunch(restoreEnv) | |
| 264 | 263 | lifetime := d.newPaneProcessLifetime(ctx, tabCtx) | |
| 265 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(restoreEnv, sessionName, tabStableID, paneStableID, restoreTerm), paneSnap.Cwd, restorePTYSize(contentRect, tabSize)) | |
| 264 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(restoreEnv, sessionName, tabStableID, paneStableID), paneSnap.Cwd, restorePTYSize(contentRect, tabSize)) | |
| 266 | 265 | if err != nil { | |
| 267 | 266 | lifetime.abort() | |
| 268 | 267 | if pty != nil { | |
| @@ -289,10 +288,10 @@ func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStable | |||
| 289 | 288 | return p, nil | |
| 290 | 289 | } | |
| 291 | 290 | ||
| 292 | - | func (d *Daemon) newRestoredSession(snap snapcodec.Session, sctx context.Context, cancel context.CancelFunc, tabs []*tab, term terminalEnv) *session { | |
| 291 | + | func (d *Daemon) newRestoredSession(snap snapcodec.Session, sctx context.Context, cancel context.CancelFunc, tabs []*tab) *session { | |
| 293 | 292 | // Restored sessions keep ordered tabs; attachment repair deterministically | |
| 294 | 293 | // selects the first tab instead of restoring an interactive client view. | |
| 295 | - | sess := &session{sessionCore: sessionCore{name: snap.Name, createdAt: int64(snap.CreatedAt)}, ctx: sctx, cancel: cancel, tabs: tabs, terminal: term, env: copyEnvironment(d.baseEnv), snapshotWake: d.snapshotWake, snapshotChunkCache: newSnapshotChunkCache(snapshotChunkCacheLimit)} | |
| 294 | + | sess := &session{sessionCore: sessionCore{name: snap.Name, createdAt: int64(snap.CreatedAt)}, ctx: sctx, cancel: cancel, tabs: tabs, env: copyEnvironment(d.baseEnv), snapshotWake: d.snapshotWake, snapshotChunkCache: newSnapshotChunkCache(snapshotChunkCacheLimit)} | |
| 296 | 295 | // Restored tabs remain private until persistAndRegisterRestoredSession. | |
| 297 | 296 | // Initialize owners now so registration and reader startup cannot expose an | |
| 298 | 297 | // ownerless pane. | |
| @@ -142,7 +142,7 @@ func (d *Daemon) restoreSession(ctx context.Context, snap snapcodec.Session, rep | |||||
| 142 | } | 142 | } | ||
| 143 | }() | 143 | }() | ||
| 144 | 144 | ||||
| 145 | - | sess := d.newRestoredSession(snap, sctx, cancel, opened, terminalEnvFromEnvironment(d.baseEnv)) | 145 | + | sess := d.newRestoredSession(snap, sctx, cancel, opened) |
| 146 | // The loaded manifest is the repository head for this name. Future dirty | 146 | // The loaded manifest is the repository head for this name. Future dirty | ||
| 147 | // checkpoints must continue from it rather than reuse generation one. | 147 | // checkpoints must continue from it rather than reuse generation one. | ||
| 148 | sess.snapshotPublishedGeneration = repositoryGeneration | 148 | sess.snapshotPublishedGeneration = repositoryGeneration | ||
| @@ -165,10 +165,9 @@ func (d *Daemon) restoredSessionAlreadyExists(name string) bool { | |||||
| 165 | } | 165 | } | ||
| 166 | 166 | ||||
| 167 | func (d *Daemon) restoreSnapshotTabs(ctx, sctx context.Context, snap snapcodec.Session) ([]*tab, error) { | 167 | func (d *Daemon) restoreSnapshotTabs(ctx, sctx context.Context, snap snapcodec.Session) ([]*tab, error) { | ||
| 168 | - | // Restore runs before client Hello. Use the daemon's startup environment, | 168 | + | // Restore runs before client Hello. Pane terminal identity is stable and does |
| 169 | - | // including its terminal capability, until an attach supplies a fresher one. | 169 | + | // not depend on the daemon's startup environment. |
| 170 | restoreEnv := copyEnvironment(d.baseEnv) | 170 | restoreEnv := copyEnvironment(d.baseEnv) | ||
| 171 | - | restoreTerm := terminalEnvFromEnvironment(restoreEnv) | |||
| 172 | allowlist := d.restoreProcessAllowlistSnapshot() | 171 | allowlist := d.restoreProcessAllowlistSnapshot() | ||
| 173 | stoppedTabNames := d.restoredSessionTabNames(snap.Name) | 172 | stoppedTabNames := d.restoredSessionTabNames(snap.Name) | ||
| 174 | opened := make([]*tab, 0, len(snap.Tabs)) | 173 | opened := make([]*tab, 0, len(snap.Tabs)) | ||
| @@ -181,7 +180,7 @@ func (d *Daemon) restoreSnapshotTabs(ctx, sctx context.Context, snap snapcodec.S | |||||
| 181 | if index < len(stoppedTabNames) { | 180 | if index < len(stoppedTabNames) { | ||
| 182 | tabName = stoppedTabNames[index] | 181 | tabName = stoppedTabNames[index] | ||
| 183 | } | 182 | } | ||
| 184 | - | tb, err := d.restoreSnapshotTab(ctx, sctx, snap.Name, tabName, tabSnap, restoreEnv, restoreTerm, allowlist) | 183 | + | tb, err := d.restoreSnapshotTab(ctx, sctx, snap.Name, tabName, tabSnap, restoreEnv, allowlist) |
| 185 | if err != nil { | 184 | if err != nil { | ||
| 186 | closeRestoredTabs(opened) | 185 | closeRestoredTabs(opened) | ||
| 187 | return nil, err | 186 | return nil, err | ||
| @@ -205,7 +204,7 @@ func (d *Daemon) restoredSessionTabNames(name string) []string { | |||||
| 205 | return append([]string(nil), d.stopped[name].tabNames...) | 204 | return append([]string(nil), d.stopped[name].tabNames...) | ||
| 206 | } | 205 | } | ||
| 207 | 206 | ||||
| 208 | - | func (d *Daemon) restoreSnapshotTab(ctx, sctx context.Context, sessionName, tabName string, tabSnap snapcodec.Tab, restoreEnv []string, restoreTerm terminalEnv, allowlist map[string]struct{}) (*tab, error) { | 207 | + | func (d *Daemon) restoreSnapshotTab(ctx, sctx context.Context, sessionName, tabName string, tabSnap snapcodec.Tab, restoreEnv []string, allowlist map[string]struct{}) (*tab, error) { |
| 209 | tbSize := domain.Size{Cols: int(tabSnap.Cols), Rows: int(tabSnap.Rows)} | 208 | tbSize := domain.Size{Cols: int(tabSnap.Cols), Rows: int(tabSnap.Rows)} | ||
| 210 | if !tbSize.Valid() { | 209 | if !tbSize.Valid() { | ||
| 211 | return nil, fmt.Errorf("snapshot: invalid tab size") | 210 | return nil, fmt.Errorf("snapshot: invalid tab size") | ||
| @@ -241,7 +240,7 @@ func (d *Daemon) restoreSnapshotTab(ctx, sctx context.Context, sessionName, tabN | |||||
| 241 | tb.closeAllPanes() | 240 | tb.closeAllPanes() | ||
| 242 | return nil, fmt.Errorf("snapshot: missing pane placement") | 241 | return nil, fmt.Errorf("snapshot: missing pane placement") | ||
| 243 | } | 242 | } | ||
| 244 | - | p, err := d.restoreSnapshotPane(ctx, sessionName, tabStableID, paneSnap, contentRect, tbSize, restoreEnv, restoreTerm, allowlist, tb.ctx) | 243 | + | p, err := d.restoreSnapshotPane(ctx, sessionName, tabStableID, paneSnap, contentRect, tbSize, restoreEnv, allowlist, tb.ctx) |
| 245 | if err != nil { | 244 | if err != nil { | ||
| 246 | tb.closeAllPanes() | 245 | tb.closeAllPanes() | ||
| 247 | return nil, err | 246 | return nil, err | ||
| @@ -251,7 +250,7 @@ func (d *Daemon) restoreSnapshotTab(ctx, sctx context.Context, sessionName, tabN | |||||
| 251 | return tb, nil | 250 | return tb, nil | ||
| 252 | } | 251 | } | ||
| 253 | 252 | ||||
| 254 | - | func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStableID string, paneSnap snapcodec.Pane, contentRect domain.Rect, tabSize domain.Size, restoreEnv []string, restoreTerm terminalEnv, allowlist map[string]struct{}, tabCtx context.Context) (*pane, error) { | 253 | + | func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStableID string, paneSnap snapcodec.Pane, contentRect domain.Rect, tabSize domain.Size, restoreEnv []string, allowlist map[string]struct{}, tabCtx context.Context) (*pane, error) { |
| 255 | paneStableID := paneSnap.StableID | 254 | paneStableID := paneSnap.StableID | ||
| 256 | if paneStableID == "" { | 255 | if paneStableID == "" { | ||
| 257 | var err error | 256 | var err error | ||
| @@ -262,7 +261,7 @@ func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStable | |||||
| 262 | } | 261 | } | ||
| 263 | launch := d.shellLaunch(restoreEnv) | 262 | launch := d.shellLaunch(restoreEnv) | ||
| 264 | lifetime := d.newPaneProcessLifetime(ctx, tabCtx) | 263 | lifetime := d.newPaneProcessLifetime(ctx, tabCtx) | ||
| 265 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(restoreEnv, sessionName, tabStableID, paneStableID, restoreTerm), paneSnap.Cwd, restorePTYSize(contentRect, tabSize)) | 264 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(restoreEnv, sessionName, tabStableID, paneStableID), paneSnap.Cwd, restorePTYSize(contentRect, tabSize)) |
| 266 | if err != nil { | 265 | if err != nil { | ||
| 267 | lifetime.abort() | 266 | lifetime.abort() | ||
| 268 | if pty != nil { | 267 | if pty != nil { | ||
| @@ -289,10 +288,10 @@ func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStable | |||||
| 289 | return p, nil | 288 | return p, nil | ||
| 290 | } | 289 | } | ||
| 291 | 290 | ||||
| 292 | - | func (d *Daemon) newRestoredSession(snap snapcodec.Session, sctx context.Context, cancel context.CancelFunc, tabs []*tab, term terminalEnv) *session { | 291 | + | func (d *Daemon) newRestoredSession(snap snapcodec.Session, sctx context.Context, cancel context.CancelFunc, tabs []*tab) *session { |
| 293 | // Restored sessions keep ordered tabs; attachment repair deterministically | 292 | // Restored sessions keep ordered tabs; attachment repair deterministically | ||
| 294 | // selects the first tab instead of restoring an interactive client view. | 293 | // selects the first tab instead of restoring an interactive client view. | ||
| 295 | - | sess := &session{sessionCore: sessionCore{name: snap.Name, createdAt: int64(snap.CreatedAt)}, ctx: sctx, cancel: cancel, tabs: tabs, terminal: term, env: copyEnvironment(d.baseEnv), snapshotWake: d.snapshotWake, snapshotChunkCache: newSnapshotChunkCache(snapshotChunkCacheLimit)} | 294 | + | sess := &session{sessionCore: sessionCore{name: snap.Name, createdAt: int64(snap.CreatedAt)}, ctx: sctx, cancel: cancel, tabs: tabs, env: copyEnvironment(d.baseEnv), snapshotWake: d.snapshotWake, snapshotChunkCache: newSnapshotChunkCache(snapshotChunkCacheLimit)} |
| 296 | // Restored tabs remain private until persistAndRegisterRestoredSession. | 295 | // Restored tabs remain private until persistAndRegisterRestoredSession. | ||
| 297 | // Initialize owners now so registration and reader startup cannot expose an | 296 | // Initialize owners now so registration and reader startup cannot expose an | ||
| 298 | // ownerless pane. | 297 | // ownerless pane. | ||
M internal/usecase/daemon/snapshot_restore_test.go +3 -3
| @@ -232,7 +232,6 @@ func TestRestoreIncrementalGenerationAcceptance(t *testing.T) { | |||
| 232 | 232 | require.NotNil(t, restored) | |
| 233 | 233 | require.False(t, restored.snapDirty.Load(), "a loaded generation must begin clean") | |
| 234 | 234 | require.Equal(t, uint64(9), restored.snapshotPublishedGeneration, "the loaded manifest is the repository generation head") | |
| 235 | - | require.True(t, restored.terminal.TrueColor, "restored sessions must retain startup terminal capability for future panes") | |
| 236 | 235 | require.Equal(t, 0, testAttachmentTabIndex(restored)) | |
| 237 | 236 | require.Equal(t, "/snapshot/cwd", restored.cwd) | |
| 238 | 237 | require.Len(t, restored.tabs, 1) | |
| @@ -321,7 +320,7 @@ func TestSnapshotRestoreUsesDaemonTerminalCapabilities(t *testing.T) { | |||
| 321 | 320 | "XDG_RUNTIME_DIR=/run/user/1000", | |
| 322 | 321 | "WAYLAND_DISPLAY=wayland-1", | |
| 323 | 322 | }, | |
| 324 | - | wantTerm: "TERM=xterm-direct", | |
| 323 | + | wantTerm: "TERM=xterm-256color", | |
| 325 | 324 | wantColorTerm: "COLORTERM=truecolor", | |
| 326 | 325 | notWantEntries: []string{"TERM=xterm-kitty"}, | |
| 327 | 326 | }, | |
| @@ -334,7 +333,8 @@ func TestSnapshotRestoreUsesDaemonTerminalCapabilities(t *testing.T) { | |||
| 334 | 333 | "WAYLAND_DISPLAY=wayland-1", | |
| 335 | 334 | }, | |
| 336 | 335 | wantTerm: "TERM=xterm-256color", | |
| 337 | - | notWantEntries: []string{"COLORTERM=old", "COLORTERM=truecolor"}, | |
| 336 | + | wantColorTerm: "COLORTERM=truecolor", | |
| 337 | + | notWantEntries: []string{"COLORTERM=old"}, | |
| 338 | 338 | }, | |
| 339 | 339 | } | |
| 340 | 340 | ||
| @@ -232,7 +232,6 @@ func TestRestoreIncrementalGenerationAcceptance(t *testing.T) { | |||||
| 232 | require.NotNil(t, restored) | 232 | require.NotNil(t, restored) | ||
| 233 | require.False(t, restored.snapDirty.Load(), "a loaded generation must begin clean") | 233 | require.False(t, restored.snapDirty.Load(), "a loaded generation must begin clean") | ||
| 234 | require.Equal(t, uint64(9), restored.snapshotPublishedGeneration, "the loaded manifest is the repository generation head") | 234 | require.Equal(t, uint64(9), restored.snapshotPublishedGeneration, "the loaded manifest is the repository generation head") | ||
| 235 | - | require.True(t, restored.terminal.TrueColor, "restored sessions must retain startup terminal capability for future panes") | |||
| 236 | require.Equal(t, 0, testAttachmentTabIndex(restored)) | 235 | require.Equal(t, 0, testAttachmentTabIndex(restored)) | ||
| 237 | require.Equal(t, "/snapshot/cwd", restored.cwd) | 236 | require.Equal(t, "/snapshot/cwd", restored.cwd) | ||
| 238 | require.Len(t, restored.tabs, 1) | 237 | require.Len(t, restored.tabs, 1) | ||
| @@ -321,7 +320,7 @@ func TestSnapshotRestoreUsesDaemonTerminalCapabilities(t *testing.T) { | |||||
| 321 | "XDG_RUNTIME_DIR=/run/user/1000", | 320 | "XDG_RUNTIME_DIR=/run/user/1000", | ||
| 322 | "WAYLAND_DISPLAY=wayland-1", | 321 | "WAYLAND_DISPLAY=wayland-1", | ||
| 323 | }, | 322 | }, | ||
| 324 | - | wantTerm: "TERM=xterm-direct", | 323 | + | wantTerm: "TERM=xterm-256color", |
| 325 | wantColorTerm: "COLORTERM=truecolor", | 324 | wantColorTerm: "COLORTERM=truecolor", | ||
| 326 | notWantEntries: []string{"TERM=xterm-kitty"}, | 325 | notWantEntries: []string{"TERM=xterm-kitty"}, | ||
| 327 | }, | 326 | }, | ||
| @@ -334,7 +333,8 @@ func TestSnapshotRestoreUsesDaemonTerminalCapabilities(t *testing.T) { | |||||
| 334 | "WAYLAND_DISPLAY=wayland-1", | 333 | "WAYLAND_DISPLAY=wayland-1", | ||
| 335 | }, | 334 | }, | ||
| 336 | wantTerm: "TERM=xterm-256color", | 335 | wantTerm: "TERM=xterm-256color", | ||
| 337 | - | notWantEntries: []string{"COLORTERM=old", "COLORTERM=truecolor"}, | 336 | + | wantColorTerm: "COLORTERM=truecolor", |
| 337 | + | notWantEntries: []string{"COLORTERM=old"}, | |||
| 338 | }, | 338 | }, | ||
| 339 | } | 339 | } | ||
| 340 | 340 | ||||
A internal/usecase/daemon/terminal_color.go +36 -0
| @@ -0,0 +1,36 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import renderer "github.com/bnema/vev-vt" | |
| 4 | + | ||
| 5 | + | // adaptFrameColors preserves the composed frame for capable attachments and | |
| 6 | + | // converts direct colors to the xterm 256-color cube for constrained ones. | |
| 7 | + | func adaptFrameColors(frame renderer.Frame, trueColor bool) renderer.Frame { | |
| 8 | + | if trueColor { | |
| 9 | + | return frame | |
| 10 | + | } | |
| 11 | + | adapted := frame.Clone() | |
| 12 | + | for i := range adapted.Cells { | |
| 13 | + | style := &adapted.Cells[i].Style | |
| 14 | + | if style.HasForegroundRGB { | |
| 15 | + | style.Foreground = xterm256Color(style.ForegroundRGB) | |
| 16 | + | style.HasForegroundRGB = false | |
| 17 | + | } | |
| 18 | + | if style.HasBackgroundRGB { | |
| 19 | + | style.Background = xterm256Color(style.BackgroundRGB) | |
| 20 | + | style.HasBackgroundRGB = false | |
| 21 | + | } | |
| 22 | + | if style.HasUnderlineColorRGB { | |
| 23 | + | style.UnderlineColor = xterm256Color(style.UnderlineColorRGB) | |
| 24 | + | style.HasUnderlineColorRGB = false | |
| 25 | + | style.HasUnderlineColor = true | |
| 26 | + | } | |
| 27 | + | } | |
| 28 | + | return adapted | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | func xterm256Color(rgb renderer.RGB) int { | |
| 32 | + | red := (int(rgb.R)*5 + 127) / 255 | |
| 33 | + | green := (int(rgb.G)*5 + 127) / 255 | |
| 34 | + | blue := (int(rgb.B)*5 + 127) / 255 | |
| 35 | + | return 16 + 36*red + 6*green + blue | |
| 36 | + | } | |
| @@ -0,0 +1,36 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import renderer "github.com/bnema/vev-vt" | |||
| 4 | + | ||||
| 5 | + | // adaptFrameColors preserves the composed frame for capable attachments and | |||
| 6 | + | // converts direct colors to the xterm 256-color cube for constrained ones. | |||
| 7 | + | func adaptFrameColors(frame renderer.Frame, trueColor bool) renderer.Frame { | |||
| 8 | + | if trueColor { | |||
| 9 | + | return frame | |||
| 10 | + | } | |||
| 11 | + | adapted := frame.Clone() | |||
| 12 | + | for i := range adapted.Cells { | |||
| 13 | + | style := &adapted.Cells[i].Style | |||
| 14 | + | if style.HasForegroundRGB { | |||
| 15 | + | style.Foreground = xterm256Color(style.ForegroundRGB) | |||
| 16 | + | style.HasForegroundRGB = false | |||
| 17 | + | } | |||
| 18 | + | if style.HasBackgroundRGB { | |||
| 19 | + | style.Background = xterm256Color(style.BackgroundRGB) | |||
| 20 | + | style.HasBackgroundRGB = false | |||
| 21 | + | } | |||
| 22 | + | if style.HasUnderlineColorRGB { | |||
| 23 | + | style.UnderlineColor = xterm256Color(style.UnderlineColorRGB) | |||
| 24 | + | style.HasUnderlineColorRGB = false | |||
| 25 | + | style.HasUnderlineColor = true | |||
| 26 | + | } | |||
| 27 | + | } | |||
| 28 | + | return adapted | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | func xterm256Color(rgb renderer.RGB) int { | |||
| 32 | + | red := (int(rgb.R)*5 + 127) / 255 | |||
| 33 | + | green := (int(rgb.G)*5 + 127) / 255 | |||
| 34 | + | blue := (int(rgb.B)*5 + 127) / 255 | |||
| 35 | + | return 16 + 36*red + 6*green + blue | |||
| 36 | + | } | |||
A internal/usecase/daemon/terminal_color_test.go +29 -0
| @@ -0,0 +1,29 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | renderer "github.com/bnema/vev-vt" | |
| 7 | + | "github.com/stretchr/testify/require" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestAdaptFrameColors(t *testing.T) { | |
| 11 | + | frame := renderer.NewFrame(1, 1) | |
| 12 | + | frame.Cells[0].Style.HasForegroundRGB = true | |
| 13 | + | frame.Cells[0].Style.ForegroundRGB = renderer.RGB{R: 255, G: 0, B: 0} | |
| 14 | + | frame.Cells[0].Style.HasBackgroundRGB = true | |
| 15 | + | frame.Cells[0].Style.BackgroundRGB = renderer.RGB{R: 0, G: 255, B: 0} | |
| 16 | + | frame.Cells[0].Style.HasUnderlineColor = true | |
| 17 | + | frame.Cells[0].Style.HasUnderlineColorRGB = true | |
| 18 | + | frame.Cells[0].Style.UnderlineColorRGB = renderer.RGB{R: 0, G: 0, B: 255} | |
| 19 | + | ||
| 20 | + | got := adaptFrameColors(frame, false) | |
| 21 | + | ||
| 22 | + | require.False(t, got.Cells[0].Style.HasForegroundRGB) | |
| 23 | + | require.Equal(t, 196, got.Cells[0].Style.Foreground) | |
| 24 | + | require.False(t, got.Cells[0].Style.HasBackgroundRGB) | |
| 25 | + | require.Equal(t, 46, got.Cells[0].Style.Background) | |
| 26 | + | require.False(t, got.Cells[0].Style.HasUnderlineColorRGB) | |
| 27 | + | require.Equal(t, 21, got.Cells[0].Style.UnderlineColor) | |
| 28 | + | require.True(t, frame.Cells[0].Style.HasForegroundRGB, "adaptation must not mutate composed state") | |
| 29 | + | } | |
| @@ -0,0 +1,29 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | renderer "github.com/bnema/vev-vt" | |||
| 7 | + | "github.com/stretchr/testify/require" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestAdaptFrameColors(t *testing.T) { | |||
| 11 | + | frame := renderer.NewFrame(1, 1) | |||
| 12 | + | frame.Cells[0].Style.HasForegroundRGB = true | |||
| 13 | + | frame.Cells[0].Style.ForegroundRGB = renderer.RGB{R: 255, G: 0, B: 0} | |||
| 14 | + | frame.Cells[0].Style.HasBackgroundRGB = true | |||
| 15 | + | frame.Cells[0].Style.BackgroundRGB = renderer.RGB{R: 0, G: 255, B: 0} | |||
| 16 | + | frame.Cells[0].Style.HasUnderlineColor = true | |||
| 17 | + | frame.Cells[0].Style.HasUnderlineColorRGB = true | |||
| 18 | + | frame.Cells[0].Style.UnderlineColorRGB = renderer.RGB{R: 0, G: 0, B: 255} | |||
| 19 | + | ||||
| 20 | + | got := adaptFrameColors(frame, false) | |||
| 21 | + | ||||
| 22 | + | require.False(t, got.Cells[0].Style.HasForegroundRGB) | |||
| 23 | + | require.Equal(t, 196, got.Cells[0].Style.Foreground) | |||
| 24 | + | require.False(t, got.Cells[0].Style.HasBackgroundRGB) | |||
| 25 | + | require.Equal(t, 46, got.Cells[0].Style.Background) | |||
| 26 | + | require.False(t, got.Cells[0].Style.HasUnderlineColorRGB) | |||
| 27 | + | require.Equal(t, 21, got.Cells[0].Style.UnderlineColor) | |||
| 28 | + | require.True(t, frame.Cells[0].Style.HasForegroundRGB, "adaptation must not mutate composed state") | |||
| 29 | + | } | |||