refactor(geometry): carry terminal geometry end to end (#219)
* feat(term): retain terminal pixel geometry * feat(pty): apply pixel geometry to winsize * test(pty): preserve pixel winsize geometry * feat(protocol): propagate terminal pixel geometry * fix(daemon): preserve pixel geometry through attachment lifecycle * fix(pty): apply initial pixel geometry * refactor(geometry): remove size-only seams * test(pty): report geometry cleanup errors * fix(daemon): preserve control session geometry
- commit
- 0933959e27f571916593a2fbe00483e00d219ec5
- author
- bnema · · 2026-08-26 20:15 UTC
- committer
- GitHub · · 2026-08-26 20:15 UTC
- parent
- bf4f2c5
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
76 files · +1146 -521
M go.mod +1 -1
| @@ -3,7 +3,7 @@ module github.com/bnema/vev | |||
| 3 | 3 | go 1.26 | |
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | - | github.com/bnema/vev-vt v0.3.0 | |
| 6 | + | github.com/bnema/vev-vt v0.4.1 | |
| 7 | 7 | github.com/stretchr/testify v1.11.1 | |
| 8 | 8 | ) | |
| 9 | 9 | ||
| @@ -3,7 +3,7 @@ module github.com/bnema/vev | |||||
| 3 | go 1.26 | 3 | go 1.26 | ||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | - | github.com/bnema/vev-vt v0.3.0 | 6 | + | github.com/bnema/vev-vt v0.4.1 |
| 7 | github.com/stretchr/testify v1.11.1 | 7 | github.com/stretchr/testify v1.11.1 | ||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
M go.sum +2 -2 generated file: collapsed
A internal/adapters/pty/geometry_linux_test.go +47 -0
| @@ -0,0 +1,47 @@ | |||
| 1 | + | //go:build linux | |
| 2 | + | ||
| 3 | + | package pty | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "os" | |
| 7 | + | "syscall" | |
| 8 | + | "testing" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/vev/internal/domain" | |
| 11 | + | "github.com/bnema/vev/pkg/rawterm" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | func TestSetGeometryPreservesPixelDimensions(t *testing.T) { | |
| 15 | + | masterFD, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0) | |
| 16 | + | if err != nil { | |
| 17 | + | t.Skipf("open /dev/ptmx: %v", err) | |
| 18 | + | } | |
| 19 | + | master := os.NewFile(uintptr(masterFD), "pty-master") | |
| 20 | + | defer func() { | |
| 21 | + | if err := master.Close(); err != nil { | |
| 22 | + | t.Errorf("close master: %v", err) | |
| 23 | + | } | |
| 24 | + | }() | |
| 25 | + | slave, err := rawterm.PreparePty(masterFD) | |
| 26 | + | if err != nil { | |
| 27 | + | t.Fatalf("PreparePty: %v", err) | |
| 28 | + | } | |
| 29 | + | defer func() { | |
| 30 | + | if err := slave.Close(); err != nil { | |
| 31 | + | t.Errorf("close slave: %v", err) | |
| 32 | + | } | |
| 33 | + | }() | |
| 34 | + | ||
| 35 | + | geometry := domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 640, PixelHeight: 384} | |
| 36 | + | if err := setGeometry(masterFD, geometry); err != nil { | |
| 37 | + | t.Fatalf("setGeometry: %v", err) | |
| 38 | + | } | |
| 39 | + | got, err := rawterm.GetWinsize(int(slave.Fd())) | |
| 40 | + | if err != nil { | |
| 41 | + | t.Fatalf("GetWinsize: %v", err) | |
| 42 | + | } | |
| 43 | + | want := rawterm.Winsize{Col: 80, Row: 24, Xpixel: 640, Ypixel: 384} | |
| 44 | + | if got != want { | |
| 45 | + | t.Fatalf("winsize = %+v, want %+v", got, want) | |
| 46 | + | } | |
| 47 | + | } | |
| @@ -0,0 +1,47 @@ | |||||
| 1 | + | //go:build linux | |||
| 2 | + | ||||
| 3 | + | package pty | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "os" | |||
| 7 | + | "syscall" | |||
| 8 | + | "testing" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/vev/internal/domain" | |||
| 11 | + | "github.com/bnema/vev/pkg/rawterm" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | func TestSetGeometryPreservesPixelDimensions(t *testing.T) { | |||
| 15 | + | masterFD, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0) | |||
| 16 | + | if err != nil { | |||
| 17 | + | t.Skipf("open /dev/ptmx: %v", err) | |||
| 18 | + | } | |||
| 19 | + | master := os.NewFile(uintptr(masterFD), "pty-master") | |||
| 20 | + | defer func() { | |||
| 21 | + | if err := master.Close(); err != nil { | |||
| 22 | + | t.Errorf("close master: %v", err) | |||
| 23 | + | } | |||
| 24 | + | }() | |||
| 25 | + | slave, err := rawterm.PreparePty(masterFD) | |||
| 26 | + | if err != nil { | |||
| 27 | + | t.Fatalf("PreparePty: %v", err) | |||
| 28 | + | } | |||
| 29 | + | defer func() { | |||
| 30 | + | if err := slave.Close(); err != nil { | |||
| 31 | + | t.Errorf("close slave: %v", err) | |||
| 32 | + | } | |||
| 33 | + | }() | |||
| 34 | + | ||||
| 35 | + | geometry := domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 640, PixelHeight: 384} | |||
| 36 | + | if err := setGeometry(masterFD, geometry); err != nil { | |||
| 37 | + | t.Fatalf("setGeometry: %v", err) | |||
| 38 | + | } | |||
| 39 | + | got, err := rawterm.GetWinsize(int(slave.Fd())) | |||
| 40 | + | if err != nil { | |||
| 41 | + | t.Fatalf("GetWinsize: %v", err) | |||
| 42 | + | } | |||
| 43 | + | want := rawterm.Winsize{Col: 80, Row: 24, Xpixel: 640, Ypixel: 384} | |||
| 44 | + | if got != want { | |||
| 45 | + | t.Fatalf("winsize = %+v, want %+v", got, want) | |||
| 46 | + | } | |||
| 47 | + | } | |||
M internal/adapters/pty/pty_darwin_test.go +5 -5
| @@ -46,12 +46,12 @@ func TestOpen_DarwinEchoAndOutputEOF(t *testing.T) { | |||
| 46 | 46 | } | |
| 47 | 47 | ||
| 48 | 48 | factory := pty.NewFactory() | |
| 49 | - | outputPTY, err := factory.Open(context.Background(), "sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 49 | + | outputPTY, err := factory.Open(context.Background(), "sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) | |
| 50 | 50 | require.NoError(t, err) | |
| 51 | 51 | t.Cleanup(func() { _ = outputPTY.Close() }) | |
| 52 | 52 | require.Equal(t, "hello", string(readAllDarwin(t, outputPTY, 5*time.Second))) | |
| 53 | 53 | ||
| 54 | - | echoPTY, err := factory.Open(context.Background(), "cat", nil, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 54 | + | echoPTY, err := factory.Open(context.Background(), "cat", nil, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) | |
| 55 | 55 | require.NoError(t, err) | |
| 56 | 56 | t.Cleanup(func() { _ = echoPTY.Close() }) | |
| 57 | 57 | _, err = io.WriteString(echoPTY, "roundtrip\n") | |
| @@ -98,7 +98,7 @@ func TestOpen_DarwinResizeAndForegroundPgid(t *testing.T) { | |||
| 98 | 98 | } | |
| 99 | 99 | ||
| 100 | 100 | const ready = "__vev_ready__" | |
| 101 | - | p, err := pty.NewFactory().Open(context.Background(), "sh", []string{"-c", "stty -echo; printf '__vev_ready__\\n'; IFS= read -r release; stty size"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 101 | + | p, err := pty.NewFactory().Open(context.Background(), "sh", []string{"-c", "stty -echo; printf '__vev_ready__\\n'; IFS= read -r release; stty size"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) | |
| 102 | 102 | require.NoError(t, err) | |
| 103 | 103 | t.Cleanup(func() { _ = p.Close() }) | |
| 104 | 104 | ||
| @@ -106,7 +106,7 @@ func TestOpen_DarwinResizeAndForegroundPgid(t *testing.T) { | |||
| 106 | 106 | pgid, err := p.ForegroundPgid() | |
| 107 | 107 | require.NoError(t, err) | |
| 108 | 108 | require.Greater(t, pgid, 0) | |
| 109 | - | require.NoError(t, p.Resize(domain.Size{Cols: 120, Rows: 40})) | |
| 109 | + | require.NoError(t, p.Resize(domain.Geometry{Size: domain.Size{Cols: 120, Rows: 40}})) | |
| 110 | 110 | _, err = io.WriteString(p, "release\n") | |
| 111 | 111 | require.NoError(t, err) | |
| 112 | 112 | require.Equal(t, "40 120", strings.TrimSpace(string(readAllDarwin(t, p, 5*time.Second)))) | |
| @@ -119,7 +119,7 @@ func TestOpen_DarwinContextCancellationEndsStream(t *testing.T) { | |||
| 119 | 119 | ||
| 120 | 120 | ctx, cancel := context.WithCancel(context.Background()) | |
| 121 | 121 | defer cancel() | |
| 122 | - | p, err := pty.NewFactory().Open(ctx, "sh", []string{"-c", "exec sleep 60"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 122 | + | p, err := pty.NewFactory().Open(ctx, "sh", []string{"-c", "exec sleep 60"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) | |
| 123 | 123 | require.NoError(t, err) | |
| 124 | 124 | t.Cleanup(func() { _ = p.Close() }) | |
| 125 | 125 | ||
| @@ -46,12 +46,12 @@ func TestOpen_DarwinEchoAndOutputEOF(t *testing.T) { | |||||
| 46 | } | 46 | } | ||
| 47 | 47 | ||||
| 48 | factory := pty.NewFactory() | 48 | factory := pty.NewFactory() | ||
| 49 | - | outputPTY, err := factory.Open(context.Background(), "sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 49 | + | outputPTY, err := factory.Open(context.Background(), "sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) |
| 50 | require.NoError(t, err) | 50 | require.NoError(t, err) | ||
| 51 | t.Cleanup(func() { _ = outputPTY.Close() }) | 51 | t.Cleanup(func() { _ = outputPTY.Close() }) | ||
| 52 | require.Equal(t, "hello", string(readAllDarwin(t, outputPTY, 5*time.Second))) | 52 | require.Equal(t, "hello", string(readAllDarwin(t, outputPTY, 5*time.Second))) | ||
| 53 | 53 | ||||
| 54 | - | echoPTY, err := factory.Open(context.Background(), "cat", nil, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 54 | + | echoPTY, err := factory.Open(context.Background(), "cat", nil, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) |
| 55 | require.NoError(t, err) | 55 | require.NoError(t, err) | ||
| 56 | t.Cleanup(func() { _ = echoPTY.Close() }) | 56 | t.Cleanup(func() { _ = echoPTY.Close() }) | ||
| 57 | _, err = io.WriteString(echoPTY, "roundtrip\n") | 57 | _, err = io.WriteString(echoPTY, "roundtrip\n") | ||
| @@ -98,7 +98,7 @@ func TestOpen_DarwinResizeAndForegroundPgid(t *testing.T) { | |||||
| 98 | } | 98 | } | ||
| 99 | 99 | ||||
| 100 | const ready = "__vev_ready__" | 100 | const ready = "__vev_ready__" | ||
| 101 | - | p, err := pty.NewFactory().Open(context.Background(), "sh", []string{"-c", "stty -echo; printf '__vev_ready__\\n'; IFS= read -r release; stty size"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 101 | + | p, err := pty.NewFactory().Open(context.Background(), "sh", []string{"-c", "stty -echo; printf '__vev_ready__\\n'; IFS= read -r release; stty size"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) |
| 102 | require.NoError(t, err) | 102 | require.NoError(t, err) | ||
| 103 | t.Cleanup(func() { _ = p.Close() }) | 103 | t.Cleanup(func() { _ = p.Close() }) | ||
| 104 | 104 | ||||
| @@ -106,7 +106,7 @@ func TestOpen_DarwinResizeAndForegroundPgid(t *testing.T) { | |||||
| 106 | pgid, err := p.ForegroundPgid() | 106 | pgid, err := p.ForegroundPgid() | ||
| 107 | require.NoError(t, err) | 107 | require.NoError(t, err) | ||
| 108 | require.Greater(t, pgid, 0) | 108 | require.Greater(t, pgid, 0) | ||
| 109 | - | require.NoError(t, p.Resize(domain.Size{Cols: 120, Rows: 40})) | 109 | + | require.NoError(t, p.Resize(domain.Geometry{Size: domain.Size{Cols: 120, Rows: 40}})) |
| 110 | _, err = io.WriteString(p, "release\n") | 110 | _, err = io.WriteString(p, "release\n") | ||
| 111 | require.NoError(t, err) | 111 | require.NoError(t, err) | ||
| 112 | require.Equal(t, "40 120", strings.TrimSpace(string(readAllDarwin(t, p, 5*time.Second)))) | 112 | require.Equal(t, "40 120", strings.TrimSpace(string(readAllDarwin(t, p, 5*time.Second)))) | ||
| @@ -119,7 +119,7 @@ func TestOpen_DarwinContextCancellationEndsStream(t *testing.T) { | |||||
| 119 | 119 | ||||
| 120 | ctx, cancel := context.WithCancel(context.Background()) | 120 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 121 | defer cancel() | 121 | defer cancel() | ||
| 122 | - | p, err := pty.NewFactory().Open(ctx, "sh", []string{"-c", "exec sleep 60"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 122 | + | p, err := pty.NewFactory().Open(ctx, "sh", []string{"-c", "exec sleep 60"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) |
| 123 | require.NoError(t, err) | 123 | require.NoError(t, err) | ||
| 124 | t.Cleanup(func() { _ = p.Close() }) | 124 | t.Cleanup(func() { _ = p.Close() }) | ||
| 125 | 125 | ||||
M internal/adapters/pty/pty_linux_test.go +10 -10
| @@ -54,7 +54,7 @@ func TestOpen_ChildOutputToEOF(t *testing.T) { | |||
| 54 | 54 | t.Skip("skipping pty integration test in -short mode") | |
| 55 | 55 | } | |
| 56 | 56 | f := newFactory() | |
| 57 | - | p, err := f.Open(context.Background(), "sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 57 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) | |
| 58 | 58 | require.NoError(t, err) | |
| 59 | 59 | t.Cleanup(func() { _ = p.Close() }) | |
| 60 | 60 | ||
| @@ -71,7 +71,7 @@ func TestOpen_EchoRoundtrip(t *testing.T) { | |||
| 71 | 71 | t.Skip("skipping pty integration test in -short mode") | |
| 72 | 72 | } | |
| 73 | 73 | f := newFactory() | |
| 74 | - | p, err := f.Open(context.Background(), "cat", nil, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 74 | + | p, err := f.Open(context.Background(), "cat", nil, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) | |
| 75 | 75 | require.NoError(t, err) | |
| 76 | 76 | t.Cleanup(func() { _ = p.Close() }) | |
| 77 | 77 | ||
| @@ -104,7 +104,7 @@ func TestForegroundPgid(t *testing.T) { | |||
| 104 | 104 | t.Skip("skipping pty integration test in -short mode") | |
| 105 | 105 | } | |
| 106 | 106 | f := newFactory() | |
| 107 | - | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 2"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 107 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 2"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) | |
| 108 | 108 | require.NoError(t, err) | |
| 109 | 109 | t.Cleanup(func() { _ = p.Close() }) | |
| 110 | 110 | ||
| @@ -142,12 +142,12 @@ func TestResize_SttySize(t *testing.T) { | |||
| 142 | 142 | for _, tt := range tests { | |
| 143 | 143 | t.Run(tt.name, func(t *testing.T) { | |
| 144 | 144 | f := newFactory() | |
| 145 | - | p, err := f.Open(context.Background(), "sh", []string{"-c", tt.script}, os.Environ(), "", tt.initial) | |
| 145 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", tt.script}, os.Environ(), "", domain.Geometry{Size: tt.initial}) | |
| 146 | 146 | require.NoError(t, err) | |
| 147 | 147 | t.Cleanup(func() { _ = p.Close() }) | |
| 148 | 148 | ||
| 149 | 149 | if tt.resize != nil { | |
| 150 | - | require.NoError(t, p.Resize(*tt.resize)) | |
| 150 | + | require.NoError(t, p.Resize(domain.Geometry{Size: *tt.resize})) | |
| 151 | 151 | } | |
| 152 | 152 | ||
| 153 | 153 | out := readAll(t, p, 5*time.Second) | |
| @@ -161,7 +161,7 @@ func TestClose_ReapsChildNoZombie(t *testing.T) { | |||
| 161 | 161 | t.Skip("skipping pty integration test in -short mode") | |
| 162 | 162 | } | |
| 163 | 163 | f := newFactory() | |
| 164 | - | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 30"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 164 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 30"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) | |
| 165 | 165 | require.NoError(t, err) | |
| 166 | 166 | ||
| 167 | 167 | pid := p.Pid() | |
| @@ -192,7 +192,7 @@ func TestClose_TerminatesLongRunningChild(t *testing.T) { | |||
| 192 | 192 | t.Skip("skipping pty integration test in -short mode") | |
| 193 | 193 | } | |
| 194 | 194 | f := newFactory() | |
| 195 | - | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 60"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 195 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 60"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) | |
| 196 | 196 | require.NoError(t, err) | |
| 197 | 197 | pid := p.Pid() | |
| 198 | 198 | ||
| @@ -214,7 +214,7 @@ func TestOpen_ContextCancellationTerminatesProcessGroup(t *testing.T) { | |||
| 214 | 214 | ||
| 215 | 215 | // The background sleep ignores SIGHUP so killing only the session leader | |
| 216 | 216 | // cannot accidentally pass via the terminal's hangup behavior. | |
| 217 | - | p, err := newFactory().Open(ctx, "sh", []string{"-c", `(trap '' HUP; exec sleep 60) & child=$!; printf '%s\n' "$child"; wait "$child"`}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 217 | + | p, err := newFactory().Open(ctx, "sh", []string{"-c", `(trap '' HUP; exec sleep 60) & child=$!; printf '%s\n' "$child"; wait "$child"`}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) | |
| 218 | 218 | require.NoError(t, err) | |
| 219 | 219 | t.Cleanup(func() { _ = p.Close() }) | |
| 220 | 220 | ||
| @@ -249,7 +249,7 @@ func TestResize_DeliversSIGWINCH(t *testing.T) { | |||
| 249 | 249 | // deliver SIGWINCH to the foreground process group. | |
| 250 | 250 | script := `trap 'echo GOTWINCH; exit 0' WINCH; echo READY; i=0; while [ $i -lt 200 ]; do sleep 0.05; i=$((i+1)); done` | |
| 251 | 251 | f := newFactory() | |
| 252 | - | p, err := f.Open(context.Background(), "sh", []string{"-c", script}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 252 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", script}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) | |
| 253 | 253 | require.NoError(t, err) | |
| 254 | 254 | t.Cleanup(func() { _ = p.Close() }) | |
| 255 | 255 | ||
| @@ -268,7 +268,7 @@ func TestResize_DeliversSIGWINCH(t *testing.T) { | |||
| 268 | 268 | } | |
| 269 | 269 | } | |
| 270 | 270 | ||
| 271 | - | require.NoError(t, p.Resize(domain.Size{Cols: 100, Rows: 30})) | |
| 271 | + | require.NoError(t, p.Resize(domain.Geometry{Size: domain.Size{Cols: 100, Rows: 30}})) | |
| 272 | 272 | ||
| 273 | 273 | for !strings.Contains(acc, "GOTWINCH") { | |
| 274 | 274 | if time.Now().After(deadline) { | |
| @@ -54,7 +54,7 @@ func TestOpen_ChildOutputToEOF(t *testing.T) { | |||||
| 54 | t.Skip("skipping pty integration test in -short mode") | 54 | t.Skip("skipping pty integration test in -short mode") | ||
| 55 | } | 55 | } | ||
| 56 | f := newFactory() | 56 | f := newFactory() | ||
| 57 | - | p, err := f.Open(context.Background(), "sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 57 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) |
| 58 | require.NoError(t, err) | 58 | require.NoError(t, err) | ||
| 59 | t.Cleanup(func() { _ = p.Close() }) | 59 | t.Cleanup(func() { _ = p.Close() }) | ||
| 60 | 60 | ||||
| @@ -71,7 +71,7 @@ func TestOpen_EchoRoundtrip(t *testing.T) { | |||||
| 71 | t.Skip("skipping pty integration test in -short mode") | 71 | t.Skip("skipping pty integration test in -short mode") | ||
| 72 | } | 72 | } | ||
| 73 | f := newFactory() | 73 | f := newFactory() | ||
| 74 | - | p, err := f.Open(context.Background(), "cat", nil, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 74 | + | p, err := f.Open(context.Background(), "cat", nil, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) |
| 75 | require.NoError(t, err) | 75 | require.NoError(t, err) | ||
| 76 | t.Cleanup(func() { _ = p.Close() }) | 76 | t.Cleanup(func() { _ = p.Close() }) | ||
| 77 | 77 | ||||
| @@ -104,7 +104,7 @@ func TestForegroundPgid(t *testing.T) { | |||||
| 104 | t.Skip("skipping pty integration test in -short mode") | 104 | t.Skip("skipping pty integration test in -short mode") | ||
| 105 | } | 105 | } | ||
| 106 | f := newFactory() | 106 | f := newFactory() | ||
| 107 | - | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 2"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 107 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 2"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) |
| 108 | require.NoError(t, err) | 108 | require.NoError(t, err) | ||
| 109 | t.Cleanup(func() { _ = p.Close() }) | 109 | t.Cleanup(func() { _ = p.Close() }) | ||
| 110 | 110 | ||||
| @@ -142,12 +142,12 @@ func TestResize_SttySize(t *testing.T) { | |||||
| 142 | for _, tt := range tests { | 142 | for _, tt := range tests { | ||
| 143 | t.Run(tt.name, func(t *testing.T) { | 143 | t.Run(tt.name, func(t *testing.T) { | ||
| 144 | f := newFactory() | 144 | f := newFactory() | ||
| 145 | - | p, err := f.Open(context.Background(), "sh", []string{"-c", tt.script}, os.Environ(), "", tt.initial) | 145 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", tt.script}, os.Environ(), "", domain.Geometry{Size: tt.initial}) |
| 146 | require.NoError(t, err) | 146 | require.NoError(t, err) | ||
| 147 | t.Cleanup(func() { _ = p.Close() }) | 147 | t.Cleanup(func() { _ = p.Close() }) | ||
| 148 | 148 | ||||
| 149 | if tt.resize != nil { | 149 | if tt.resize != nil { | ||
| 150 | - | require.NoError(t, p.Resize(*tt.resize)) | 150 | + | require.NoError(t, p.Resize(domain.Geometry{Size: *tt.resize})) |
| 151 | } | 151 | } | ||
| 152 | 152 | ||||
| 153 | out := readAll(t, p, 5*time.Second) | 153 | out := readAll(t, p, 5*time.Second) | ||
| @@ -161,7 +161,7 @@ func TestClose_ReapsChildNoZombie(t *testing.T) { | |||||
| 161 | t.Skip("skipping pty integration test in -short mode") | 161 | t.Skip("skipping pty integration test in -short mode") | ||
| 162 | } | 162 | } | ||
| 163 | f := newFactory() | 163 | f := newFactory() | ||
| 164 | - | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 30"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 164 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 30"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) |
| 165 | require.NoError(t, err) | 165 | require.NoError(t, err) | ||
| 166 | 166 | ||||
| 167 | pid := p.Pid() | 167 | pid := p.Pid() | ||
| @@ -192,7 +192,7 @@ func TestClose_TerminatesLongRunningChild(t *testing.T) { | |||||
| 192 | t.Skip("skipping pty integration test in -short mode") | 192 | t.Skip("skipping pty integration test in -short mode") | ||
| 193 | } | 193 | } | ||
| 194 | f := newFactory() | 194 | f := newFactory() | ||
| 195 | - | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 60"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 195 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 60"}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) |
| 196 | require.NoError(t, err) | 196 | require.NoError(t, err) | ||
| 197 | pid := p.Pid() | 197 | pid := p.Pid() | ||
| 198 | 198 | ||||
| @@ -214,7 +214,7 @@ func TestOpen_ContextCancellationTerminatesProcessGroup(t *testing.T) { | |||||
| 214 | 214 | ||||
| 215 | // The background sleep ignores SIGHUP so killing only the session leader | 215 | // The background sleep ignores SIGHUP so killing only the session leader | ||
| 216 | // cannot accidentally pass via the terminal's hangup behavior. | 216 | // cannot accidentally pass via the terminal's hangup behavior. | ||
| 217 | - | p, err := newFactory().Open(ctx, "sh", []string{"-c", `(trap '' HUP; exec sleep 60) & child=$!; printf '%s\n' "$child"; wait "$child"`}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 217 | + | p, err := newFactory().Open(ctx, "sh", []string{"-c", `(trap '' HUP; exec sleep 60) & child=$!; printf '%s\n' "$child"; wait "$child"`}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) |
| 218 | require.NoError(t, err) | 218 | require.NoError(t, err) | ||
| 219 | t.Cleanup(func() { _ = p.Close() }) | 219 | t.Cleanup(func() { _ = p.Close() }) | ||
| 220 | 220 | ||||
| @@ -249,7 +249,7 @@ func TestResize_DeliversSIGWINCH(t *testing.T) { | |||||
| 249 | // deliver SIGWINCH to the foreground process group. | 249 | // deliver SIGWINCH to the foreground process group. | ||
| 250 | script := `trap 'echo GOTWINCH; exit 0' WINCH; echo READY; i=0; while [ $i -lt 200 ]; do sleep 0.05; i=$((i+1)); done` | 250 | script := `trap 'echo GOTWINCH; exit 0' WINCH; echo READY; i=0; while [ $i -lt 200 ]; do sleep 0.05; i=$((i+1)); done` | ||
| 251 | f := newFactory() | 251 | f := newFactory() | ||
| 252 | - | p, err := f.Open(context.Background(), "sh", []string{"-c", script}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 252 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", script}, os.Environ(), "", domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}) |
| 253 | require.NoError(t, err) | 253 | require.NoError(t, err) | ||
| 254 | t.Cleanup(func() { _ = p.Close() }) | 254 | t.Cleanup(func() { _ = p.Close() }) | ||
| 255 | 255 | ||||
| @@ -268,7 +268,7 @@ func TestResize_DeliversSIGWINCH(t *testing.T) { | |||||
| 268 | } | 268 | } | ||
| 269 | } | 269 | } | ||
| 270 | 270 | ||||
| 271 | - | require.NoError(t, p.Resize(domain.Size{Cols: 100, Rows: 30})) | 271 | + | require.NoError(t, p.Resize(domain.Geometry{Size: domain.Size{Cols: 100, Rows: 30}})) |
| 272 | 272 | ||||
| 273 | for !strings.Contains(acc, "GOTWINCH") { | 273 | for !strings.Contains(acc, "GOTWINCH") { | ||
| 274 | if time.Now().After(deadline) { | 274 | if time.Now().After(deadline) { | ||
M internal/adapters/pty/pty_unix.go +25 -10
| @@ -36,9 +36,10 @@ func NewFactory() *Factory { return &Factory{} } | |||
| 36 | 36 | // Open spawns command with args attached to a freshly allocated pseudo-terminal | |
| 37 | 37 | // and returns the master side as a ports.PTY. env is passed to the child | |
| 38 | 38 | // verbatim (nil means inherit the current process environment); the caller | |
| 39 | - | // decides TERM and friends. sz sets the terminal window size before the child | |
| 40 | - | // starts, so the child observes the correct dimensions on its first query. | |
| 41 | - | func (Factory) Open(ctx context.Context, command string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { | |
| 39 | + | // decides TERM and friends. geometry sets the terminal window geometry before | |
| 40 | + | // the child starts, so the child observes the correct dimensions on its first | |
| 41 | + | // query. | |
| 42 | + | func (Factory) Open(ctx context.Context, command string, args []string, env []string, dir string, geometry domain.Geometry) (ports.PTY, error) { | |
| 42 | 43 | if err := ctx.Err(); err != nil { | |
| 43 | 44 | return nil, err | |
| 44 | 45 | } | |
| @@ -63,10 +64,11 @@ func (Factory) Open(ctx context.Context, command string, args []string, env []st | |||
| 63 | 64 | // parent always drops the slave once Start has run (or on any error). | |
| 64 | 65 | defer func() { _ = slave.Close() }() | |
| 65 | 66 | ||
| 66 | - | // Set the initial window size on the master before Start so the child's very | |
| 67 | - | // first size query already reflects sz. | |
| 68 | - | if sz.Valid() { | |
| 69 | - | if err := setWinsize(masterFd, sz); err != nil { | |
| 67 | + | // Set the initial window geometry on the master before Start so the child's | |
| 68 | + | // very first size query already reflects the authoritative geometry. | |
| 69 | + | geometry = geometry.NormalizePixels() | |
| 70 | + | if geometry.Valid() { | |
| 71 | + | if err := setGeometry(masterFd, geometry); err != nil { | |
| 70 | 72 | return nil, fmt.Errorf("pty: initial TIOCSWINSZ: %w", err) | |
| 71 | 73 | } | |
| 72 | 74 | } | |
| @@ -148,14 +150,15 @@ func (p *unixPTY) Write(b []byte) (int, error) { | |||
| 148 | 150 | // every subsequent Read into a thread-parking syscall and breaking the | |
| 149 | 151 | // Close-unblocks-Read behavior. Control also pins the fd for the duration and | |
| 150 | 152 | // fails cleanly (os.ErrClosed) after Close. | |
| 151 | - | func (p *unixPTY) Resize(sz domain.Size) error { | |
| 153 | + | func (p *unixPTY) Resize(geometry domain.Geometry) error { | |
| 154 | + | geometry = geometry.NormalizePixels() | |
| 152 | 155 | rc, err := p.master.SyscallConn() | |
| 153 | 156 | if err != nil { | |
| 154 | 157 | return fmt.Errorf("pty: resize: %w", err) | |
| 155 | 158 | } | |
| 156 | 159 | var ioctlErr error | |
| 157 | 160 | if err := rc.Control(func(fd uintptr) { | |
| 158 | - | ioctlErr = setWinsize(int(fd), sz) | |
| 161 | + | ioctlErr = setGeometry(int(fd), geometry) | |
| 159 | 162 | }); err != nil { | |
| 160 | 163 | return fmt.Errorf("pty: resize: %w", err) | |
| 161 | 164 | } | |
| @@ -228,5 +231,17 @@ func signalProcessGroup(pid int, signal syscall.Signal) error { | |||
| 228 | 231 | ||
| 229 | 232 | // setWinsize applies sz to the terminal referenced by fd via TIOCSWINSZ. | |
| 230 | 233 | func setWinsize(fd int, sz domain.Size) error { | |
| 231 | - | return rawterm.SetWinsize(fd, uint16(sz.Cols), uint16(sz.Rows)) | |
| 234 | + | return setGeometry(fd, domain.Geometry{Size: sz}) | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | // setGeometry applies cell dimensions and any pixel dimensions reported by the | |
| 238 | + | // geometry authority. Zero pixel dimensions deliberately preserve the kernel's | |
| 239 | + | // explicit unknown value instead of fabricating a cell-derived size. | |
| 240 | + | func setGeometry(fd int, geometry domain.Geometry) error { | |
| 241 | + | return rawterm.SetWinsizeFull(fd, rawterm.Winsize{ | |
| 242 | + | Col: uint16(geometry.Cols), | |
| 243 | + | Row: uint16(geometry.Rows), | |
| 244 | + | Xpixel: uint16(geometry.PixelWidth), | |
| 245 | + | Ypixel: uint16(geometry.PixelHeight), | |
| 246 | + | }) | |
| 232 | 247 | } | |
| @@ -36,9 +36,10 @@ func NewFactory() *Factory { return &Factory{} } | |||||
| 36 | // Open spawns command with args attached to a freshly allocated pseudo-terminal | 36 | // Open spawns command with args attached to a freshly allocated pseudo-terminal | ||
| 37 | // and returns the master side as a ports.PTY. env is passed to the child | 37 | // and returns the master side as a ports.PTY. env is passed to the child | ||
| 38 | // verbatim (nil means inherit the current process environment); the caller | 38 | // verbatim (nil means inherit the current process environment); the caller | ||
| 39 | - | // decides TERM and friends. sz sets the terminal window size before the child | 39 | + | // decides TERM and friends. geometry sets the terminal window geometry before |
| 40 | - | // starts, so the child observes the correct dimensions on its first query. | 40 | + | // the child starts, so the child observes the correct dimensions on its first |
| 41 | - | func (Factory) Open(ctx context.Context, command string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { | 41 | + | // query. |
| 42 | + | func (Factory) Open(ctx context.Context, command string, args []string, env []string, dir string, geometry domain.Geometry) (ports.PTY, error) { | |||
| 42 | if err := ctx.Err(); err != nil { | 43 | if err := ctx.Err(); err != nil { | ||
| 43 | return nil, err | 44 | return nil, err | ||
| 44 | } | 45 | } | ||
| @@ -63,10 +64,11 @@ func (Factory) Open(ctx context.Context, command string, args []string, env []st | |||||
| 63 | // parent always drops the slave once Start has run (or on any error). | 64 | // parent always drops the slave once Start has run (or on any error). | ||
| 64 | defer func() { _ = slave.Close() }() | 65 | defer func() { _ = slave.Close() }() | ||
| 65 | 66 | ||||
| 66 | - | // Set the initial window size on the master before Start so the child's very | 67 | + | // Set the initial window geometry on the master before Start so the child's |
| 67 | - | // first size query already reflects sz. | 68 | + | // very first size query already reflects the authoritative geometry. |
| 68 | - | if sz.Valid() { | 69 | + | geometry = geometry.NormalizePixels() |
| 69 | - | if err := setWinsize(masterFd, sz); err != nil { | 70 | + | if geometry.Valid() { |
| 71 | + | if err := setGeometry(masterFd, geometry); err != nil { | |||
| 70 | return nil, fmt.Errorf("pty: initial TIOCSWINSZ: %w", err) | 72 | return nil, fmt.Errorf("pty: initial TIOCSWINSZ: %w", err) | ||
| 71 | } | 73 | } | ||
| 72 | } | 74 | } | ||
| @@ -148,14 +150,15 @@ func (p *unixPTY) Write(b []byte) (int, error) { | |||||
| 148 | // every subsequent Read into a thread-parking syscall and breaking the | 150 | // every subsequent Read into a thread-parking syscall and breaking the | ||
| 149 | // Close-unblocks-Read behavior. Control also pins the fd for the duration and | 151 | // Close-unblocks-Read behavior. Control also pins the fd for the duration and | ||
| 150 | // fails cleanly (os.ErrClosed) after Close. | 152 | // fails cleanly (os.ErrClosed) after Close. | ||
| 151 | - | func (p *unixPTY) Resize(sz domain.Size) error { | 153 | + | func (p *unixPTY) Resize(geometry domain.Geometry) error { |
| 154 | + | geometry = geometry.NormalizePixels() | |||
| 152 | rc, err := p.master.SyscallConn() | 155 | rc, err := p.master.SyscallConn() | ||
| 153 | if err != nil { | 156 | if err != nil { | ||
| 154 | return fmt.Errorf("pty: resize: %w", err) | 157 | return fmt.Errorf("pty: resize: %w", err) | ||
| 155 | } | 158 | } | ||
| 156 | var ioctlErr error | 159 | var ioctlErr error | ||
| 157 | if err := rc.Control(func(fd uintptr) { | 160 | if err := rc.Control(func(fd uintptr) { | ||
| 158 | - | ioctlErr = setWinsize(int(fd), sz) | 161 | + | ioctlErr = setGeometry(int(fd), geometry) |
| 159 | }); err != nil { | 162 | }); err != nil { | ||
| 160 | return fmt.Errorf("pty: resize: %w", err) | 163 | return fmt.Errorf("pty: resize: %w", err) | ||
| 161 | } | 164 | } | ||
| @@ -228,5 +231,17 @@ func signalProcessGroup(pid int, signal syscall.Signal) error { | |||||
| 228 | 231 | ||||
| 229 | // setWinsize applies sz to the terminal referenced by fd via TIOCSWINSZ. | 232 | // setWinsize applies sz to the terminal referenced by fd via TIOCSWINSZ. | ||
| 230 | func setWinsize(fd int, sz domain.Size) error { | 233 | func setWinsize(fd int, sz domain.Size) error { | ||
| 231 | - | return rawterm.SetWinsize(fd, uint16(sz.Cols), uint16(sz.Rows)) | 234 | + | return setGeometry(fd, domain.Geometry{Size: sz}) |
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | // setGeometry applies cell dimensions and any pixel dimensions reported by the | |||
| 238 | + | // geometry authority. Zero pixel dimensions deliberately preserve the kernel's | |||
| 239 | + | // explicit unknown value instead of fabricating a cell-derived size. | |||
| 240 | + | func setGeometry(fd int, geometry domain.Geometry) error { | |||
| 241 | + | return rawterm.SetWinsizeFull(fd, rawterm.Winsize{ | |||
| 242 | + | Col: uint16(geometry.Cols), | |||
| 243 | + | Row: uint16(geometry.Rows), | |||
| 244 | + | Xpixel: uint16(geometry.PixelWidth), | |||
| 245 | + | Ypixel: uint16(geometry.PixelHeight), | |||
| 246 | + | }) | |||
| 232 | } | 247 | } | ||
M internal/adapters/term/pty_integration_test.go +8 -5
| @@ -41,7 +41,7 @@ func TestTerminal_RealPTY_RawModeSizeAndEscapes(t *testing.T) { | |||
| 41 | 41 | defer func() { _ = master.Close() }() | |
| 42 | 42 | defer func() { _ = slave.Close() }() | |
| 43 | 43 | ||
| 44 | - | if err := rawterm.SetWinsize(int(slave.Fd()), 80, 24); err != nil { | |
| 44 | + | if err := rawterm.SetWinsizeFull(int(slave.Fd()), rawterm.Winsize{Col: 80, Row: 24, Xpixel: 640, Ypixel: 384}); err != nil { | |
| 45 | 45 | t.Fatalf("set winsize: %v", err) | |
| 46 | 46 | } | |
| 47 | 47 | ||
| @@ -55,12 +55,15 @@ func TestTerminal_RealPTY_RawModeSizeAndEscapes(t *testing.T) { | |||
| 55 | 55 | ||
| 56 | 56 | tm := NewWithFiles(slave, slave) | |
| 57 | 57 | ||
| 58 | - | sz, err := tm.Size() | |
| 58 | + | geometry, err := tm.Geometry() | |
| 59 | 59 | if err != nil { | |
| 60 | - | t.Fatalf("Size: %v", err) | |
| 60 | + | t.Fatalf("Geometry: %v", err) | |
| 61 | 61 | } | |
| 62 | - | if sz.Cols != 80 || sz.Rows != 24 { | |
| 63 | - | t.Fatalf("Size = %+v, want {Cols:80 Rows:24}", sz) | |
| 62 | + | if geometry.Cols != 80 || geometry.Rows != 24 { | |
| 63 | + | t.Fatalf("Geometry = %+v, want {Cols:80 Rows:24}", geometry) | |
| 64 | + | } | |
| 65 | + | if geometry.PixelWidth != 640 || geometry.PixelHeight != 384 { | |
| 66 | + | t.Fatalf("Geometry = %+v, want 640x384 pixels", geometry) | |
| 64 | 67 | } | |
| 65 | 68 | ||
| 66 | 69 | restore, err := tm.EnterRaw() | |
| @@ -41,7 +41,7 @@ func TestTerminal_RealPTY_RawModeSizeAndEscapes(t *testing.T) { | |||||
| 41 | defer func() { _ = master.Close() }() | 41 | defer func() { _ = master.Close() }() | ||
| 42 | defer func() { _ = slave.Close() }() | 42 | defer func() { _ = slave.Close() }() | ||
| 43 | 43 | ||||
| 44 | - | if err := rawterm.SetWinsize(int(slave.Fd()), 80, 24); err != nil { | 44 | + | if err := rawterm.SetWinsizeFull(int(slave.Fd()), rawterm.Winsize{Col: 80, Row: 24, Xpixel: 640, Ypixel: 384}); err != nil { |
| 45 | t.Fatalf("set winsize: %v", err) | 45 | t.Fatalf("set winsize: %v", err) | ||
| 46 | } | 46 | } | ||
| 47 | 47 | ||||
| @@ -55,12 +55,15 @@ func TestTerminal_RealPTY_RawModeSizeAndEscapes(t *testing.T) { | |||||
| 55 | 55 | ||||
| 56 | tm := NewWithFiles(slave, slave) | 56 | tm := NewWithFiles(slave, slave) | ||
| 57 | 57 | ||||
| 58 | - | sz, err := tm.Size() | 58 | + | geometry, err := tm.Geometry() |
| 59 | if err != nil { | 59 | if err != nil { | ||
| 60 | - | t.Fatalf("Size: %v", err) | 60 | + | t.Fatalf("Geometry: %v", err) |
| 61 | } | 61 | } | ||
| 62 | - | if sz.Cols != 80 || sz.Rows != 24 { | 62 | + | if geometry.Cols != 80 || geometry.Rows != 24 { |
| 63 | - | t.Fatalf("Size = %+v, want {Cols:80 Rows:24}", sz) | 63 | + | t.Fatalf("Geometry = %+v, want {Cols:80 Rows:24}", geometry) |
| 64 | + | } | |||
| 65 | + | if geometry.PixelWidth != 640 || geometry.PixelHeight != 384 { | |||
| 66 | + | t.Fatalf("Geometry = %+v, want 640x384 pixels", geometry) | |||
| 64 | } | 67 | } | ||
| 65 | 68 | ||||
| 66 | restore, err := tm.EnterRaw() | 69 | restore, err := tm.EnterRaw() | ||
M internal/adapters/term/resize.go +7 -7
| @@ -7,14 +7,14 @@ import ( | |||
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | 9 | // resizeLoop watches sig for signal notifications (SIGWINCH in | |
| 10 | - | // production) and emits the coalesced terminal size on out: a burst of | |
| 11 | - | // signals collapses to a single emitted size, since any signals still | |
| 10 | + | // production) and emits the coalesced terminal geometry on out: a burst of | |
| 11 | + | // signals collapses to a single emitted value, since any signals still | |
| 12 | 12 | // queued once the first is observed are drained before querying the | |
| 13 | - | // size. getSize is called to resolve the size to emit; a getSize error | |
| 14 | - | // is treated as "no event this round" and the loop continues waiting. | |
| 13 | + | // geometry. getGeometry is called to resolve the value to emit; an error is | |
| 14 | + | // treated as "no event this round" and the loop continues waiting. | |
| 15 | 15 | // | |
| 16 | 16 | // The loop exits and closes out when quit is closed. | |
| 17 | - | func resizeLoop(sig <-chan os.Signal, out chan<- domain.Size, quit <-chan struct{}, getSize func() (domain.Size, error)) { | |
| 17 | + | func resizeLoop(sig <-chan os.Signal, out chan<- domain.Geometry, quit <-chan struct{}, getGeometry func() (domain.Geometry, error)) { | |
| 18 | 18 | defer close(out) | |
| 19 | 19 | for { | |
| 20 | 20 | select { | |
| @@ -22,12 +22,12 @@ func resizeLoop(sig <-chan os.Signal, out chan<- domain.Size, quit <-chan struct | |||
| 22 | 22 | return | |
| 23 | 23 | case <-sig: | |
| 24 | 24 | drainSignals(sig) | |
| 25 | - | sz, err := getSize() | |
| 25 | + | geometry, err := getGeometry() | |
| 26 | 26 | if err != nil { | |
| 27 | 27 | continue | |
| 28 | 28 | } | |
| 29 | 29 | select { | |
| 30 | - | case out <- sz: | |
| 30 | + | case out <- geometry: | |
| 31 | 31 | case <-quit: | |
| 32 | 32 | return | |
| 33 | 33 | } | |
| @@ -7,14 +7,14 @@ import ( | |||||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | // resizeLoop watches sig for signal notifications (SIGWINCH in | 9 | // resizeLoop watches sig for signal notifications (SIGWINCH in | ||
| 10 | - | // production) and emits the coalesced terminal size on out: a burst of | 10 | + | // production) and emits the coalesced terminal geometry on out: a burst of |
| 11 | - | // signals collapses to a single emitted size, since any signals still | 11 | + | // signals collapses to a single emitted value, since any signals still |
| 12 | // queued once the first is observed are drained before querying the | 12 | // queued once the first is observed are drained before querying the | ||
| 13 | - | // size. getSize is called to resolve the size to emit; a getSize error | 13 | + | // geometry. getGeometry is called to resolve the value to emit; an error is |
| 14 | - | // is treated as "no event this round" and the loop continues waiting. | 14 | + | // treated as "no event this round" and the loop continues waiting. |
| 15 | // | 15 | // | ||
| 16 | // The loop exits and closes out when quit is closed. | 16 | // The loop exits and closes out when quit is closed. | ||
| 17 | - | func resizeLoop(sig <-chan os.Signal, out chan<- domain.Size, quit <-chan struct{}, getSize func() (domain.Size, error)) { | 17 | + | func resizeLoop(sig <-chan os.Signal, out chan<- domain.Geometry, quit <-chan struct{}, getGeometry func() (domain.Geometry, error)) { |
| 18 | defer close(out) | 18 | defer close(out) | ||
| 19 | for { | 19 | for { | ||
| 20 | select { | 20 | select { | ||
| @@ -22,12 +22,12 @@ func resizeLoop(sig <-chan os.Signal, out chan<- domain.Size, quit <-chan struct | |||||
| 22 | return | 22 | return | ||
| 23 | case <-sig: | 23 | case <-sig: | ||
| 24 | drainSignals(sig) | 24 | drainSignals(sig) | ||
| 25 | - | sz, err := getSize() | 25 | + | geometry, err := getGeometry() |
| 26 | if err != nil { | 26 | if err != nil { | ||
| 27 | continue | 27 | continue | ||
| 28 | } | 28 | } | ||
| 29 | select { | 29 | select { | ||
| 30 | - | case out <- sz: | 30 | + | case out <- geometry: |
| 31 | case <-quit: | 31 | case <-quit: | ||
| 32 | return | 32 | return | ||
| 33 | } | 33 | } | ||
M internal/adapters/term/resize_test.go +16 -16
| @@ -19,19 +19,19 @@ func TestResizeLoop_CoalescesBurstToOneEmission(t *testing.T) { | |||
| 19 | 19 | sig <- syscall.SIGWINCH | |
| 20 | 20 | } | |
| 21 | 21 | ||
| 22 | - | out := make(chan domain.Size) | |
| 22 | + | out := make(chan domain.Geometry) | |
| 23 | 23 | quit := make(chan struct{}) | |
| 24 | 24 | ||
| 25 | 25 | var calls int | |
| 26 | - | getSize := func() (domain.Size, error) { | |
| 26 | + | getGeometry := func() (domain.Geometry, error) { | |
| 27 | 27 | calls++ | |
| 28 | - | return domain.Size{Cols: 80, Rows: 24}, nil | |
| 28 | + | return domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480}, nil | |
| 29 | 29 | } | |
| 30 | 30 | ||
| 31 | - | go resizeLoop(sig, out, quit, getSize) | |
| 31 | + | go resizeLoop(sig, out, quit, getGeometry) | |
| 32 | 32 | ||
| 33 | 33 | got := <-out | |
| 34 | - | want := domain.Size{Cols: 80, Rows: 24} | |
| 34 | + | want := domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480} | |
| 35 | 35 | if got != want { | |
| 36 | 36 | t.Fatalf("emitted size = %+v, want %+v", got, want) | |
| 37 | 37 | } | |
| @@ -54,28 +54,28 @@ func TestResizeLoop_CoalescesBurstToOneEmission(t *testing.T) { | |||
| 54 | 54 | ||
| 55 | 55 | func TestResizeLoop_GetSizeErrorSkipsEmission(t *testing.T) { | |
| 56 | 56 | sig := make(chan os.Signal) // unbuffered: sends rendezvous with the loop's receive | |
| 57 | - | out := make(chan domain.Size) | |
| 57 | + | out := make(chan domain.Geometry) | |
| 58 | 58 | quit := make(chan struct{}) | |
| 59 | 59 | ||
| 60 | 60 | var calls int | |
| 61 | 61 | firstGetSize := make(chan struct{}) | |
| 62 | - | getSize := func() (domain.Size, error) { | |
| 62 | + | getGeometry := func() (domain.Geometry, error) { | |
| 63 | 63 | calls++ | |
| 64 | 64 | if calls == 1 { | |
| 65 | 65 | close(firstGetSize) | |
| 66 | - | return domain.Size{}, errors.New("boom") | |
| 66 | + | return domain.Geometry{}, errors.New("boom") | |
| 67 | 67 | } | |
| 68 | - | return domain.Size{Cols: 10, Rows: 5}, nil | |
| 68 | + | return domain.Geometry{Size: domain.Size{Cols: 10, Rows: 5}}, nil | |
| 69 | 69 | } | |
| 70 | 70 | ||
| 71 | - | go resizeLoop(sig, out, quit, getSize) | |
| 71 | + | go resizeLoop(sig, out, quit, getGeometry) | |
| 72 | 72 | ||
| 73 | 73 | sig <- syscall.SIGWINCH // errors; no emission | |
| 74 | 74 | <-firstGetSize // wait until the failed signal has been handled | |
| 75 | 75 | sig <- syscall.SIGWINCH // succeeds | |
| 76 | 76 | ||
| 77 | 77 | got := <-out | |
| 78 | - | want := domain.Size{Cols: 10, Rows: 5} | |
| 78 | + | want := domain.Geometry{Size: domain.Size{Cols: 10, Rows: 5}} | |
| 79 | 79 | if got != want { | |
| 80 | 80 | t.Fatalf("emitted size = %+v, want %+v", got, want) | |
| 81 | 81 | } | |
| @@ -91,15 +91,15 @@ func TestResizeLoop_GetSizeErrorSkipsEmission(t *testing.T) { | |||
| 91 | 91 | ||
| 92 | 92 | func TestResizeLoop_QuitClosesOutImmediately(t *testing.T) { | |
| 93 | 93 | sig := make(chan os.Signal) | |
| 94 | - | out := make(chan domain.Size) | |
| 94 | + | out := make(chan domain.Geometry) | |
| 95 | 95 | quit := make(chan struct{}) | |
| 96 | 96 | ||
| 97 | - | getSize := func() (domain.Size, error) { | |
| 98 | - | t.Fatalf("getSize should not be called") | |
| 99 | - | return domain.Size{}, nil | |
| 97 | + | getGeometry := func() (domain.Geometry, error) { | |
| 98 | + | t.Fatalf("getGeometry should not be called") | |
| 99 | + | return domain.Geometry{}, nil | |
| 100 | 100 | } | |
| 101 | 101 | ||
| 102 | - | go resizeLoop(sig, out, quit, getSize) | |
| 102 | + | go resizeLoop(sig, out, quit, getGeometry) | |
| 103 | 103 | close(quit) | |
| 104 | 104 | ||
| 105 | 105 | select { | |
| @@ -19,19 +19,19 @@ func TestResizeLoop_CoalescesBurstToOneEmission(t *testing.T) { | |||||
| 19 | sig <- syscall.SIGWINCH | 19 | sig <- syscall.SIGWINCH | ||
| 20 | } | 20 | } | ||
| 21 | 21 | ||||
| 22 | - | out := make(chan domain.Size) | 22 | + | out := make(chan domain.Geometry) |
| 23 | quit := make(chan struct{}) | 23 | quit := make(chan struct{}) | ||
| 24 | 24 | ||||
| 25 | var calls int | 25 | var calls int | ||
| 26 | - | getSize := func() (domain.Size, error) { | 26 | + | getGeometry := func() (domain.Geometry, error) { |
| 27 | calls++ | 27 | calls++ | ||
| 28 | - | return domain.Size{Cols: 80, Rows: 24}, nil | 28 | + | return domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480}, nil |
| 29 | } | 29 | } | ||
| 30 | 30 | ||||
| 31 | - | go resizeLoop(sig, out, quit, getSize) | 31 | + | go resizeLoop(sig, out, quit, getGeometry) |
| 32 | 32 | ||||
| 33 | got := <-out | 33 | got := <-out | ||
| 34 | - | want := domain.Size{Cols: 80, Rows: 24} | 34 | + | want := domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480} |
| 35 | if got != want { | 35 | if got != want { | ||
| 36 | t.Fatalf("emitted size = %+v, want %+v", got, want) | 36 | t.Fatalf("emitted size = %+v, want %+v", got, want) | ||
| 37 | } | 37 | } | ||
| @@ -54,28 +54,28 @@ func TestResizeLoop_CoalescesBurstToOneEmission(t *testing.T) { | |||||
| 54 | 54 | ||||
| 55 | func TestResizeLoop_GetSizeErrorSkipsEmission(t *testing.T) { | 55 | func TestResizeLoop_GetSizeErrorSkipsEmission(t *testing.T) { | ||
| 56 | sig := make(chan os.Signal) // unbuffered: sends rendezvous with the loop's receive | 56 | sig := make(chan os.Signal) // unbuffered: sends rendezvous with the loop's receive | ||
| 57 | - | out := make(chan domain.Size) | 57 | + | out := make(chan domain.Geometry) |
| 58 | quit := make(chan struct{}) | 58 | quit := make(chan struct{}) | ||
| 59 | 59 | ||||
| 60 | var calls int | 60 | var calls int | ||
| 61 | firstGetSize := make(chan struct{}) | 61 | firstGetSize := make(chan struct{}) | ||
| 62 | - | getSize := func() (domain.Size, error) { | 62 | + | getGeometry := func() (domain.Geometry, error) { |
| 63 | calls++ | 63 | calls++ | ||
| 64 | if calls == 1 { | 64 | if calls == 1 { | ||
| 65 | close(firstGetSize) | 65 | close(firstGetSize) | ||
| 66 | - | return domain.Size{}, errors.New("boom") | 66 | + | return domain.Geometry{}, errors.New("boom") |
| 67 | } | 67 | } | ||
| 68 | - | return domain.Size{Cols: 10, Rows: 5}, nil | 68 | + | return domain.Geometry{Size: domain.Size{Cols: 10, Rows: 5}}, nil |
| 69 | } | 69 | } | ||
| 70 | 70 | ||||
| 71 | - | go resizeLoop(sig, out, quit, getSize) | 71 | + | go resizeLoop(sig, out, quit, getGeometry) |
| 72 | 72 | ||||
| 73 | sig <- syscall.SIGWINCH // errors; no emission | 73 | sig <- syscall.SIGWINCH // errors; no emission | ||
| 74 | <-firstGetSize // wait until the failed signal has been handled | 74 | <-firstGetSize // wait until the failed signal has been handled | ||
| 75 | sig <- syscall.SIGWINCH // succeeds | 75 | sig <- syscall.SIGWINCH // succeeds | ||
| 76 | 76 | ||||
| 77 | got := <-out | 77 | got := <-out | ||
| 78 | - | want := domain.Size{Cols: 10, Rows: 5} | 78 | + | want := domain.Geometry{Size: domain.Size{Cols: 10, Rows: 5}} |
| 79 | if got != want { | 79 | if got != want { | ||
| 80 | t.Fatalf("emitted size = %+v, want %+v", got, want) | 80 | t.Fatalf("emitted size = %+v, want %+v", got, want) | ||
| 81 | } | 81 | } | ||
| @@ -91,15 +91,15 @@ func TestResizeLoop_GetSizeErrorSkipsEmission(t *testing.T) { | |||||
| 91 | 91 | ||||
| 92 | func TestResizeLoop_QuitClosesOutImmediately(t *testing.T) { | 92 | func TestResizeLoop_QuitClosesOutImmediately(t *testing.T) { | ||
| 93 | sig := make(chan os.Signal) | 93 | sig := make(chan os.Signal) | ||
| 94 | - | out := make(chan domain.Size) | 94 | + | out := make(chan domain.Geometry) |
| 95 | quit := make(chan struct{}) | 95 | quit := make(chan struct{}) | ||
| 96 | 96 | ||||
| 97 | - | getSize := func() (domain.Size, error) { | 97 | + | getGeometry := func() (domain.Geometry, error) { |
| 98 | - | t.Fatalf("getSize should not be called") | 98 | + | t.Fatalf("getGeometry should not be called") |
| 99 | - | return domain.Size{}, nil | 99 | + | return domain.Geometry{}, nil |
| 100 | } | 100 | } | ||
| 101 | 101 | ||||
| 102 | - | go resizeLoop(sig, out, quit, getSize) | 102 | + | go resizeLoop(sig, out, quit, getGeometry) |
| 103 | close(quit) | 103 | close(quit) | ||
| 104 | 104 | ||||
| 105 | select { | 105 | select { | ||
M internal/adapters/term/terminal.go +14 -9
| @@ -73,7 +73,7 @@ type Terminal struct { | |||
| 73 | 73 | // (ResizeEvents) and watcher stop (restore) are strictly ordered: | |
| 74 | 74 | // a restore that runs before the first ResizeEvents call must | |
| 75 | 75 | // prevent any watcher (and its signal.Notify) from ever starting. | |
| 76 | - | resizeCh chan domain.Size | |
| 76 | + | resizeCh chan domain.Geometry | |
| 77 | 77 | resizeQuit chan struct{} | |
| 78 | 78 | sigCh chan os.Signal | |
| 79 | 79 | resizeDone bool // set by stopResizeLocked; no watcher may start afterwards | |
| @@ -206,13 +206,18 @@ func (t *Terminal) restoreRawLocked() error { | |||
| 206 | 206 | return err | |
| 207 | 207 | } | |
| 208 | 208 | ||
| 209 | - | // Size returns the current terminal dimensions. | |
| 210 | - | func (t *Terminal) Size() (domain.Size, error) { | |
| 211 | - | cols, rows, err := rawterm.GetSize(t.fd) | |
| 209 | + | // Geometry returns the current terminal cell dimensions and optional pixel | |
| 210 | + | // dimensions reported by the controlling terminal. | |
| 211 | + | func (t *Terminal) Geometry() (domain.Geometry, error) { | |
| 212 | + | ws, err := rawterm.GetWinsize(t.fd) | |
| 212 | 213 | if err != nil { | |
| 213 | - | return domain.Size{}, fmt.Errorf("term: get size: %w", err) | |
| 214 | + | return domain.Geometry{}, fmt.Errorf("term: get size: %w", err) | |
| 214 | 215 | } | |
| 215 | - | return domain.Size{Cols: cols, Rows: rows}, nil | |
| 216 | + | return (domain.Geometry{ | |
| 217 | + | Size: domain.Size{Cols: int(ws.Col), Rows: int(ws.Row)}, | |
| 218 | + | PixelWidth: int(ws.Xpixel), | |
| 219 | + | PixelHeight: int(ws.Ypixel), | |
| 220 | + | }).NormalizePixels(), nil | |
| 216 | 221 | } | |
| 217 | 222 | ||
| 218 | 223 | // ResizeEvents returns a channel of coalesced terminal sizes, one per | |
| @@ -221,14 +226,14 @@ func (t *Terminal) Size() (domain.Size, error) { | |||
| 221 | 226 | // restore (returned by EnterRaw) runs. If restore has already run when | |
| 222 | 227 | // ResizeEvents is first called, no watcher is started and the returned | |
| 223 | 228 | // channel is already closed. | |
| 224 | - | func (t *Terminal) ResizeEvents() <-chan domain.Size { | |
| 229 | + | func (t *Terminal) ResizeEvents() <-chan domain.Geometry { | |
| 225 | 230 | t.mu.Lock() | |
| 226 | 231 | defer t.mu.Unlock() | |
| 227 | 232 | ||
| 228 | 233 | if t.resizeCh != nil { | |
| 229 | 234 | return t.resizeCh | |
| 230 | 235 | } | |
| 231 | - | t.resizeCh = make(chan domain.Size) | |
| 236 | + | t.resizeCh = make(chan domain.Geometry) | |
| 232 | 237 | ||
| 233 | 238 | if t.resizeDone { | |
| 234 | 239 | // restore ran before the first ResizeEvents call: never start a | |
| @@ -244,7 +249,7 @@ func (t *Terminal) ResizeEvents() <-chan domain.Size { | |||
| 244 | 249 | ||
| 245 | 250 | t.resizeWG.Go(func() { | |
| 246 | 251 | defer signal.Stop(t.sigCh) | |
| 247 | - | resizeLoop(t.sigCh, t.resizeCh, t.resizeQuit, t.Size) | |
| 252 | + | resizeLoop(t.sigCh, t.resizeCh, t.resizeQuit, t.Geometry) | |
| 248 | 253 | }) | |
| 249 | 254 | ||
| 250 | 255 | return t.resizeCh | |
| @@ -73,7 +73,7 @@ type Terminal struct { | |||||
| 73 | // (ResizeEvents) and watcher stop (restore) are strictly ordered: | 73 | // (ResizeEvents) and watcher stop (restore) are strictly ordered: | ||
| 74 | // a restore that runs before the first ResizeEvents call must | 74 | // a restore that runs before the first ResizeEvents call must | ||
| 75 | // prevent any watcher (and its signal.Notify) from ever starting. | 75 | // prevent any watcher (and its signal.Notify) from ever starting. | ||
| 76 | - | resizeCh chan domain.Size | 76 | + | resizeCh chan domain.Geometry |
| 77 | resizeQuit chan struct{} | 77 | resizeQuit chan struct{} | ||
| 78 | sigCh chan os.Signal | 78 | sigCh chan os.Signal | ||
| 79 | resizeDone bool // set by stopResizeLocked; no watcher may start afterwards | 79 | resizeDone bool // set by stopResizeLocked; no watcher may start afterwards | ||
| @@ -206,13 +206,18 @@ func (t *Terminal) restoreRawLocked() error { | |||||
| 206 | return err | 206 | return err | ||
| 207 | } | 207 | } | ||
| 208 | 208 | ||||
| 209 | - | // Size returns the current terminal dimensions. | 209 | + | // Geometry returns the current terminal cell dimensions and optional pixel |
| 210 | - | func (t *Terminal) Size() (domain.Size, error) { | 210 | + | // dimensions reported by the controlling terminal. |
| 211 | - | cols, rows, err := rawterm.GetSize(t.fd) | 211 | + | func (t *Terminal) Geometry() (domain.Geometry, error) { |
| 212 | + | ws, err := rawterm.GetWinsize(t.fd) | |||
| 212 | if err != nil { | 213 | if err != nil { | ||
| 213 | - | return domain.Size{}, fmt.Errorf("term: get size: %w", err) | 214 | + | return domain.Geometry{}, fmt.Errorf("term: get size: %w", err) |
| 214 | } | 215 | } | ||
| 215 | - | return domain.Size{Cols: cols, Rows: rows}, nil | 216 | + | return (domain.Geometry{ |
| 217 | + | Size: domain.Size{Cols: int(ws.Col), Rows: int(ws.Row)}, | |||
| 218 | + | PixelWidth: int(ws.Xpixel), | |||
| 219 | + | PixelHeight: int(ws.Ypixel), | |||
| 220 | + | }).NormalizePixels(), nil | |||
| 216 | } | 221 | } | ||
| 217 | 222 | ||||
| 218 | // ResizeEvents returns a channel of coalesced terminal sizes, one per | 223 | // ResizeEvents returns a channel of coalesced terminal sizes, one per | ||
| @@ -221,14 +226,14 @@ func (t *Terminal) Size() (domain.Size, error) { | |||||
| 221 | // restore (returned by EnterRaw) runs. If restore has already run when | 226 | // restore (returned by EnterRaw) runs. If restore has already run when | ||
| 222 | // ResizeEvents is first called, no watcher is started and the returned | 227 | // ResizeEvents is first called, no watcher is started and the returned | ||
| 223 | // channel is already closed. | 228 | // channel is already closed. | ||
| 224 | - | func (t *Terminal) ResizeEvents() <-chan domain.Size { | 229 | + | func (t *Terminal) ResizeEvents() <-chan domain.Geometry { |
| 225 | t.mu.Lock() | 230 | t.mu.Lock() | ||
| 226 | defer t.mu.Unlock() | 231 | defer t.mu.Unlock() | ||
| 227 | 232 | ||||
| 228 | if t.resizeCh != nil { | 233 | if t.resizeCh != nil { | ||
| 229 | return t.resizeCh | 234 | return t.resizeCh | ||
| 230 | } | 235 | } | ||
| 231 | - | t.resizeCh = make(chan domain.Size) | 236 | + | t.resizeCh = make(chan domain.Geometry) |
| 232 | 237 | ||||
| 233 | if t.resizeDone { | 238 | if t.resizeDone { | ||
| 234 | // restore ran before the first ResizeEvents call: never start a | 239 | // restore ran before the first ResizeEvents call: never start a | ||
| @@ -244,7 +249,7 @@ func (t *Terminal) ResizeEvents() <-chan domain.Size { | |||||
| 244 | 249 | ||||
| 245 | t.resizeWG.Go(func() { | 250 | t.resizeWG.Go(func() { | ||
| 246 | defer signal.Stop(t.sigCh) | 251 | defer signal.Stop(t.sigCh) | ||
| 247 | - | resizeLoop(t.sigCh, t.resizeCh, t.resizeQuit, t.Size) | 252 | + | resizeLoop(t.sigCh, t.resizeCh, t.resizeQuit, t.Geometry) |
| 248 | }) | 253 | }) | ||
| 249 | 254 | ||||
| 250 | return t.resizeCh | 255 | return t.resizeCh | ||
M internal/adapters/term/terminal_test.go +3 -3
| @@ -433,7 +433,7 @@ func TestTerminal_ResizeEvents_ConcurrentWithRestore(t *testing.T) { | |||
| 433 | 433 | } | |
| 434 | 434 | ||
| 435 | 435 | start := make(chan struct{}) | |
| 436 | - | chch := make(chan (<-chan domain.Size), 1) | |
| 436 | + | chch := make(chan (<-chan domain.Geometry), 1) | |
| 437 | 437 | go func() { | |
| 438 | 438 | <-start | |
| 439 | 439 | chch <- tm.ResizeEvents() | |
| @@ -486,7 +486,7 @@ func TestTerminal_Size_NonTTY_ReturnsError(t *testing.T) { | |||
| 486 | 486 | ||
| 487 | 487 | tm := NewWithFiles(inR, outW) | |
| 488 | 488 | ||
| 489 | - | if _, err := tm.Size(); err == nil { | |
| 490 | - | t.Fatalf("expected Size() to error on a non-tty fd") | |
| 489 | + | if _, err := tm.Geometry(); err == nil { | |
| 490 | + | t.Fatalf("expected Geometry() to error on a non-tty fd") | |
| 491 | 491 | } | |
| 492 | 492 | } | |
| @@ -433,7 +433,7 @@ func TestTerminal_ResizeEvents_ConcurrentWithRestore(t *testing.T) { | |||||
| 433 | } | 433 | } | ||
| 434 | 434 | ||||
| 435 | start := make(chan struct{}) | 435 | start := make(chan struct{}) | ||
| 436 | - | chch := make(chan (<-chan domain.Size), 1) | 436 | + | chch := make(chan (<-chan domain.Geometry), 1) |
| 437 | go func() { | 437 | go func() { | ||
| 438 | <-start | 438 | <-start | ||
| 439 | chch <- tm.ResizeEvents() | 439 | chch <- tm.ResizeEvents() | ||
| @@ -486,7 +486,7 @@ func TestTerminal_Size_NonTTY_ReturnsError(t *testing.T) { | |||||
| 486 | 486 | ||||
| 487 | tm := NewWithFiles(inR, outW) | 487 | tm := NewWithFiles(inR, outW) | ||
| 488 | 488 | ||||
| 489 | - | if _, err := tm.Size(); err == nil { | 489 | + | if _, err := tm.Geometry(); err == nil { |
| 490 | - | t.Fatalf("expected Size() to error on a non-tty fd") | 490 | + | t.Fatalf("expected Geometry() to error on a non-tty fd") |
| 491 | } | 491 | } | ||
| 492 | } | 492 | } | ||
M internal/app/remote_final_acceptance_test.go +6 -6
| @@ -104,13 +104,13 @@ type acceptanceRemoteTerminal struct { | |||
| 104 | 104 | func (t *acceptanceRemoteTerminal) EnterRaw() (func() error, error) { | |
| 105 | 105 | return func() error { return nil }, nil | |
| 106 | 106 | } | |
| 107 | - | func (*acceptanceRemoteTerminal) Size() (domain.Size, error) { | |
| 108 | - | return domain.Size{Cols: 80, Rows: 24}, nil | |
| 107 | + | func (*acceptanceRemoteTerminal) Geometry() (domain.Geometry, error) { | |
| 108 | + | return domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil | |
| 109 | 109 | } | |
| 110 | - | func (*acceptanceRemoteTerminal) ResizeEvents() <-chan domain.Size { return nil } | |
| 111 | - | func (t *acceptanceRemoteTerminal) In() io.Reader { return t.in } | |
| 112 | - | func (t *acceptanceRemoteTerminal) Out() io.Writer { return &t.out } | |
| 113 | - | func (*acceptanceRemoteTerminal) Flush() error { return nil } | |
| 110 | + | func (*acceptanceRemoteTerminal) ResizeEvents() <-chan domain.Geometry { return nil } | |
| 111 | + | func (t *acceptanceRemoteTerminal) In() io.Reader { return t.in } | |
| 112 | + | func (t *acceptanceRemoteTerminal) Out() io.Writer { return &t.out } | |
| 113 | + | func (*acceptanceRemoteTerminal) Flush() error { return nil } | |
| 114 | 114 | ||
| 115 | 115 | func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) { | |
| 116 | 116 | outputs := make(chan string, 4) | |
| @@ -104,13 +104,13 @@ type acceptanceRemoteTerminal struct { | |||||
| 104 | func (t *acceptanceRemoteTerminal) EnterRaw() (func() error, error) { | 104 | func (t *acceptanceRemoteTerminal) EnterRaw() (func() error, error) { | ||
| 105 | return func() error { return nil }, nil | 105 | return func() error { return nil }, nil | ||
| 106 | } | 106 | } | ||
| 107 | - | func (*acceptanceRemoteTerminal) Size() (domain.Size, error) { | 107 | + | func (*acceptanceRemoteTerminal) Geometry() (domain.Geometry, error) { |
| 108 | - | return domain.Size{Cols: 80, Rows: 24}, nil | 108 | + | return domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil |
| 109 | } | 109 | } | ||
| 110 | - | func (*acceptanceRemoteTerminal) ResizeEvents() <-chan domain.Size { return nil } | 110 | + | func (*acceptanceRemoteTerminal) ResizeEvents() <-chan domain.Geometry { return nil } |
| 111 | - | func (t *acceptanceRemoteTerminal) In() io.Reader { return t.in } | 111 | + | func (t *acceptanceRemoteTerminal) In() io.Reader { return t.in } |
| 112 | - | func (t *acceptanceRemoteTerminal) Out() io.Writer { return &t.out } | 112 | + | func (t *acceptanceRemoteTerminal) Out() io.Writer { return &t.out } |
| 113 | - | func (*acceptanceRemoteTerminal) Flush() error { return nil } | 113 | + | func (*acceptanceRemoteTerminal) Flush() error { return nil } |
| 114 | 114 | ||||
| 115 | func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) { | 115 | func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) { | ||
| 116 | outputs := make(chan string, 4) | 116 | outputs := make(chan string, 4) | ||
M internal/domain/types.go +29 -0
| @@ -84,6 +84,35 @@ func (sz Size) Valid() bool { | |||
| 84 | 84 | return sz.Cols > 0 && sz.Rows > 0 | |
| 85 | 85 | } | |
| 86 | 86 | ||
| 87 | + | // Geometry combines a terminal's required cell dimensions with optional pixel | |
| 88 | + | // dimensions reported by the controlling terminal. Zero pixel dimensions mean | |
| 89 | + | // the terminal did not report them; they must not be inferred from cells. | |
| 90 | + | type Geometry struct { | |
| 91 | + | Size | |
| 92 | + | PixelWidth, PixelHeight int | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | // Valid reports whether the cell dimensions can describe a terminal screen. | |
| 96 | + | func (g Geometry) Valid() bool { | |
| 97 | + | return g.Size.Valid() | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | // PixelsKnown reports whether both pixel dimensions are available. | |
| 101 | + | func (g Geometry) PixelsKnown() bool { | |
| 102 | + | return g.PixelWidth > 0 && g.PixelHeight > 0 | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // NormalizePixels clears partial or negative pixel dimensions. Pixel geometry | |
| 106 | + | // is optional as a pair; retaining only one axis would give downstream PTYs and | |
| 107 | + | // terminal queries a geometry the controlling terminal never reported. | |
| 108 | + | func (g Geometry) NormalizePixels() Geometry { | |
| 109 | + | if !g.PixelsKnown() { | |
| 110 | + | g.PixelWidth = 0 | |
| 111 | + | g.PixelHeight = 0 | |
| 112 | + | } | |
| 113 | + | return g | |
| 114 | + | } | |
| 115 | + | ||
| 87 | 116 | // Rect is an axis-aligned rectangle in cell coordinates. | |
| 88 | 117 | type Rect struct { | |
| 89 | 118 | X, Y, Width, Height int | |
| @@ -84,6 +84,35 @@ func (sz Size) Valid() bool { | |||||
| 84 | return sz.Cols > 0 && sz.Rows > 0 | 84 | return sz.Cols > 0 && sz.Rows > 0 | ||
| 85 | } | 85 | } | ||
| 86 | 86 | ||||
| 87 | + | // Geometry combines a terminal's required cell dimensions with optional pixel | |||
| 88 | + | // dimensions reported by the controlling terminal. Zero pixel dimensions mean | |||
| 89 | + | // the terminal did not report them; they must not be inferred from cells. | |||
| 90 | + | type Geometry struct { | |||
| 91 | + | Size | |||
| 92 | + | PixelWidth, PixelHeight int | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | // Valid reports whether the cell dimensions can describe a terminal screen. | |||
| 96 | + | func (g Geometry) Valid() bool { | |||
| 97 | + | return g.Size.Valid() | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | // PixelsKnown reports whether both pixel dimensions are available. | |||
| 101 | + | func (g Geometry) PixelsKnown() bool { | |||
| 102 | + | return g.PixelWidth > 0 && g.PixelHeight > 0 | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // NormalizePixels clears partial or negative pixel dimensions. Pixel geometry | |||
| 106 | + | // is optional as a pair; retaining only one axis would give downstream PTYs and | |||
| 107 | + | // terminal queries a geometry the controlling terminal never reported. | |||
| 108 | + | func (g Geometry) NormalizePixels() Geometry { | |||
| 109 | + | if !g.PixelsKnown() { | |||
| 110 | + | g.PixelWidth = 0 | |||
| 111 | + | g.PixelHeight = 0 | |||
| 112 | + | } | |||
| 113 | + | return g | |||
| 114 | + | } | |||
| 115 | + | ||||
| 87 | // Rect is an axis-aligned rectangle in cell coordinates. | 116 | // Rect is an axis-aligned rectangle in cell coordinates. | ||
| 88 | type Rect struct { | 117 | type Rect struct { | ||
| 89 | X, Y, Width, Height int | 118 | X, Y, Width, Height int | ||
M internal/ports/frame.go +1 -1
| @@ -12,7 +12,7 @@ import ( | |||
| 12 | 12 | const HandshakeTimeout = 15 * time.Second | |
| 13 | 13 | ||
| 14 | 14 | // ProtocolVersion is the current vev IPC wire protocol version. | |
| 15 | - | const ProtocolVersion uint16 = 34 | |
| 15 | + | const ProtocolVersion uint16 = 35 | |
| 16 | 16 | ||
| 17 | 17 | // MaxFrameLen is the largest permitted frame length, including the type byte | |
| 18 | 18 | // and excluding the four-byte length prefix. | |
| @@ -12,7 +12,7 @@ import ( | |||||
| 12 | const HandshakeTimeout = 15 * time.Second | 12 | const HandshakeTimeout = 15 * time.Second | ||
| 13 | 13 | ||||
| 14 | // ProtocolVersion is the current vev IPC wire protocol version. | 14 | // ProtocolVersion is the current vev IPC wire protocol version. | ||
| 15 | - | const ProtocolVersion uint16 = 34 | 15 | + | const ProtocolVersion uint16 = 35 |
| 16 | 16 | ||||
| 17 | // MaxFrameLen is the largest permitted frame length, including the type byte | 17 | // MaxFrameLen is the largest permitted frame length, including the type byte | ||
| 18 | // and excluding the four-byte length prefix. | 18 | // and excluding the four-byte length prefix. | ||
M internal/ports/frame_test.go +2 -2
| @@ -3,8 +3,8 @@ package ports | |||
| 3 | 3 | import "testing" | |
| 4 | 4 | ||
| 5 | 5 | func TestProtocolVersion(t *testing.T) { | |
| 6 | - | if ProtocolVersion != 34 { | |
| 7 | - | t.Fatalf("ProtocolVersion = %d, want 34", ProtocolVersion) | |
| 6 | + | if ProtocolVersion != 35 { | |
| 7 | + | t.Fatalf("ProtocolVersion = %d, want 35", ProtocolVersion) | |
| 8 | 8 | } | |
| 9 | 9 | } | |
| 10 | 10 | ||
| @@ -3,8 +3,8 @@ package ports | |||||
| 3 | import "testing" | 3 | import "testing" | ||
| 4 | 4 | ||||
| 5 | func TestProtocolVersion(t *testing.T) { | 5 | func TestProtocolVersion(t *testing.T) { | ||
| 6 | - | if ProtocolVersion != 34 { | 6 | + | if ProtocolVersion != 35 { |
| 7 | - | t.Fatalf("ProtocolVersion = %d, want 34", ProtocolVersion) | 7 | + | t.Fatalf("ProtocolVersion = %d, want 35", ProtocolVersion) |
| 8 | } | 8 | } | ||
| 9 | } | 9 | } | ||
| 10 | 10 | ||||
M internal/ports/mocks/pty_mock.go +26 -26
| @@ -241,16 +241,16 @@ func (_c *MockPTY_Read_Call) RunAndReturn(run func(p []byte) (int, error)) *Mock | |||
| 241 | 241 | } | |
| 242 | 242 | ||
| 243 | 243 | // Resize provides a mock function for the type MockPTY | |
| 244 | - | func (_mock *MockPTY) Resize(sz domain.Size) error { | |
| 245 | - | ret := _mock.Called(sz) | |
| 244 | + | func (_mock *MockPTY) Resize(geometry domain.Geometry) error { | |
| 245 | + | ret := _mock.Called(geometry) | |
| 246 | 246 | ||
| 247 | 247 | if len(ret) == 0 { | |
| 248 | 248 | panic("no return value specified for Resize") | |
| 249 | 249 | } | |
| 250 | 250 | ||
| 251 | 251 | var r0 error | |
| 252 | - | if returnFunc, ok := ret.Get(0).(func(domain.Size) error); ok { | |
| 253 | - | r0 = returnFunc(sz) | |
| 252 | + | if returnFunc, ok := ret.Get(0).(func(domain.Geometry) error); ok { | |
| 253 | + | r0 = returnFunc(geometry) | |
| 254 | 254 | } else { | |
| 255 | 255 | r0 = ret.Error(0) | |
| 256 | 256 | } | |
| @@ -263,16 +263,16 @@ type MockPTY_Resize_Call struct { | |||
| 263 | 263 | } | |
| 264 | 264 | ||
| 265 | 265 | // Resize is a helper method to define mock.On call | |
| 266 | - | // - sz domain.Size | |
| 267 | - | func (_e *MockPTY_Expecter) Resize(sz any) *MockPTY_Resize_Call { | |
| 268 | - | return &MockPTY_Resize_Call{Call: _e.mock.On("Resize", sz)} | |
| 266 | + | // - geometry domain.Geometry | |
| 267 | + | func (_e *MockPTY_Expecter) Resize(geometry any) *MockPTY_Resize_Call { | |
| 268 | + | return &MockPTY_Resize_Call{Call: _e.mock.On("Resize", geometry)} | |
| 269 | 269 | } | |
| 270 | 270 | ||
| 271 | - | func (_c *MockPTY_Resize_Call) Run(run func(sz domain.Size)) *MockPTY_Resize_Call { | |
| 271 | + | func (_c *MockPTY_Resize_Call) Run(run func(geometry domain.Geometry)) *MockPTY_Resize_Call { | |
| 272 | 272 | _c.Call.Run(func(args mock.Arguments) { | |
| 273 | - | var arg0 domain.Size | |
| 273 | + | var arg0 domain.Geometry | |
| 274 | 274 | if args[0] != nil { | |
| 275 | - | arg0 = args[0].(domain.Size) | |
| 275 | + | arg0 = args[0].(domain.Geometry) | |
| 276 | 276 | } | |
| 277 | 277 | run( | |
| 278 | 278 | arg0, | |
| @@ -286,7 +286,7 @@ func (_c *MockPTY_Resize_Call) Return(err error) *MockPTY_Resize_Call { | |||
| 286 | 286 | return _c | |
| 287 | 287 | } | |
| 288 | 288 | ||
| 289 | - | func (_c *MockPTY_Resize_Call) RunAndReturn(run func(sz domain.Size) error) *MockPTY_Resize_Call { | |
| 289 | + | func (_c *MockPTY_Resize_Call) RunAndReturn(run func(geometry domain.Geometry) error) *MockPTY_Resize_Call { | |
| 290 | 290 | _c.Call.Return(run) | |
| 291 | 291 | return _c | |
| 292 | 292 | } | |
| @@ -379,8 +379,8 @@ func (_m *MockPTYFactory) EXPECT() *MockPTYFactory_Expecter { | |||
| 379 | 379 | } | |
| 380 | 380 | ||
| 381 | 381 | // Open provides a mock function for the type MockPTYFactory | |
| 382 | - | func (_mock *MockPTYFactory) Open(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { | |
| 383 | - | ret := _mock.Called(ctx, cmd, args, env, dir, sz) | |
| 382 | + | func (_mock *MockPTYFactory) Open(ctx context.Context, cmd string, args []string, env []string, dir string, geometry domain.Geometry) (ports.PTY, error) { | |
| 383 | + | ret := _mock.Called(ctx, cmd, args, env, dir, geometry) | |
| 384 | 384 | ||
| 385 | 385 | if len(ret) == 0 { | |
| 386 | 386 | panic("no return value specified for Open") | |
| @@ -388,18 +388,18 @@ func (_mock *MockPTYFactory) Open(ctx context.Context, cmd string, args []string | |||
| 388 | 388 | ||
| 389 | 389 | var r0 ports.PTY | |
| 390 | 390 | var r1 error | |
| 391 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, []string, string, domain.Size) (ports.PTY, error)); ok { | |
| 392 | - | return returnFunc(ctx, cmd, args, env, dir, sz) | |
| 391 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, []string, string, domain.Geometry) (ports.PTY, error)); ok { | |
| 392 | + | return returnFunc(ctx, cmd, args, env, dir, geometry) | |
| 393 | 393 | } | |
| 394 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, []string, string, domain.Size) ports.PTY); ok { | |
| 395 | - | r0 = returnFunc(ctx, cmd, args, env, dir, sz) | |
| 394 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, []string, string, domain.Geometry) ports.PTY); ok { | |
| 395 | + | r0 = returnFunc(ctx, cmd, args, env, dir, geometry) | |
| 396 | 396 | } else { | |
| 397 | 397 | if ret.Get(0) != nil { | |
| 398 | 398 | r0 = ret.Get(0).(ports.PTY) | |
| 399 | 399 | } | |
| 400 | 400 | } | |
| 401 | - | if returnFunc, ok := ret.Get(1).(func(context.Context, string, []string, []string, string, domain.Size) error); ok { | |
| 402 | - | r1 = returnFunc(ctx, cmd, args, env, dir, sz) | |
| 401 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string, []string, []string, string, domain.Geometry) error); ok { | |
| 402 | + | r1 = returnFunc(ctx, cmd, args, env, dir, geometry) | |
| 403 | 403 | } else { | |
| 404 | 404 | r1 = ret.Error(1) | |
| 405 | 405 | } | |
| @@ -417,12 +417,12 @@ type MockPTYFactory_Open_Call struct { | |||
| 417 | 417 | // - args []string | |
| 418 | 418 | // - env []string | |
| 419 | 419 | // - dir string | |
| 420 | - | // - sz domain.Size | |
| 421 | - | func (_e *MockPTYFactory_Expecter) Open(ctx any, cmd any, args any, env any, dir any, sz any) *MockPTYFactory_Open_Call { | |
| 422 | - | return &MockPTYFactory_Open_Call{Call: _e.mock.On("Open", ctx, cmd, args, env, dir, sz)} | |
| 420 | + | // - geometry domain.Geometry | |
| 421 | + | func (_e *MockPTYFactory_Expecter) Open(ctx any, cmd any, args any, env any, dir any, geometry any) *MockPTYFactory_Open_Call { | |
| 422 | + | return &MockPTYFactory_Open_Call{Call: _e.mock.On("Open", ctx, cmd, args, env, dir, geometry)} | |
| 423 | 423 | } | |
| 424 | 424 | ||
| 425 | - | func (_c *MockPTYFactory_Open_Call) Run(run func(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size)) *MockPTYFactory_Open_Call { | |
| 425 | + | func (_c *MockPTYFactory_Open_Call) Run(run func(ctx context.Context, cmd string, args []string, env []string, dir string, geometry domain.Geometry)) *MockPTYFactory_Open_Call { | |
| 426 | 426 | _c.Call.Run(func(args mock.Arguments) { | |
| 427 | 427 | var arg0 context.Context | |
| 428 | 428 | if args[0] != nil { | |
| @@ -444,9 +444,9 @@ func (_c *MockPTYFactory_Open_Call) Run(run func(ctx context.Context, cmd string | |||
| 444 | 444 | if args[4] != nil { | |
| 445 | 445 | arg4 = args[4].(string) | |
| 446 | 446 | } | |
| 447 | - | var arg5 domain.Size | |
| 447 | + | var arg5 domain.Geometry | |
| 448 | 448 | if args[5] != nil { | |
| 449 | - | arg5 = args[5].(domain.Size) | |
| 449 | + | arg5 = args[5].(domain.Geometry) | |
| 450 | 450 | } | |
| 451 | 451 | run( | |
| 452 | 452 | arg0, | |
| @@ -465,7 +465,7 @@ func (_c *MockPTYFactory_Open_Call) Return(pTY ports.PTY, err error) *MockPTYFac | |||
| 465 | 465 | return _c | |
| 466 | 466 | } | |
| 467 | 467 | ||
| 468 | - | func (_c *MockPTYFactory_Open_Call) RunAndReturn(run func(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error)) *MockPTYFactory_Open_Call { | |
| 468 | + | func (_c *MockPTYFactory_Open_Call) RunAndReturn(run func(ctx context.Context, cmd string, args []string, env []string, dir string, geometry domain.Geometry) (ports.PTY, error)) *MockPTYFactory_Open_Call { | |
| 469 | 469 | _c.Call.Return(run) | |
| 470 | 470 | return _c | |
| 471 | 471 | } | |
| @@ -241,16 +241,16 @@ func (_c *MockPTY_Read_Call) RunAndReturn(run func(p []byte) (int, error)) *Mock | |||||
| 241 | } | 241 | } | ||
| 242 | 242 | ||||
| 243 | // Resize provides a mock function for the type MockPTY | 243 | // Resize provides a mock function for the type MockPTY | ||
| 244 | - | func (_mock *MockPTY) Resize(sz domain.Size) error { | 244 | + | func (_mock *MockPTY) Resize(geometry domain.Geometry) error { |
| 245 | - | ret := _mock.Called(sz) | 245 | + | ret := _mock.Called(geometry) |
| 246 | 246 | ||||
| 247 | if len(ret) == 0 { | 247 | if len(ret) == 0 { | ||
| 248 | panic("no return value specified for Resize") | 248 | panic("no return value specified for Resize") | ||
| 249 | } | 249 | } | ||
| 250 | 250 | ||||
| 251 | var r0 error | 251 | var r0 error | ||
| 252 | - | if returnFunc, ok := ret.Get(0).(func(domain.Size) error); ok { | 252 | + | if returnFunc, ok := ret.Get(0).(func(domain.Geometry) error); ok { |
| 253 | - | r0 = returnFunc(sz) | 253 | + | r0 = returnFunc(geometry) |
| 254 | } else { | 254 | } else { | ||
| 255 | r0 = ret.Error(0) | 255 | r0 = ret.Error(0) | ||
| 256 | } | 256 | } | ||
| @@ -263,16 +263,16 @@ type MockPTY_Resize_Call struct { | |||||
| 263 | } | 263 | } | ||
| 264 | 264 | ||||
| 265 | // Resize is a helper method to define mock.On call | 265 | // Resize is a helper method to define mock.On call | ||
| 266 | - | // - sz domain.Size | 266 | + | // - geometry domain.Geometry |
| 267 | - | func (_e *MockPTY_Expecter) Resize(sz any) *MockPTY_Resize_Call { | 267 | + | func (_e *MockPTY_Expecter) Resize(geometry any) *MockPTY_Resize_Call { |
| 268 | - | return &MockPTY_Resize_Call{Call: _e.mock.On("Resize", sz)} | 268 | + | return &MockPTY_Resize_Call{Call: _e.mock.On("Resize", geometry)} |
| 269 | } | 269 | } | ||
| 270 | 270 | ||||
| 271 | - | func (_c *MockPTY_Resize_Call) Run(run func(sz domain.Size)) *MockPTY_Resize_Call { | 271 | + | func (_c *MockPTY_Resize_Call) Run(run func(geometry domain.Geometry)) *MockPTY_Resize_Call { |
| 272 | _c.Call.Run(func(args mock.Arguments) { | 272 | _c.Call.Run(func(args mock.Arguments) { | ||
| 273 | - | var arg0 domain.Size | 273 | + | var arg0 domain.Geometry |
| 274 | if args[0] != nil { | 274 | if args[0] != nil { | ||
| 275 | - | arg0 = args[0].(domain.Size) | 275 | + | arg0 = args[0].(domain.Geometry) |
| 276 | } | 276 | } | ||
| 277 | run( | 277 | run( | ||
| 278 | arg0, | 278 | arg0, | ||
| @@ -286,7 +286,7 @@ func (_c *MockPTY_Resize_Call) Return(err error) *MockPTY_Resize_Call { | |||||
| 286 | return _c | 286 | return _c | ||
| 287 | } | 287 | } | ||
| 288 | 288 | ||||
| 289 | - | func (_c *MockPTY_Resize_Call) RunAndReturn(run func(sz domain.Size) error) *MockPTY_Resize_Call { | 289 | + | func (_c *MockPTY_Resize_Call) RunAndReturn(run func(geometry domain.Geometry) error) *MockPTY_Resize_Call { |
| 290 | _c.Call.Return(run) | 290 | _c.Call.Return(run) | ||
| 291 | return _c | 291 | return _c | ||
| 292 | } | 292 | } | ||
| @@ -379,8 +379,8 @@ func (_m *MockPTYFactory) EXPECT() *MockPTYFactory_Expecter { | |||||
| 379 | } | 379 | } | ||
| 380 | 380 | ||||
| 381 | // Open provides a mock function for the type MockPTYFactory | 381 | // Open provides a mock function for the type MockPTYFactory | ||
| 382 | - | func (_mock *MockPTYFactory) Open(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { | 382 | + | func (_mock *MockPTYFactory) Open(ctx context.Context, cmd string, args []string, env []string, dir string, geometry domain.Geometry) (ports.PTY, error) { |
| 383 | - | ret := _mock.Called(ctx, cmd, args, env, dir, sz) | 383 | + | ret := _mock.Called(ctx, cmd, args, env, dir, geometry) |
| 384 | 384 | ||||
| 385 | if len(ret) == 0 { | 385 | if len(ret) == 0 { | ||
| 386 | panic("no return value specified for Open") | 386 | panic("no return value specified for Open") | ||
| @@ -388,18 +388,18 @@ func (_mock *MockPTYFactory) Open(ctx context.Context, cmd string, args []string | |||||
| 388 | 388 | ||||
| 389 | var r0 ports.PTY | 389 | var r0 ports.PTY | ||
| 390 | var r1 error | 390 | var r1 error | ||
| 391 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, []string, string, domain.Size) (ports.PTY, error)); ok { | 391 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, []string, string, domain.Geometry) (ports.PTY, error)); ok { |
| 392 | - | return returnFunc(ctx, cmd, args, env, dir, sz) | 392 | + | return returnFunc(ctx, cmd, args, env, dir, geometry) |
| 393 | } | 393 | } | ||
| 394 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, []string, string, domain.Size) ports.PTY); ok { | 394 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, []string, string, domain.Geometry) ports.PTY); ok { |
| 395 | - | r0 = returnFunc(ctx, cmd, args, env, dir, sz) | 395 | + | r0 = returnFunc(ctx, cmd, args, env, dir, geometry) |
| 396 | } else { | 396 | } else { | ||
| 397 | if ret.Get(0) != nil { | 397 | if ret.Get(0) != nil { | ||
| 398 | r0 = ret.Get(0).(ports.PTY) | 398 | r0 = ret.Get(0).(ports.PTY) | ||
| 399 | } | 399 | } | ||
| 400 | } | 400 | } | ||
| 401 | - | if returnFunc, ok := ret.Get(1).(func(context.Context, string, []string, []string, string, domain.Size) error); ok { | 401 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string, []string, []string, string, domain.Geometry) error); ok { |
| 402 | - | r1 = returnFunc(ctx, cmd, args, env, dir, sz) | 402 | + | r1 = returnFunc(ctx, cmd, args, env, dir, geometry) |
| 403 | } else { | 403 | } else { | ||
| 404 | r1 = ret.Error(1) | 404 | r1 = ret.Error(1) | ||
| 405 | } | 405 | } | ||
| @@ -417,12 +417,12 @@ type MockPTYFactory_Open_Call struct { | |||||
| 417 | // - args []string | 417 | // - args []string | ||
| 418 | // - env []string | 418 | // - env []string | ||
| 419 | // - dir string | 419 | // - dir string | ||
| 420 | - | // - sz domain.Size | 420 | + | // - geometry domain.Geometry |
| 421 | - | func (_e *MockPTYFactory_Expecter) Open(ctx any, cmd any, args any, env any, dir any, sz any) *MockPTYFactory_Open_Call { | 421 | + | func (_e *MockPTYFactory_Expecter) Open(ctx any, cmd any, args any, env any, dir any, geometry any) *MockPTYFactory_Open_Call { |
| 422 | - | return &MockPTYFactory_Open_Call{Call: _e.mock.On("Open", ctx, cmd, args, env, dir, sz)} | 422 | + | return &MockPTYFactory_Open_Call{Call: _e.mock.On("Open", ctx, cmd, args, env, dir, geometry)} |
| 423 | } | 423 | } | ||
| 424 | 424 | ||||
| 425 | - | func (_c *MockPTYFactory_Open_Call) Run(run func(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size)) *MockPTYFactory_Open_Call { | 425 | + | func (_c *MockPTYFactory_Open_Call) Run(run func(ctx context.Context, cmd string, args []string, env []string, dir string, geometry domain.Geometry)) *MockPTYFactory_Open_Call { |
| 426 | _c.Call.Run(func(args mock.Arguments) { | 426 | _c.Call.Run(func(args mock.Arguments) { | ||
| 427 | var arg0 context.Context | 427 | var arg0 context.Context | ||
| 428 | if args[0] != nil { | 428 | if args[0] != nil { | ||
| @@ -444,9 +444,9 @@ func (_c *MockPTYFactory_Open_Call) Run(run func(ctx context.Context, cmd string | |||||
| 444 | if args[4] != nil { | 444 | if args[4] != nil { | ||
| 445 | arg4 = args[4].(string) | 445 | arg4 = args[4].(string) | ||
| 446 | } | 446 | } | ||
| 447 | - | var arg5 domain.Size | 447 | + | var arg5 domain.Geometry |
| 448 | if args[5] != nil { | 448 | if args[5] != nil { | ||
| 449 | - | arg5 = args[5].(domain.Size) | 449 | + | arg5 = args[5].(domain.Geometry) |
| 450 | } | 450 | } | ||
| 451 | run( | 451 | run( | ||
| 452 | arg0, | 452 | arg0, | ||
| @@ -465,7 +465,7 @@ func (_c *MockPTYFactory_Open_Call) Return(pTY ports.PTY, err error) *MockPTYFac | |||||
| 465 | return _c | 465 | return _c | ||
| 466 | } | 466 | } | ||
| 467 | 467 | ||||
| 468 | - | func (_c *MockPTYFactory_Open_Call) RunAndReturn(run func(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error)) *MockPTYFactory_Open_Call { | 468 | + | func (_c *MockPTYFactory_Open_Call) RunAndReturn(run func(ctx context.Context, cmd string, args []string, env []string, dir string, geometry domain.Geometry) (ports.PTY, error)) *MockPTYFactory_Open_Call { |
| 469 | _c.Call.Return(run) | 469 | _c.Call.Return(run) | ||
| 470 | return _c | 470 | return _c | ||
| 471 | } | 471 | } | ||
M internal/ports/mocks/terminal_mock.go +60 -60
| @@ -137,6 +137,59 @@ func (_c *MockTerminal_Flush_Call) RunAndReturn(run func() error) *MockTerminal_ | |||
| 137 | 137 | return _c | |
| 138 | 138 | } | |
| 139 | 139 | ||
| 140 | + | // Geometry provides a mock function for the type MockTerminal | |
| 141 | + | func (_mock *MockTerminal) Geometry() (domain.Geometry, error) { | |
| 142 | + | ret := _mock.Called() | |
| 143 | + | ||
| 144 | + | if len(ret) == 0 { | |
| 145 | + | panic("no return value specified for Geometry") | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | var r0 domain.Geometry | |
| 149 | + | var r1 error | |
| 150 | + | if returnFunc, ok := ret.Get(0).(func() (domain.Geometry, error)); ok { | |
| 151 | + | return returnFunc() | |
| 152 | + | } | |
| 153 | + | if returnFunc, ok := ret.Get(0).(func() domain.Geometry); ok { | |
| 154 | + | r0 = returnFunc() | |
| 155 | + | } else { | |
| 156 | + | r0 = ret.Get(0).(domain.Geometry) | |
| 157 | + | } | |
| 158 | + | if returnFunc, ok := ret.Get(1).(func() error); ok { | |
| 159 | + | r1 = returnFunc() | |
| 160 | + | } else { | |
| 161 | + | r1 = ret.Error(1) | |
| 162 | + | } | |
| 163 | + | return r0, r1 | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | // MockTerminal_Geometry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Geometry' | |
| 167 | + | type MockTerminal_Geometry_Call struct { | |
| 168 | + | *mock.Call | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | // Geometry is a helper method to define mock.On call | |
| 172 | + | func (_e *MockTerminal_Expecter) Geometry() *MockTerminal_Geometry_Call { | |
| 173 | + | return &MockTerminal_Geometry_Call{Call: _e.mock.On("Geometry")} | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | func (_c *MockTerminal_Geometry_Call) Run(run func()) *MockTerminal_Geometry_Call { | |
| 177 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 178 | + | run() | |
| 179 | + | }) | |
| 180 | + | return _c | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | func (_c *MockTerminal_Geometry_Call) Return(geometry domain.Geometry, err error) *MockTerminal_Geometry_Call { | |
| 184 | + | _c.Call.Return(geometry, err) | |
| 185 | + | return _c | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | func (_c *MockTerminal_Geometry_Call) RunAndReturn(run func() (domain.Geometry, error)) *MockTerminal_Geometry_Call { | |
| 189 | + | _c.Call.Return(run) | |
| 190 | + | return _c | |
| 191 | + | } | |
| 192 | + | ||
| 140 | 193 | // In provides a mock function for the type MockTerminal | |
| 141 | 194 | func (_mock *MockTerminal) In() io.Reader { | |
| 142 | 195 | ret := _mock.Called() | |
| @@ -230,19 +283,19 @@ func (_c *MockTerminal_Out_Call) RunAndReturn(run func() io.Writer) *MockTermina | |||
| 230 | 283 | } | |
| 231 | 284 | ||
| 232 | 285 | // ResizeEvents provides a mock function for the type MockTerminal | |
| 233 | - | func (_mock *MockTerminal) ResizeEvents() <-chan domain.Size { | |
| 286 | + | func (_mock *MockTerminal) ResizeEvents() <-chan domain.Geometry { | |
| 234 | 287 | ret := _mock.Called() | |
| 235 | 288 | ||
| 236 | 289 | if len(ret) == 0 { | |
| 237 | 290 | panic("no return value specified for ResizeEvents") | |
| 238 | 291 | } | |
| 239 | 292 | ||
| 240 | - | var r0 <-chan domain.Size | |
| 241 | - | if returnFunc, ok := ret.Get(0).(func() <-chan domain.Size); ok { | |
| 293 | + | var r0 <-chan domain.Geometry | |
| 294 | + | if returnFunc, ok := ret.Get(0).(func() <-chan domain.Geometry); ok { | |
| 242 | 295 | r0 = returnFunc() | |
| 243 | 296 | } else { | |
| 244 | 297 | if ret.Get(0) != nil { | |
| 245 | - | r0 = ret.Get(0).(<-chan domain.Size) | |
| 298 | + | r0 = ret.Get(0).(<-chan domain.Geometry) | |
| 246 | 299 | } | |
| 247 | 300 | } | |
| 248 | 301 | return r0 | |
| @@ -265,65 +318,12 @@ func (_c *MockTerminal_ResizeEvents_Call) Run(run func()) *MockTerminal_ResizeEv | |||
| 265 | 318 | return _c | |
| 266 | 319 | } | |
| 267 | 320 | ||
| 268 | - | func (_c *MockTerminal_ResizeEvents_Call) Return(sizeCh <-chan domain.Size) *MockTerminal_ResizeEvents_Call { | |
| 269 | - | _c.Call.Return(sizeCh) | |
| 270 | - | return _c | |
| 271 | - | } | |
| 272 | - | ||
| 273 | - | func (_c *MockTerminal_ResizeEvents_Call) RunAndReturn(run func() <-chan domain.Size) *MockTerminal_ResizeEvents_Call { | |
| 274 | - | _c.Call.Return(run) | |
| 275 | - | return _c | |
| 276 | - | } | |
| 277 | - | ||
| 278 | - | // Size provides a mock function for the type MockTerminal | |
| 279 | - | func (_mock *MockTerminal) Size() (domain.Size, error) { | |
| 280 | - | ret := _mock.Called() | |
| 281 | - | ||
| 282 | - | if len(ret) == 0 { | |
| 283 | - | panic("no return value specified for Size") | |
| 284 | - | } | |
| 285 | - | ||
| 286 | - | var r0 domain.Size | |
| 287 | - | var r1 error | |
| 288 | - | if returnFunc, ok := ret.Get(0).(func() (domain.Size, error)); ok { | |
| 289 | - | return returnFunc() | |
| 290 | - | } | |
| 291 | - | if returnFunc, ok := ret.Get(0).(func() domain.Size); ok { | |
| 292 | - | r0 = returnFunc() | |
| 293 | - | } else { | |
| 294 | - | r0 = ret.Get(0).(domain.Size) | |
| 295 | - | } | |
| 296 | - | if returnFunc, ok := ret.Get(1).(func() error); ok { | |
| 297 | - | r1 = returnFunc() | |
| 298 | - | } else { | |
| 299 | - | r1 = ret.Error(1) | |
| 300 | - | } | |
| 301 | - | return r0, r1 | |
| 302 | - | } | |
| 303 | - | ||
| 304 | - | // MockTerminal_Size_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Size' | |
| 305 | - | type MockTerminal_Size_Call struct { | |
| 306 | - | *mock.Call | |
| 307 | - | } | |
| 308 | - | ||
| 309 | - | // Size is a helper method to define mock.On call | |
| 310 | - | func (_e *MockTerminal_Expecter) Size() *MockTerminal_Size_Call { | |
| 311 | - | return &MockTerminal_Size_Call{Call: _e.mock.On("Size")} | |
| 312 | - | } | |
| 313 | - | ||
| 314 | - | func (_c *MockTerminal_Size_Call) Run(run func()) *MockTerminal_Size_Call { | |
| 315 | - | _c.Call.Run(func(args mock.Arguments) { | |
| 316 | - | run() | |
| 317 | - | }) | |
| 318 | - | return _c | |
| 319 | - | } | |
| 320 | - | ||
| 321 | - | func (_c *MockTerminal_Size_Call) Return(size domain.Size, err error) *MockTerminal_Size_Call { | |
| 322 | - | _c.Call.Return(size, err) | |
| 321 | + | func (_c *MockTerminal_ResizeEvents_Call) Return(geometryCh <-chan domain.Geometry) *MockTerminal_ResizeEvents_Call { | |
| 322 | + | _c.Call.Return(geometryCh) | |
| 323 | 323 | return _c | |
| 324 | 324 | } | |
| 325 | 325 | ||
| 326 | - | func (_c *MockTerminal_Size_Call) RunAndReturn(run func() (domain.Size, error)) *MockTerminal_Size_Call { | |
| 326 | + | func (_c *MockTerminal_ResizeEvents_Call) RunAndReturn(run func() <-chan domain.Geometry) *MockTerminal_ResizeEvents_Call { | |
| 327 | 327 | _c.Call.Return(run) | |
| 328 | 328 | return _c | |
| 329 | 329 | } | |
| @@ -137,6 +137,59 @@ func (_c *MockTerminal_Flush_Call) RunAndReturn(run func() error) *MockTerminal_ | |||||
| 137 | return _c | 137 | return _c | ||
| 138 | } | 138 | } | ||
| 139 | 139 | ||||
| 140 | + | // Geometry provides a mock function for the type MockTerminal | |||
| 141 | + | func (_mock *MockTerminal) Geometry() (domain.Geometry, error) { | |||
| 142 | + | ret := _mock.Called() | |||
| 143 | + | ||||
| 144 | + | if len(ret) == 0 { | |||
| 145 | + | panic("no return value specified for Geometry") | |||
| 146 | + | } | |||
| 147 | + | ||||
| 148 | + | var r0 domain.Geometry | |||
| 149 | + | var r1 error | |||
| 150 | + | if returnFunc, ok := ret.Get(0).(func() (domain.Geometry, error)); ok { | |||
| 151 | + | return returnFunc() | |||
| 152 | + | } | |||
| 153 | + | if returnFunc, ok := ret.Get(0).(func() domain.Geometry); ok { | |||
| 154 | + | r0 = returnFunc() | |||
| 155 | + | } else { | |||
| 156 | + | r0 = ret.Get(0).(domain.Geometry) | |||
| 157 | + | } | |||
| 158 | + | if returnFunc, ok := ret.Get(1).(func() error); ok { | |||
| 159 | + | r1 = returnFunc() | |||
| 160 | + | } else { | |||
| 161 | + | r1 = ret.Error(1) | |||
| 162 | + | } | |||
| 163 | + | return r0, r1 | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | // MockTerminal_Geometry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Geometry' | |||
| 167 | + | type MockTerminal_Geometry_Call struct { | |||
| 168 | + | *mock.Call | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | // Geometry is a helper method to define mock.On call | |||
| 172 | + | func (_e *MockTerminal_Expecter) Geometry() *MockTerminal_Geometry_Call { | |||
| 173 | + | return &MockTerminal_Geometry_Call{Call: _e.mock.On("Geometry")} | |||
| 174 | + | } | |||
| 175 | + | ||||
| 176 | + | func (_c *MockTerminal_Geometry_Call) Run(run func()) *MockTerminal_Geometry_Call { | |||
| 177 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 178 | + | run() | |||
| 179 | + | }) | |||
| 180 | + | return _c | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | func (_c *MockTerminal_Geometry_Call) Return(geometry domain.Geometry, err error) *MockTerminal_Geometry_Call { | |||
| 184 | + | _c.Call.Return(geometry, err) | |||
| 185 | + | return _c | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | func (_c *MockTerminal_Geometry_Call) RunAndReturn(run func() (domain.Geometry, error)) *MockTerminal_Geometry_Call { | |||
| 189 | + | _c.Call.Return(run) | |||
| 190 | + | return _c | |||
| 191 | + | } | |||
| 192 | + | ||||
| 140 | // In provides a mock function for the type MockTerminal | 193 | // In provides a mock function for the type MockTerminal | ||
| 141 | func (_mock *MockTerminal) In() io.Reader { | 194 | func (_mock *MockTerminal) In() io.Reader { | ||
| 142 | ret := _mock.Called() | 195 | ret := _mock.Called() | ||
| @@ -230,19 +283,19 @@ func (_c *MockTerminal_Out_Call) RunAndReturn(run func() io.Writer) *MockTermina | |||||
| 230 | } | 283 | } | ||
| 231 | 284 | ||||
| 232 | // ResizeEvents provides a mock function for the type MockTerminal | 285 | // ResizeEvents provides a mock function for the type MockTerminal | ||
| 233 | - | func (_mock *MockTerminal) ResizeEvents() <-chan domain.Size { | 286 | + | func (_mock *MockTerminal) ResizeEvents() <-chan domain.Geometry { |
| 234 | ret := _mock.Called() | 287 | ret := _mock.Called() | ||
| 235 | 288 | ||||
| 236 | if len(ret) == 0 { | 289 | if len(ret) == 0 { | ||
| 237 | panic("no return value specified for ResizeEvents") | 290 | panic("no return value specified for ResizeEvents") | ||
| 238 | } | 291 | } | ||
| 239 | 292 | ||||
| 240 | - | var r0 <-chan domain.Size | 293 | + | var r0 <-chan domain.Geometry |
| 241 | - | if returnFunc, ok := ret.Get(0).(func() <-chan domain.Size); ok { | 294 | + | if returnFunc, ok := ret.Get(0).(func() <-chan domain.Geometry); ok { |
| 242 | r0 = returnFunc() | 295 | r0 = returnFunc() | ||
| 243 | } else { | 296 | } else { | ||
| 244 | if ret.Get(0) != nil { | 297 | if ret.Get(0) != nil { | ||
| 245 | - | r0 = ret.Get(0).(<-chan domain.Size) | 298 | + | r0 = ret.Get(0).(<-chan domain.Geometry) |
| 246 | } | 299 | } | ||
| 247 | } | 300 | } | ||
| 248 | return r0 | 301 | return r0 | ||
| @@ -265,65 +318,12 @@ func (_c *MockTerminal_ResizeEvents_Call) Run(run func()) *MockTerminal_ResizeEv | |||||
| 265 | return _c | 318 | return _c | ||
| 266 | } | 319 | } | ||
| 267 | 320 | ||||
| 268 | - | func (_c *MockTerminal_ResizeEvents_Call) Return(sizeCh <-chan domain.Size) *MockTerminal_ResizeEvents_Call { | 321 | + | func (_c *MockTerminal_ResizeEvents_Call) Return(geometryCh <-chan domain.Geometry) *MockTerminal_ResizeEvents_Call { |
| 269 | - | _c.Call.Return(sizeCh) | 322 | + | _c.Call.Return(geometryCh) |
| 270 | - | return _c | |||
| 271 | - | } | |||
| 272 | - | ||||
| 273 | - | func (_c *MockTerminal_ResizeEvents_Call) RunAndReturn(run func() <-chan domain.Size) *MockTerminal_ResizeEvents_Call { | |||
| 274 | - | _c.Call.Return(run) | |||
| 275 | - | return _c | |||
| 276 | - | } | |||
| 277 | - | ||||
| 278 | - | // Size provides a mock function for the type MockTerminal | |||
| 279 | - | func (_mock *MockTerminal) Size() (domain.Size, error) { | |||
| 280 | - | ret := _mock.Called() | |||
| 281 | - | ||||
| 282 | - | if len(ret) == 0 { | |||
| 283 | - | panic("no return value specified for Size") | |||
| 284 | - | } | |||
| 285 | - | ||||
| 286 | - | var r0 domain.Size | |||
| 287 | - | var r1 error | |||
| 288 | - | if returnFunc, ok := ret.Get(0).(func() (domain.Size, error)); ok { | |||
| 289 | - | return returnFunc() | |||
| 290 | - | } | |||
| 291 | - | if returnFunc, ok := ret.Get(0).(func() domain.Size); ok { | |||
| 292 | - | r0 = returnFunc() | |||
| 293 | - | } else { | |||
| 294 | - | r0 = ret.Get(0).(domain.Size) | |||
| 295 | - | } | |||
| 296 | - | if returnFunc, ok := ret.Get(1).(func() error); ok { | |||
| 297 | - | r1 = returnFunc() | |||
| 298 | - | } else { | |||
| 299 | - | r1 = ret.Error(1) | |||
| 300 | - | } | |||
| 301 | - | return r0, r1 | |||
| 302 | - | } | |||
| 303 | - | ||||
| 304 | - | // MockTerminal_Size_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Size' | |||
| 305 | - | type MockTerminal_Size_Call struct { | |||
| 306 | - | *mock.Call | |||
| 307 | - | } | |||
| 308 | - | ||||
| 309 | - | // Size is a helper method to define mock.On call | |||
| 310 | - | func (_e *MockTerminal_Expecter) Size() *MockTerminal_Size_Call { | |||
| 311 | - | return &MockTerminal_Size_Call{Call: _e.mock.On("Size")} | |||
| 312 | - | } | |||
| 313 | - | ||||
| 314 | - | func (_c *MockTerminal_Size_Call) Run(run func()) *MockTerminal_Size_Call { | |||
| 315 | - | _c.Call.Run(func(args mock.Arguments) { | |||
| 316 | - | run() | |||
| 317 | - | }) | |||
| 318 | - | return _c | |||
| 319 | - | } | |||
| 320 | - | ||||
| 321 | - | func (_c *MockTerminal_Size_Call) Return(size domain.Size, err error) *MockTerminal_Size_Call { | |||
| 322 | - | _c.Call.Return(size, err) | |||
| 323 | return _c | 323 | return _c | ||
| 324 | } | 324 | } | ||
| 325 | 325 | ||||
| 326 | - | func (_c *MockTerminal_Size_Call) RunAndReturn(run func() (domain.Size, error)) *MockTerminal_Size_Call { | 326 | + | func (_c *MockTerminal_ResizeEvents_Call) RunAndReturn(run func() <-chan domain.Geometry) *MockTerminal_ResizeEvents_Call { |
| 327 | _c.Call.Return(run) | 327 | _c.Call.Return(run) | ||
| 328 | return _c | 328 | return _c | ||
| 329 | } | 329 | } | ||
M internal/ports/navigation_test.go +1 -1
| @@ -152,7 +152,7 @@ func TestHelloNavigationValidationTable(t *testing.T) { | |||
| 152 | 152 | {name: "client-owned home picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityHomePicker}, valid: false}, | |
| 153 | 153 | {name: "daemon-owned back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false}, | |
| 154 | 154 | {name: "remote-target back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false}, | |
| 155 | - | {name: "back with startup picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: true, capability: NavigationCapabilityBack, overlay: StartupOverlaySessionPicker, wantPayload: "002202" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "00" + "0000" + "0201"}, | |
| 155 | + | {name: "back with startup picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: true, capability: NavigationCapabilityBack, overlay: StartupOverlaySessionPicker, wantPayload: "002302" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "00000000" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "00" + "0000" + "0201"}, | |
| 156 | 156 | {name: "new rejects navigation", hello: Hello{Version: ProtocolVersion, Intent: IntentNew, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: false}, | |
| 157 | 157 | {name: "unknown capability", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: 4}, valid: false}, | |
| 158 | 158 | {name: "back without picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack}, valid: false}, | |
| @@ -152,7 +152,7 @@ func TestHelloNavigationValidationTable(t *testing.T) { | |||||
| 152 | {name: "client-owned home picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityHomePicker}, valid: false}, | 152 | {name: "client-owned home picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityHomePicker}, valid: false}, | ||
| 153 | {name: "daemon-owned back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false}, | 153 | {name: "daemon-owned back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false}, | ||
| 154 | {name: "remote-target back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false}, | 154 | {name: "remote-target back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false}, | ||
| 155 | - | {name: "back with startup picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: true, capability: NavigationCapabilityBack, overlay: StartupOverlaySessionPicker, wantPayload: "002202" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "00" + "0000" + "0201"}, | 155 | + | {name: "back with startup picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: true, capability: NavigationCapabilityBack, overlay: StartupOverlaySessionPicker, wantPayload: "002302" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "00000000" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "00" + "0000" + "0201"}, |
| 156 | {name: "new rejects navigation", hello: Hello{Version: ProtocolVersion, Intent: IntentNew, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: false}, | 156 | {name: "new rejects navigation", hello: Hello{Version: ProtocolVersion, Intent: IntentNew, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: false}, | ||
| 157 | {name: "unknown capability", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: 4}, valid: false}, | 157 | {name: "unknown capability", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: 4}, valid: false}, | ||
| 158 | {name: "back without picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack}, valid: false}, | 158 | {name: "back without picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack}, valid: false}, | ||
M internal/ports/pty.go +4 -2
| @@ -10,7 +10,7 @@ import ( | |||
| 10 | 10 | // PTY is a running pseudo-terminal-backed child process. | |
| 11 | 11 | type PTY interface { | |
| 12 | 12 | io.ReadWriteCloser // Read = child output, Write = child input | |
| 13 | - | Resize(sz domain.Size) error | |
| 13 | + | Resize(geometry domain.Geometry) error | |
| 14 | 14 | Pid() int | |
| 15 | 15 | ForegroundPgid() (int, error) | |
| 16 | 16 | } | |
| @@ -19,5 +19,7 @@ type PTY interface { | |||
| 19 | 19 | type PTYFactory interface { | |
| 20 | 20 | // Open creates a PTY while honoring ctx cancellation. Implementations must | |
| 21 | 21 | // return promptly once ctx is cancelled and must not leave a child running. | |
| 22 | - | Open(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size) (PTY, error) | |
| 22 | + | // geometry is applied before the child starts, including optional pixel | |
| 23 | + | // dimensions when they are known. | |
| 24 | + | Open(ctx context.Context, cmd string, args []string, env []string, dir string, geometry domain.Geometry) (PTY, error) | |
| 23 | 25 | } | |
| @@ -10,7 +10,7 @@ import ( | |||||
| 10 | // PTY is a running pseudo-terminal-backed child process. | 10 | // PTY is a running pseudo-terminal-backed child process. | ||
| 11 | type PTY interface { | 11 | type PTY interface { | ||
| 12 | io.ReadWriteCloser // Read = child output, Write = child input | 12 | io.ReadWriteCloser // Read = child output, Write = child input | ||
| 13 | - | Resize(sz domain.Size) error | 13 | + | Resize(geometry domain.Geometry) error |
| 14 | Pid() int | 14 | Pid() int | ||
| 15 | ForegroundPgid() (int, error) | 15 | ForegroundPgid() (int, error) | ||
| 16 | } | 16 | } | ||
| @@ -19,5 +19,7 @@ type PTY interface { | |||||
| 19 | type PTYFactory interface { | 19 | type PTYFactory interface { | ||
| 20 | // Open creates a PTY while honoring ctx cancellation. Implementations must | 20 | // Open creates a PTY while honoring ctx cancellation. Implementations must | ||
| 21 | // return promptly once ctx is cancelled and must not leave a child running. | 21 | // return promptly once ctx is cancelled and must not leave a child running. | ||
| 22 | - | Open(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size) (PTY, error) | 22 | + | // geometry is applied before the child starts, including optional pixel |
| 23 | + | // dimensions when they are known. | |||
| 24 | + | Open(ctx context.Context, cmd string, args []string, env []string, dir string, geometry domain.Geometry) (PTY, error) | |||
| 23 | } | 25 | } | ||
M internal/ports/terminal.go +2 -2
| @@ -10,8 +10,8 @@ import ( | |||
| 10 | 10 | // Terminal is the CLIENT-side controlling terminal. | |
| 11 | 11 | type Terminal interface { | |
| 12 | 12 | EnterRaw() (restore func() error, err error) | |
| 13 | - | Size() (domain.Size, error) | |
| 14 | - | ResizeEvents() <-chan domain.Size | |
| 13 | + | Geometry() (domain.Geometry, error) | |
| 14 | + | ResizeEvents() <-chan domain.Geometry | |
| 15 | 15 | In() io.Reader | |
| 16 | 16 | Out() io.Writer | |
| 17 | 17 | Flush() error | |
| @@ -10,8 +10,8 @@ import ( | |||||
| 10 | // Terminal is the CLIENT-side controlling terminal. | 10 | // Terminal is the CLIENT-side controlling terminal. | ||
| 11 | type Terminal interface { | 11 | type Terminal interface { | ||
| 12 | EnterRaw() (restore func() error, err error) | 12 | EnterRaw() (restore func() error, err error) | ||
| 13 | - | Size() (domain.Size, error) | 13 | + | Geometry() (domain.Geometry, error) |
| 14 | - | ResizeEvents() <-chan domain.Size | 14 | + | ResizeEvents() <-chan domain.Geometry |
| 15 | In() io.Reader | 15 | In() io.Reader | ||
| 16 | Out() io.Writer | 16 | Out() io.Writer | ||
| 17 | Flush() error | 17 | Flush() error | ||
M internal/ports/wire.go +55 -7
| @@ -169,6 +169,8 @@ type Hello struct { | |||
| 169 | 169 | ResumeToken uint64 | |
| 170 | 170 | Name string | |
| 171 | 171 | Size domain.Size | |
| 172 | + | PixelWidth int | |
| 173 | + | PixelHeight int | |
| 172 | 174 | TermEnv string | |
| 173 | 175 | Cwd string | |
| 174 | 176 | TrueColor bool | |
| @@ -200,9 +202,23 @@ type Input struct { | |||
| 200 | 202 | Data []byte | |
| 201 | 203 | } | |
| 202 | 204 | ||
| 203 | - | // Resize notifies the daemon of a client-side terminal size change. | |
| 205 | + | // Geometry returns the complete controlling-terminal geometry carried by the | |
| 206 | + | // handshake. Pixel dimensions are optional as a pair. | |
| 207 | + | func (h Hello) Geometry() domain.Geometry { | |
| 208 | + | return domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}.NormalizePixels() | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // Resize notifies the daemon of a client-side terminal geometry change. | |
| 204 | 212 | type Resize struct { | |
| 205 | - | Size domain.Size | |
| 213 | + | Size domain.Size | |
| 214 | + | PixelWidth int | |
| 215 | + | PixelHeight int | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | // Geometry returns the complete controlling-terminal geometry carried by the | |
| 219 | + | // resize message. Pixel dimensions are optional as a pair. | |
| 220 | + | func (m Resize) Geometry() domain.Geometry { | |
| 221 | + | return domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}.NormalizePixels() | |
| 206 | 222 | } | |
| 207 | 223 | ||
| 208 | 224 | // Theme reports the client's terminal foreground/background colors, ANSI | |
| @@ -732,8 +748,8 @@ func ValidateHello(h Hello) error { | |||
| 732 | 748 | if h.Intent != IntentEphemeral && h.Intent != IntentNew && h.Intent != IntentAttach && h.Intent != IntentResume { | |
| 733 | 749 | return ErrInvalidHello | |
| 734 | 750 | } | |
| 735 | - | if err := ValidateSize(h.Size); err != nil { | |
| 736 | - | return fmt.Errorf("%w: size", ErrInvalidHello) | |
| 751 | + | if err := ValidateGeometry(domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}); err != nil { | |
| 752 | + | return fmt.Errorf("%w: geometry", ErrInvalidHello) | |
| 737 | 753 | } | |
| 738 | 754 | if !validEnvironmentPolicy(h.EnvironmentPolicy) { | |
| 739 | 755 | return ErrInvalidHello | |
| @@ -881,6 +897,8 @@ func MarshalHello(h Hello) []byte { | |||
| 881 | 897 | w.putString(h.Name) | |
| 882 | 898 | w.putUint16(uint16(h.Size.Cols)) | |
| 883 | 899 | w.putUint16(uint16(h.Size.Rows)) | |
| 900 | + | w.putUint16(uint16(h.PixelWidth)) | |
| 901 | + | w.putUint16(uint16(h.PixelHeight)) | |
| 884 | 902 | w.putString(h.TermEnv) | |
| 885 | 903 | w.putString(h.Cwd) | |
| 886 | 904 | w.putBool(h.TrueColor) | |
| @@ -925,6 +943,12 @@ func preflightHello(b []byte) error { | |||
| 925 | 943 | if _, err := r.getUint16(); err != nil { | |
| 926 | 944 | return err | |
| 927 | 945 | } | |
| 946 | + | if _, err := r.getUint16(); err != nil { | |
| 947 | + | return err | |
| 948 | + | } | |
| 949 | + | if _, err := r.getUint16(); err != nil { | |
| 950 | + | return err | |
| 951 | + | } | |
| 928 | 952 | if err := r.skipString(); err != nil { | |
| 929 | 953 | return err | |
| 930 | 954 | } | |
| @@ -1004,7 +1028,17 @@ func UnmarshalHello(b []byte) (Hello, error) { | |||
| 1004 | 1028 | if err != nil { | |
| 1005 | 1029 | return Hello{}, err | |
| 1006 | 1030 | } | |
| 1031 | + | pixelWidth, err := r.getUint16() | |
| 1032 | + | if err != nil { | |
| 1033 | + | return Hello{}, err | |
| 1034 | + | } | |
| 1035 | + | pixelHeight, err := r.getUint16() | |
| 1036 | + | if err != nil { | |
| 1037 | + | return Hello{}, err | |
| 1038 | + | } | |
| 1007 | 1039 | h.Size = domain.Size{Cols: int(cols), Rows: int(rows)} | |
| 1040 | + | h.PixelWidth = int(pixelWidth) | |
| 1041 | + | h.PixelHeight = int(pixelHeight) | |
| 1008 | 1042 | if h.TermEnv, err = r.getString(); err != nil { | |
| 1009 | 1043 | return Hello{}, err | |
| 1010 | 1044 | } | |
| @@ -1134,12 +1168,14 @@ func UnmarshalClientNotice(b []byte) (ClientNotice, error) { | |||
| 1134 | 1168 | ||
| 1135 | 1169 | // MarshalResize encodes m into a Resize message payload. | |
| 1136 | 1170 | func MarshalResize(m Resize) ([]byte, error) { | |
| 1137 | - | if err := ValidateSize(m.Size); err != nil { | |
| 1171 | + | if err := ValidateGeometry(domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}); err != nil { | |
| 1138 | 1172 | return nil, err | |
| 1139 | 1173 | } | |
| 1140 | 1174 | w := payloadWriter{} | |
| 1141 | 1175 | w.putUint16(uint16(m.Size.Cols)) | |
| 1142 | 1176 | w.putUint16(uint16(m.Size.Rows)) | |
| 1177 | + | w.putUint16(uint16(m.PixelWidth)) | |
| 1178 | + | w.putUint16(uint16(m.PixelHeight)) | |
| 1143 | 1179 | return w.b, nil | |
| 1144 | 1180 | } | |
| 1145 | 1181 | ||
| @@ -1256,11 +1292,23 @@ func UnmarshalResize(b []byte) (Resize, error) { | |||
| 1256 | 1292 | if err != nil { | |
| 1257 | 1293 | return Resize{}, err | |
| 1258 | 1294 | } | |
| 1295 | + | pixelWidth, err := r.getUint16() | |
| 1296 | + | if err != nil { | |
| 1297 | + | return Resize{}, err | |
| 1298 | + | } | |
| 1299 | + | pixelHeight, err := r.getUint16() | |
| 1300 | + | if err != nil { | |
| 1301 | + | return Resize{}, err | |
| 1302 | + | } | |
| 1259 | 1303 | if err := r.done(); err != nil { | |
| 1260 | 1304 | return Resize{}, err | |
| 1261 | 1305 | } | |
| 1262 | - | m := Resize{Size: domain.Size{Cols: int(cols), Rows: int(rows)}} | |
| 1263 | - | if err := ValidateSize(m.Size); err != nil { | |
| 1306 | + | m := Resize{ | |
| 1307 | + | Size: domain.Size{Cols: int(cols), Rows: int(rows)}, | |
| 1308 | + | PixelWidth: int(pixelWidth), | |
| 1309 | + | PixelHeight: int(pixelHeight), | |
| 1310 | + | } | |
| 1311 | + | if err := ValidateGeometry(domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}); err != nil { | |
| 1264 | 1312 | return Resize{}, err | |
| 1265 | 1313 | } | |
| 1266 | 1314 | return m, nil | |
| @@ -169,6 +169,8 @@ type Hello struct { | |||||
| 169 | ResumeToken uint64 | 169 | ResumeToken uint64 | ||
| 170 | Name string | 170 | Name string | ||
| 171 | Size domain.Size | 171 | Size domain.Size | ||
| 172 | + | PixelWidth int | |||
| 173 | + | PixelHeight int | |||
| 172 | TermEnv string | 174 | TermEnv string | ||
| 173 | Cwd string | 175 | Cwd string | ||
| 174 | TrueColor bool | 176 | TrueColor bool | ||
| @@ -200,9 +202,23 @@ type Input struct { | |||||
| 200 | Data []byte | 202 | Data []byte | ||
| 201 | } | 203 | } | ||
| 202 | 204 | ||||
| 203 | - | // Resize notifies the daemon of a client-side terminal size change. | 205 | + | // Geometry returns the complete controlling-terminal geometry carried by the |
| 206 | + | // handshake. Pixel dimensions are optional as a pair. | |||
| 207 | + | func (h Hello) Geometry() domain.Geometry { | |||
| 208 | + | return domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}.NormalizePixels() | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | // Resize notifies the daemon of a client-side terminal geometry change. | |||
| 204 | type Resize struct { | 212 | type Resize struct { | ||
| 205 | - | Size domain.Size | 213 | + | Size domain.Size |
| 214 | + | PixelWidth int | |||
| 215 | + | PixelHeight int | |||
| 216 | + | } | |||
| 217 | + | ||||
| 218 | + | // Geometry returns the complete controlling-terminal geometry carried by the | |||
| 219 | + | // resize message. Pixel dimensions are optional as a pair. | |||
| 220 | + | func (m Resize) Geometry() domain.Geometry { | |||
| 221 | + | return domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}.NormalizePixels() | |||
| 206 | } | 222 | } | ||
| 207 | 223 | ||||
| 208 | // Theme reports the client's terminal foreground/background colors, ANSI | 224 | // Theme reports the client's terminal foreground/background colors, ANSI | ||
| @@ -732,8 +748,8 @@ func ValidateHello(h Hello) error { | |||||
| 732 | if h.Intent != IntentEphemeral && h.Intent != IntentNew && h.Intent != IntentAttach && h.Intent != IntentResume { | 748 | if h.Intent != IntentEphemeral && h.Intent != IntentNew && h.Intent != IntentAttach && h.Intent != IntentResume { | ||
| 733 | return ErrInvalidHello | 749 | return ErrInvalidHello | ||
| 734 | } | 750 | } | ||
| 735 | - | if err := ValidateSize(h.Size); err != nil { | 751 | + | if err := ValidateGeometry(domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}); err != nil { |
| 736 | - | return fmt.Errorf("%w: size", ErrInvalidHello) | 752 | + | return fmt.Errorf("%w: geometry", ErrInvalidHello) |
| 737 | } | 753 | } | ||
| 738 | if !validEnvironmentPolicy(h.EnvironmentPolicy) { | 754 | if !validEnvironmentPolicy(h.EnvironmentPolicy) { | ||
| 739 | return ErrInvalidHello | 755 | return ErrInvalidHello | ||
| @@ -881,6 +897,8 @@ func MarshalHello(h Hello) []byte { | |||||
| 881 | w.putString(h.Name) | 897 | w.putString(h.Name) | ||
| 882 | w.putUint16(uint16(h.Size.Cols)) | 898 | w.putUint16(uint16(h.Size.Cols)) | ||
| 883 | w.putUint16(uint16(h.Size.Rows)) | 899 | w.putUint16(uint16(h.Size.Rows)) | ||
| 900 | + | w.putUint16(uint16(h.PixelWidth)) | |||
| 901 | + | w.putUint16(uint16(h.PixelHeight)) | |||
| 884 | w.putString(h.TermEnv) | 902 | w.putString(h.TermEnv) | ||
| 885 | w.putString(h.Cwd) | 903 | w.putString(h.Cwd) | ||
| 886 | w.putBool(h.TrueColor) | 904 | w.putBool(h.TrueColor) | ||
| @@ -925,6 +943,12 @@ func preflightHello(b []byte) error { | |||||
| 925 | if _, err := r.getUint16(); err != nil { | 943 | if _, err := r.getUint16(); err != nil { | ||
| 926 | return err | 944 | return err | ||
| 927 | } | 945 | } | ||
| 946 | + | if _, err := r.getUint16(); err != nil { | |||
| 947 | + | return err | |||
| 948 | + | } | |||
| 949 | + | if _, err := r.getUint16(); err != nil { | |||
| 950 | + | return err | |||
| 951 | + | } | |||
| 928 | if err := r.skipString(); err != nil { | 952 | if err := r.skipString(); err != nil { | ||
| 929 | return err | 953 | return err | ||
| 930 | } | 954 | } | ||
| @@ -1004,7 +1028,17 @@ func UnmarshalHello(b []byte) (Hello, error) { | |||||
| 1004 | if err != nil { | 1028 | if err != nil { | ||
| 1005 | return Hello{}, err | 1029 | return Hello{}, err | ||
| 1006 | } | 1030 | } | ||
| 1031 | + | pixelWidth, err := r.getUint16() | |||
| 1032 | + | if err != nil { | |||
| 1033 | + | return Hello{}, err | |||
| 1034 | + | } | |||
| 1035 | + | pixelHeight, err := r.getUint16() | |||
| 1036 | + | if err != nil { | |||
| 1037 | + | return Hello{}, err | |||
| 1038 | + | } | |||
| 1007 | h.Size = domain.Size{Cols: int(cols), Rows: int(rows)} | 1039 | h.Size = domain.Size{Cols: int(cols), Rows: int(rows)} | ||
| 1040 | + | h.PixelWidth = int(pixelWidth) | |||
| 1041 | + | h.PixelHeight = int(pixelHeight) | |||
| 1008 | if h.TermEnv, err = r.getString(); err != nil { | 1042 | if h.TermEnv, err = r.getString(); err != nil { | ||
| 1009 | return Hello{}, err | 1043 | return Hello{}, err | ||
| 1010 | } | 1044 | } | ||
| @@ -1134,12 +1168,14 @@ func UnmarshalClientNotice(b []byte) (ClientNotice, error) { | |||||
| 1134 | 1168 | ||||
| 1135 | // MarshalResize encodes m into a Resize message payload. | 1169 | // MarshalResize encodes m into a Resize message payload. | ||
| 1136 | func MarshalResize(m Resize) ([]byte, error) { | 1170 | func MarshalResize(m Resize) ([]byte, error) { | ||
| 1137 | - | if err := ValidateSize(m.Size); err != nil { | 1171 | + | if err := ValidateGeometry(domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}); err != nil { |
| 1138 | return nil, err | 1172 | return nil, err | ||
| 1139 | } | 1173 | } | ||
| 1140 | w := payloadWriter{} | 1174 | w := payloadWriter{} | ||
| 1141 | w.putUint16(uint16(m.Size.Cols)) | 1175 | w.putUint16(uint16(m.Size.Cols)) | ||
| 1142 | w.putUint16(uint16(m.Size.Rows)) | 1176 | w.putUint16(uint16(m.Size.Rows)) | ||
| 1177 | + | w.putUint16(uint16(m.PixelWidth)) | |||
| 1178 | + | w.putUint16(uint16(m.PixelHeight)) | |||
| 1143 | return w.b, nil | 1179 | return w.b, nil | ||
| 1144 | } | 1180 | } | ||
| 1145 | 1181 | ||||
| @@ -1256,11 +1292,23 @@ func UnmarshalResize(b []byte) (Resize, error) { | |||||
| 1256 | if err != nil { | 1292 | if err != nil { | ||
| 1257 | return Resize{}, err | 1293 | return Resize{}, err | ||
| 1258 | } | 1294 | } | ||
| 1295 | + | pixelWidth, err := r.getUint16() | |||
| 1296 | + | if err != nil { | |||
| 1297 | + | return Resize{}, err | |||
| 1298 | + | } | |||
| 1299 | + | pixelHeight, err := r.getUint16() | |||
| 1300 | + | if err != nil { | |||
| 1301 | + | return Resize{}, err | |||
| 1302 | + | } | |||
| 1259 | if err := r.done(); err != nil { | 1303 | if err := r.done(); err != nil { | ||
| 1260 | return Resize{}, err | 1304 | return Resize{}, err | ||
| 1261 | } | 1305 | } | ||
| 1262 | - | m := Resize{Size: domain.Size{Cols: int(cols), Rows: int(rows)}} | 1306 | + | m := Resize{ |
| 1263 | - | if err := ValidateSize(m.Size); err != nil { | 1307 | + | Size: domain.Size{Cols: int(cols), Rows: int(rows)}, |
| 1308 | + | PixelWidth: int(pixelWidth), | |||
| 1309 | + | PixelHeight: int(pixelHeight), | |||
| 1310 | + | } | |||
| 1311 | + | if err := ValidateGeometry(domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}); err != nil { | |||
| 1264 | return Resize{}, err | 1312 | return Resize{}, err | ||
| 1265 | } | 1313 | } | ||
| 1266 | return m, nil | 1314 | return m, nil | ||
M internal/ports/wire_final_test.go +5 -5
| @@ -11,8 +11,8 @@ import ( | |||
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | 13 | func TestFinalProtocolVersionAndNoV21Hello(t *testing.T) { | |
| 14 | - | if ProtocolVersion != 34 { | |
| 15 | - | t.Fatalf("ProtocolVersion = %d, want 34", ProtocolVersion) | |
| 14 | + | if ProtocolVersion != 35 { | |
| 15 | + | t.Fatalf("ProtocolVersion = %d, want 35", ProtocolVersion) | |
| 16 | 16 | } | |
| 17 | 17 | payload := MarshalHello(Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}) | |
| 18 | 18 | if len(payload) < 2 { | |
| @@ -26,8 +26,8 @@ func TestFinalProtocolVersionAndNoV21Hello(t *testing.T) { | |||
| 26 | 26 | ||
| 27 | 27 | func TestFinalHelloGoldenStrict(t *testing.T) { | |
| 28 | 28 | msg := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}} | |
| 29 | - | want := append([]byte{0, 34, 2}, make([]byte, 16+8)...) | |
| 30 | - | want = append(want, 0, 0, 0, 80, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) | |
| 29 | + | want := append([]byte{0, 35, 2}, make([]byte, 16+8)...) | |
| 30 | + | want = append(want, 0, 0, 0, 80, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) | |
| 31 | 31 | got := MarshalHello(msg) | |
| 32 | 32 | if !bytes.Equal(got, want) { | |
| 33 | 33 | t.Fatalf("Hello bytes = %x, want %x", got, want) | |
| @@ -66,7 +66,7 @@ func TestFinalHelloSemanticValidation(t *testing.T) { | |||
| 66 | 66 | ||
| 67 | 67 | func TestFinalResizeGoldenStrict(t *testing.T) { | |
| 68 | 68 | msg := Resize{Size: domain.Size{Cols: 80, Rows: 24}} | |
| 69 | - | want := []byte{0, 80, 0, 24} | |
| 69 | + | want := []byte{0, 80, 0, 24, 0, 0, 0, 0} | |
| 70 | 70 | got, err := MarshalResize(msg) | |
| 71 | 71 | if err != nil { | |
| 72 | 72 | t.Fatalf("MarshalResize() error = %v", err) | |
| @@ -11,8 +11,8 @@ import ( | |||||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| 13 | func TestFinalProtocolVersionAndNoV21Hello(t *testing.T) { | 13 | func TestFinalProtocolVersionAndNoV21Hello(t *testing.T) { | ||
| 14 | - | if ProtocolVersion != 34 { | 14 | + | if ProtocolVersion != 35 { |
| 15 | - | t.Fatalf("ProtocolVersion = %d, want 34", ProtocolVersion) | 15 | + | t.Fatalf("ProtocolVersion = %d, want 35", ProtocolVersion) |
| 16 | } | 16 | } | ||
| 17 | payload := MarshalHello(Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}) | 17 | payload := MarshalHello(Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}) | ||
| 18 | if len(payload) < 2 { | 18 | if len(payload) < 2 { | ||
| @@ -26,8 +26,8 @@ func TestFinalProtocolVersionAndNoV21Hello(t *testing.T) { | |||||
| 26 | 26 | ||||
| 27 | func TestFinalHelloGoldenStrict(t *testing.T) { | 27 | func TestFinalHelloGoldenStrict(t *testing.T) { | ||
| 28 | msg := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}} | 28 | msg := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}} | ||
| 29 | - | want := append([]byte{0, 34, 2}, make([]byte, 16+8)...) | 29 | + | want := append([]byte{0, 35, 2}, make([]byte, 16+8)...) |
| 30 | - | want = append(want, 0, 0, 0, 80, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) | 30 | + | want = append(want, 0, 0, 0, 80, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) |
| 31 | got := MarshalHello(msg) | 31 | got := MarshalHello(msg) | ||
| 32 | if !bytes.Equal(got, want) { | 32 | if !bytes.Equal(got, want) { | ||
| 33 | t.Fatalf("Hello bytes = %x, want %x", got, want) | 33 | t.Fatalf("Hello bytes = %x, want %x", got, want) | ||
| @@ -66,7 +66,7 @@ func TestFinalHelloSemanticValidation(t *testing.T) { | |||||
| 66 | 66 | ||||
| 67 | func TestFinalResizeGoldenStrict(t *testing.T) { | 67 | func TestFinalResizeGoldenStrict(t *testing.T) { | ||
| 68 | msg := Resize{Size: domain.Size{Cols: 80, Rows: 24}} | 68 | msg := Resize{Size: domain.Size{Cols: 80, Rows: 24}} | ||
| 69 | - | want := []byte{0, 80, 0, 24} | 69 | + | want := []byte{0, 80, 0, 24, 0, 0, 0, 0} |
| 70 | got, err := MarshalResize(msg) | 70 | got, err := MarshalResize(msg) | ||
| 71 | if err != nil { | 71 | if err != nil { | ||
| 72 | t.Fatalf("MarshalResize() error = %v", err) | 72 | t.Fatalf("MarshalResize() error = %v", err) | ||
M internal/ports/wire_size.go +14 -0
| @@ -21,3 +21,17 @@ func ValidateSize(size domain.Size) error { | |||
| 21 | 21 | } | |
| 22 | 22 | return nil | |
| 23 | 23 | } | |
| 24 | + | ||
| 25 | + | // ValidateGeometry validates cell geometry and its optional uint16 pixel pair. | |
| 26 | + | func ValidateGeometry(geometry domain.Geometry) error { | |
| 27 | + | if err := ValidateSize(geometry.Size); err != nil { | |
| 28 | + | return err | |
| 29 | + | } | |
| 30 | + | if geometry.PixelWidth == 0 && geometry.PixelHeight == 0 { | |
| 31 | + | return nil | |
| 32 | + | } | |
| 33 | + | if geometry.PixelWidth <= 0 || geometry.PixelHeight <= 0 || geometry.PixelWidth > math.MaxUint16 || geometry.PixelHeight > math.MaxUint16 { | |
| 34 | + | return ErrInvalidSize | |
| 35 | + | } | |
| 36 | + | return nil | |
| 37 | + | } | |
| @@ -21,3 +21,17 @@ func ValidateSize(size domain.Size) error { | |||||
| 21 | } | 21 | } | ||
| 22 | return nil | 22 | return nil | ||
| 23 | } | 23 | } | ||
| 24 | + | ||||
| 25 | + | // ValidateGeometry validates cell geometry and its optional uint16 pixel pair. | |||
| 26 | + | func ValidateGeometry(geometry domain.Geometry) error { | |||
| 27 | + | if err := ValidateSize(geometry.Size); err != nil { | |||
| 28 | + | return err | |||
| 29 | + | } | |||
| 30 | + | if geometry.PixelWidth == 0 && geometry.PixelHeight == 0 { | |||
| 31 | + | return nil | |||
| 32 | + | } | |||
| 33 | + | if geometry.PixelWidth <= 0 || geometry.PixelHeight <= 0 || geometry.PixelWidth > math.MaxUint16 || geometry.PixelHeight > math.MaxUint16 { | |||
| 34 | + | return ErrInvalidSize | |||
| 35 | + | } | |||
| 36 | + | return nil | |||
| 37 | + | } | |||
M internal/ports/wire_test.go +15 -10
| @@ -79,7 +79,7 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) { | |||
| 79 | 79 | TrueColor: true, | |
| 80 | 80 | MaxOutputInFlight: 8, | |
| 81 | 81 | }, | |
| 82 | - | want: []byte{0x00, 0x01, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x02, 0x77, 0x30, 0x00, 0x50, 0x00, 0x18, 0x00, 0x0e, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x2d, 0x32, 0x35, 0x36, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x0c, 0x2f, 0x74, 0x6d, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | |
| 82 | + | want: []byte{0x00, 0x01, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x02, 0x77, 0x30, 0x00, 0x50, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x2d, 0x32, 0x35, 0x36, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x0c, 0x2f, 0x74, 0x6d, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | |
| 83 | 83 | }, | |
| 84 | 84 | { | |
| 85 | 85 | name: "empty strings", | |
| @@ -92,7 +92,11 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) { | |||
| 92 | 92 | Cwd: "", | |
| 93 | 93 | TrueColor: false, | |
| 94 | 94 | }, | |
| 95 | - | want: []byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | |
| 95 | + | want: func() []byte { | |
| 96 | + | out := append([]byte{0x00, 0x01, 0x00}, make([]byte, 26)...) | |
| 97 | + | out = append(out, 0x00, 0x01, 0x00, 0x01) // cell size | |
| 98 | + | return append(out, make([]byte, 16)...) // pixels and remaining fields | |
| 99 | + | }(), | |
| 96 | 100 | }, | |
| 97 | 101 | } | |
| 98 | 102 | ||
| @@ -131,12 +135,13 @@ func TestHelloEnvironmentCodec(t *testing.T) { | |||
| 131 | 135 | } | |
| 132 | 136 | got := MarshalHello(msg) | |
| 133 | 137 | want := []byte{ | |
| 134 | - | 0x00, 0x22, 0x00, // version, intent, first client ID byte | |
| 138 | + | 0x00, 0x23, 0x00, // version, intent, first client ID byte | |
| 135 | 139 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 136 | 140 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // client ID | |
| 137 | 141 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // resume token | |
| 138 | 142 | 0x00, 0x00, // name | |
| 139 | 143 | 0x00, 0x01, 0x00, 0x01, // size | |
| 144 | + | 0x00, 0x00, 0x00, 0x00, // optional pixel size | |
| 140 | 145 | 0x00, 0x00, // TERM | |
| 141 | 146 | 0x00, 0x00, // cwd | |
| 142 | 147 | 0x00, 0x00, // true color, max output in flight | |
| @@ -361,12 +366,12 @@ func TestThemeGenerationClearedWireGoldenPreservesProtocolVersion(t *testing.T) | |||
| 361 | 366 | } | |
| 362 | 367 | want := append([]byte{0x0f, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x00}, make([]byte, 48)...) | |
| 363 | 368 | require.Equal(t, want, MarshalTheme(cleared)) | |
| 364 | - | require.Equal(t, uint16(34), ProtocolVersion) | |
| 369 | + | require.Equal(t, uint16(35), ProtocolVersion) | |
| 365 | 370 | } | |
| 366 | 371 | ||
| 367 | 372 | func TestResizeGoldenAndRoundTrip(t *testing.T) { | |
| 368 | 373 | msg := Resize{Size: domain.Size{Cols: 100, Rows: 40}} | |
| 369 | - | want := []byte{0x00, 0x64, 0x00, 0x28} | |
| 374 | + | want := []byte{0x00, 0x64, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00} | |
| 370 | 375 | ||
| 371 | 376 | got, err := MarshalResize(msg) | |
| 372 | 377 | if err != nil { | |
| @@ -522,7 +527,7 @@ func TestCommandRequestGoldenAndRoundTrip(t *testing.T) { | |||
| 522 | 527 | name: "minimal", | |
| 523 | 528 | msg: CommandRequest{Version: ProtocolVersion, Slug: "split-right"}, | |
| 524 | 529 | want: []byte{ | |
| 525 | - | 0x00, 0x22, | |
| 530 | + | 0x00, 0x23, | |
| 526 | 531 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID | |
| 527 | 532 | 0x00, // attached | |
| 528 | 533 | 0x00, // self | |
| @@ -545,7 +550,7 @@ func TestCommandRequestGoldenAndRoundTrip(t *testing.T) { | |||
| 545 | 550 | JSON: true, | |
| 546 | 551 | }, | |
| 547 | 552 | want: []byte{ | |
| 548 | - | 0x00, 0x22, | |
| 553 | + | 0x00, 0x23, | |
| 549 | 554 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID | |
| 550 | 555 | 0x00, // attached | |
| 551 | 556 | 0x01, // self | |
| @@ -943,7 +948,7 @@ func TestHelloOutputWindowByteExactValues(t *testing.T) { | |||
| 943 | 948 | t.Run(fmt.Sprintf("window_%d", window), func(t *testing.T) { | |
| 944 | 949 | hello := Hello{Version: 14, Size: domain.Size{Cols: 1, Rows: 1}, MaxOutputInFlight: window} | |
| 945 | 950 | // The empty Hello has a fixed 1x1 size before the negotiated output window. | |
| 946 | - | want := append(make([]byte, 38), window, 0, 0, 0, 0, 0, 0) | |
| 951 | + | want := append(make([]byte, 42), window, 0, 0, 0, 0, 0, 0) | |
| 947 | 952 | want = appendNoNavigationTail(want) | |
| 948 | 953 | want[1] = 14 | |
| 949 | 954 | want[30], want[32] = 1, 1 | |
| @@ -974,7 +979,7 @@ func TestCommandCorrelationGoldenAndStrict(t *testing.T) { | |||
| 974 | 979 | name: "attached", | |
| 975 | 980 | msg: CommandRequest{Version: ProtocolVersion, RequestID: 0x0102030405060708, Attached: true, Self: true, Slug: "split-right", Args: []string{"--vertical"}}, | |
| 976 | 981 | want: []byte{ | |
| 977 | - | 0x00, 0x22, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x01, | |
| 982 | + | 0x00, 0x23, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x01, | |
| 978 | 983 | 0x00, 0x0b, 's', 'p', 'l', 'i', 't', '-', 'r', 'i', 'g', 'h', 't', | |
| 979 | 984 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, '-', '-', 'v', 'e', 'r', 't', 'i', 'c', 'a', 'l', | |
| 980 | 985 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| @@ -984,7 +989,7 @@ func TestCommandCorrelationGoldenAndStrict(t *testing.T) { | |||
| 984 | 989 | name: "control", | |
| 985 | 990 | msg: CommandRequest{Version: ProtocolVersion, Slug: "ls"}, | |
| 986 | 991 | want: []byte{ | |
| 987 | - | 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 992 | + | 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 988 | 993 | 0x00, 0x02, 'l', 's', 0x00, 0x00, | |
| 989 | 994 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 990 | 995 | }, | |
| @@ -79,7 +79,7 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) { | |||||
| 79 | TrueColor: true, | 79 | TrueColor: true, | ||
| 80 | MaxOutputInFlight: 8, | 80 | MaxOutputInFlight: 8, | ||
| 81 | }, | 81 | }, | ||
| 82 | - | want: []byte{0x00, 0x01, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x02, 0x77, 0x30, 0x00, 0x50, 0x00, 0x18, 0x00, 0x0e, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x2d, 0x32, 0x35, 0x36, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x0c, 0x2f, 0x74, 0x6d, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | 82 | + | want: []byte{0x00, 0x01, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x02, 0x77, 0x30, 0x00, 0x50, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x2d, 0x32, 0x35, 0x36, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x0c, 0x2f, 0x74, 0x6d, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, |
| 83 | }, | 83 | }, | ||
| 84 | { | 84 | { | ||
| 85 | name: "empty strings", | 85 | name: "empty strings", | ||
| @@ -92,7 +92,11 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) { | |||||
| 92 | Cwd: "", | 92 | Cwd: "", | ||
| 93 | TrueColor: false, | 93 | TrueColor: false, | ||
| 94 | }, | 94 | }, | ||
| 95 | - | want: []byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | 95 | + | want: func() []byte { |
| 96 | + | out := append([]byte{0x00, 0x01, 0x00}, make([]byte, 26)...) | |||
| 97 | + | out = append(out, 0x00, 0x01, 0x00, 0x01) // cell size | |||
| 98 | + | return append(out, make([]byte, 16)...) // pixels and remaining fields | |||
| 99 | + | }(), | |||
| 96 | }, | 100 | }, | ||
| 97 | } | 101 | } | ||
| 98 | 102 | ||||
| @@ -131,12 +135,13 @@ func TestHelloEnvironmentCodec(t *testing.T) { | |||||
| 131 | } | 135 | } | ||
| 132 | got := MarshalHello(msg) | 136 | got := MarshalHello(msg) | ||
| 133 | want := []byte{ | 137 | want := []byte{ | ||
| 134 | - | 0x00, 0x22, 0x00, // version, intent, first client ID byte | 138 | + | 0x00, 0x23, 0x00, // version, intent, first client ID byte |
| 135 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | 139 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
| 136 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // client ID | 140 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // client ID | ||
| 137 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // resume token | 141 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // resume token | ||
| 138 | 0x00, 0x00, // name | 142 | 0x00, 0x00, // name | ||
| 139 | 0x00, 0x01, 0x00, 0x01, // size | 143 | 0x00, 0x01, 0x00, 0x01, // size | ||
| 144 | + | 0x00, 0x00, 0x00, 0x00, // optional pixel size | |||
| 140 | 0x00, 0x00, // TERM | 145 | 0x00, 0x00, // TERM | ||
| 141 | 0x00, 0x00, // cwd | 146 | 0x00, 0x00, // cwd | ||
| 142 | 0x00, 0x00, // true color, max output in flight | 147 | 0x00, 0x00, // true color, max output in flight | ||
| @@ -361,12 +366,12 @@ func TestThemeGenerationClearedWireGoldenPreservesProtocolVersion(t *testing.T) | |||||
| 361 | } | 366 | } | ||
| 362 | want := append([]byte{0x0f, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x00}, make([]byte, 48)...) | 367 | want := append([]byte{0x0f, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x00}, make([]byte, 48)...) | ||
| 363 | require.Equal(t, want, MarshalTheme(cleared)) | 368 | require.Equal(t, want, MarshalTheme(cleared)) | ||
| 364 | - | require.Equal(t, uint16(34), ProtocolVersion) | 369 | + | require.Equal(t, uint16(35), ProtocolVersion) |
| 365 | } | 370 | } | ||
| 366 | 371 | ||||
| 367 | func TestResizeGoldenAndRoundTrip(t *testing.T) { | 372 | func TestResizeGoldenAndRoundTrip(t *testing.T) { | ||
| 368 | msg := Resize{Size: domain.Size{Cols: 100, Rows: 40}} | 373 | msg := Resize{Size: domain.Size{Cols: 100, Rows: 40}} | ||
| 369 | - | want := []byte{0x00, 0x64, 0x00, 0x28} | 374 | + | want := []byte{0x00, 0x64, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00} |
| 370 | 375 | ||||
| 371 | got, err := MarshalResize(msg) | 376 | got, err := MarshalResize(msg) | ||
| 372 | if err != nil { | 377 | if err != nil { | ||
| @@ -522,7 +527,7 @@ func TestCommandRequestGoldenAndRoundTrip(t *testing.T) { | |||||
| 522 | name: "minimal", | 527 | name: "minimal", | ||
| 523 | msg: CommandRequest{Version: ProtocolVersion, Slug: "split-right"}, | 528 | msg: CommandRequest{Version: ProtocolVersion, Slug: "split-right"}, | ||
| 524 | want: []byte{ | 529 | want: []byte{ | ||
| 525 | - | 0x00, 0x22, | 530 | + | 0x00, 0x23, |
| 526 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID | 531 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID | ||
| 527 | 0x00, // attached | 532 | 0x00, // attached | ||
| 528 | 0x00, // self | 533 | 0x00, // self | ||
| @@ -545,7 +550,7 @@ func TestCommandRequestGoldenAndRoundTrip(t *testing.T) { | |||||
| 545 | JSON: true, | 550 | JSON: true, | ||
| 546 | }, | 551 | }, | ||
| 547 | want: []byte{ | 552 | want: []byte{ | ||
| 548 | - | 0x00, 0x22, | 553 | + | 0x00, 0x23, |
| 549 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID | 554 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID | ||
| 550 | 0x00, // attached | 555 | 0x00, // attached | ||
| 551 | 0x01, // self | 556 | 0x01, // self | ||
| @@ -943,7 +948,7 @@ func TestHelloOutputWindowByteExactValues(t *testing.T) { | |||||
| 943 | t.Run(fmt.Sprintf("window_%d", window), func(t *testing.T) { | 948 | t.Run(fmt.Sprintf("window_%d", window), func(t *testing.T) { | ||
| 944 | hello := Hello{Version: 14, Size: domain.Size{Cols: 1, Rows: 1}, MaxOutputInFlight: window} | 949 | hello := Hello{Version: 14, Size: domain.Size{Cols: 1, Rows: 1}, MaxOutputInFlight: window} | ||
| 945 | // The empty Hello has a fixed 1x1 size before the negotiated output window. | 950 | // The empty Hello has a fixed 1x1 size before the negotiated output window. | ||
| 946 | - | want := append(make([]byte, 38), window, 0, 0, 0, 0, 0, 0) | 951 | + | want := append(make([]byte, 42), window, 0, 0, 0, 0, 0, 0) |
| 947 | want = appendNoNavigationTail(want) | 952 | want = appendNoNavigationTail(want) | ||
| 948 | want[1] = 14 | 953 | want[1] = 14 | ||
| 949 | want[30], want[32] = 1, 1 | 954 | want[30], want[32] = 1, 1 | ||
| @@ -974,7 +979,7 @@ func TestCommandCorrelationGoldenAndStrict(t *testing.T) { | |||||
| 974 | name: "attached", | 979 | name: "attached", | ||
| 975 | msg: CommandRequest{Version: ProtocolVersion, RequestID: 0x0102030405060708, Attached: true, Self: true, Slug: "split-right", Args: []string{"--vertical"}}, | 980 | msg: CommandRequest{Version: ProtocolVersion, RequestID: 0x0102030405060708, Attached: true, Self: true, Slug: "split-right", Args: []string{"--vertical"}}, | ||
| 976 | want: []byte{ | 981 | want: []byte{ | ||
| 977 | - | 0x00, 0x22, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x01, | 982 | + | 0x00, 0x23, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x01, |
| 978 | 0x00, 0x0b, 's', 'p', 'l', 'i', 't', '-', 'r', 'i', 'g', 'h', 't', | 983 | 0x00, 0x0b, 's', 'p', 'l', 'i', 't', '-', 'r', 'i', 'g', 'h', 't', | ||
| 979 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, '-', '-', 'v', 'e', 'r', 't', 'i', 'c', 'a', 'l', | 984 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, '-', '-', 'v', 'e', 'r', 't', 'i', 'c', 'a', 'l', | ||
| 980 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | 985 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
| @@ -984,7 +989,7 @@ func TestCommandCorrelationGoldenAndStrict(t *testing.T) { | |||||
| 984 | name: "control", | 989 | name: "control", | ||
| 985 | msg: CommandRequest{Version: ProtocolVersion, Slug: "ls"}, | 990 | msg: CommandRequest{Version: ProtocolVersion, Slug: "ls"}, | ||
| 986 | want: []byte{ | 991 | want: []byte{ | ||
| 987 | - | 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | 992 | + | 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 988 | 0x00, 0x02, 'l', 's', 0x00, 0x00, | 993 | 0x00, 0x02, 'l', 's', 0x00, 0x00, | ||
| 989 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | 994 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
| 990 | }, | 995 | }, | ||
M internal/usecase/client/client.go +32 -25
| @@ -863,11 +863,11 @@ func (u *reconnectUI) redraw(size domain.Size) error { | |||
| 863 | 863 | func (u *reconnectUI) drawStage(stage reconnectStage) error { | |
| 864 | 864 | u.stage = stage | |
| 865 | 865 | if u.showing && u.remote { | |
| 866 | - | size, err := u.term.Size() | |
| 866 | + | geometry, err := u.term.Geometry() | |
| 867 | 867 | if err != nil { | |
| 868 | - | return fmt.Errorf("reading terminal size for reconnect toast: %w", err) | |
| 868 | + | return fmt.Errorf("reading terminal geometry for reconnect toast: %w", err) | |
| 869 | 869 | } | |
| 870 | - | return u.redraw(size) | |
| 870 | + | return u.redraw(geometry.Size) | |
| 871 | 871 | } | |
| 872 | 872 | return u.draw() | |
| 873 | 873 | } | |
| @@ -877,11 +877,11 @@ func (u *reconnectUI) draw() error { | |||
| 877 | 877 | return nil | |
| 878 | 878 | } | |
| 879 | 879 | if u.remote { | |
| 880 | - | size, err := u.term.Size() | |
| 880 | + | geometry, err := u.term.Geometry() | |
| 881 | 881 | if err != nil { | |
| 882 | - | return fmt.Errorf("reading terminal size for reconnect toast: %w", err) | |
| 882 | + | return fmt.Errorf("reading terminal geometry for reconnect toast: %w", err) | |
| 883 | 883 | } | |
| 884 | - | return u.redraw(size) | |
| 884 | + | return u.redraw(geometry.Size) | |
| 885 | 885 | } | |
| 886 | 886 | if _, err := u.term.Out().Write([]byte(statusReconnect)); err != nil { | |
| 887 | 887 | return fmt.Errorf("writing reconnect status: %w", err) | |
| @@ -927,7 +927,7 @@ func sleepReconnect(ctx context.Context, clk ports.Clock, d time.Duration) bool | |||
| 927 | 927 | } | |
| 928 | 928 | } | |
| 929 | 929 | ||
| 930 | - | func sleepReconnectWithResizeEvents(ctx context.Context, clk ports.Clock, d time.Duration, resizeEvents <-chan domain.Size, onResize func(domain.Size) error) (bool, error) { | |
| 930 | + | func sleepReconnectWithResizeEvents(ctx context.Context, clk ports.Clock, d time.Duration, resizeEvents <-chan domain.Geometry, onResize func(domain.Size) error) (bool, error) { | |
| 931 | 931 | t := clk.NewTimer(d) | |
| 932 | 932 | defer t.Stop() | |
| 933 | 933 | for { | |
| @@ -936,13 +936,13 @@ func sleepReconnectWithResizeEvents(ctx context.Context, clk ports.Clock, d time | |||
| 936 | 936 | return true, nil | |
| 937 | 937 | case <-ctx.Done(): | |
| 938 | 938 | return false, nil | |
| 939 | - | case size, ok := <-resizeEvents: | |
| 939 | + | case geometry, ok := <-resizeEvents: | |
| 940 | 940 | if !ok { | |
| 941 | 941 | resizeEvents = nil | |
| 942 | 942 | continue | |
| 943 | 943 | } | |
| 944 | 944 | if onResize != nil { | |
| 945 | - | if err := onResize(size); err != nil { | |
| 945 | + | if err := onResize(geometry.Size); err != nil { | |
| 946 | 946 | return false, err | |
| 947 | 947 | } | |
| 948 | 948 | } | |
| @@ -1083,11 +1083,13 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||
| 1083 | 1083 | } | |
| 1084 | 1084 | return nil | |
| 1085 | 1085 | } | |
| 1086 | - | // 1. Handshake: send Hello with our size and TERM. | |
| 1087 | - | size, err := term.Size() | |
| 1086 | + | // 1. Handshake: send Hello with our geometry and TERM. | |
| 1087 | + | geometry, err := term.Geometry() | |
| 1088 | 1088 | if err != nil { | |
| 1089 | - | return attachResult{err: fmt.Errorf("vev: reading terminal size: %w", err)} | |
| 1089 | + | return attachResult{err: fmt.Errorf("vev: reading terminal geometry: %w", err)} | |
| 1090 | 1090 | } | |
| 1091 | + | geometry = geometry.NormalizePixels() | |
| 1092 | + | size := geometry.Size | |
| 1091 | 1093 | cwd, err := os.Getwd() | |
| 1092 | 1094 | if err != nil { | |
| 1093 | 1095 | cwd = "" | |
| @@ -1110,6 +1112,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||
| 1110 | 1112 | ResumeToken: resumeToken, | |
| 1111 | 1113 | Name: name, | |
| 1112 | 1114 | Size: size, | |
| 1115 | + | PixelWidth: geometry.PixelWidth, | |
| 1116 | + | PixelHeight: geometry.PixelHeight, | |
| 1113 | 1117 | TermEnv: termEnv, | |
| 1114 | 1118 | Cwd: cwd, | |
| 1115 | 1119 | TrueColor: trueColor, | |
| @@ -1383,7 +1387,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||
| 1383 | 1387 | // Before the asynchronous sender starts, preserve transport ordering by | |
| 1384 | 1388 | // requesting the reset synchronously after the initial Theme publication. | |
| 1385 | 1389 | resetErr := sendHandshake(func() error { | |
| 1386 | - | payload, err := ports.MarshalResize(ports.Resize{Size: size}) | |
| 1390 | + | payload, err := ports.MarshalResize(ports.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}) | |
| 1387 | 1391 | if err != nil { | |
| 1388 | 1392 | return err | |
| 1389 | 1393 | } | |
| @@ -1504,11 +1508,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||
| 1504 | 1508 | defer clearParkedPending() | |
| 1505 | 1509 | ||
| 1506 | 1510 | sendParkedRouteSize := func() error { | |
| 1507 | - | current, err := term.Size() | |
| 1511 | + | current, err := term.Geometry() | |
| 1508 | 1512 | if err != nil { | |
| 1509 | - | return fmt.Errorf("reading terminal size for parked route: %w", err) | |
| 1513 | + | return fmt.Errorf("reading terminal geometry for parked route: %w", err) | |
| 1510 | 1514 | } | |
| 1511 | - | payload, err := ports.MarshalResize(ports.Resize{Size: current}) | |
| 1515 | + | current = current.NormalizePixels() | |
| 1516 | + | payload, err := ports.MarshalResize(ports.Resize{Size: current.Size, PixelWidth: current.PixelWidth, PixelHeight: current.PixelHeight}) | |
| 1512 | 1517 | if err != nil { | |
| 1513 | 1518 | return fmt.Errorf("encoding terminal size for parked route: %w", err) | |
| 1514 | 1519 | } | |
| @@ -1600,11 +1605,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||
| 1600 | 1605 | if awaitingReconnectReset { | |
| 1601 | 1606 | return nil | |
| 1602 | 1607 | } | |
| 1603 | - | size, serr := term.Size() | |
| 1608 | + | current, serr := term.Geometry() | |
| 1604 | 1609 | if serr != nil { | |
| 1605 | - | return fmt.Errorf("reading terminal size for reconnect reconciliation: %w", serr) | |
| 1610 | + | return fmt.Errorf("reading terminal geometry for reconnect reconciliation: %w", serr) | |
| 1606 | 1611 | } | |
| 1607 | - | payload, err := ports.MarshalResize(ports.Resize{Size: size}) | |
| 1612 | + | current = current.NormalizePixels() | |
| 1613 | + | payload, err := ports.MarshalResize(ports.Resize{Size: current.Size, PixelWidth: current.PixelWidth, PixelHeight: current.PixelHeight}) | |
| 1608 | 1614 | if err != nil { | |
| 1609 | 1615 | return fmt.Errorf("encoding terminal size for reconnect reconciliation: %w", err) | |
| 1610 | 1616 | } | |
| @@ -1792,11 +1798,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||
| 1792 | 1798 | awaitingReconnectReset = false | |
| 1793 | 1799 | } | |
| 1794 | 1800 | if reconnect.showing && reconnect.remote { | |
| 1795 | - | size, serr := term.Size() | |
| 1801 | + | current, serr := term.Geometry() | |
| 1796 | 1802 | if serr != nil { | |
| 1797 | - | return welcomedResult(fmt.Errorf("vev: reading terminal size for reconnect redraw: %w", serr)) | |
| 1803 | + | return welcomedResult(fmt.Errorf("vev: reading terminal geometry for reconnect redraw: %w", serr)) | |
| 1798 | 1804 | } | |
| 1799 | - | if rerr := reconnect.redraw(size); rerr != nil { | |
| 1805 | + | if rerr := reconnect.redraw(current.Size); rerr != nil { | |
| 1800 | 1806 | return welcomedResult(fmt.Errorf("vev: redrawing reconnect toast: %w", rerr)) | |
| 1801 | 1807 | } | |
| 1802 | 1808 | } else if ferr := term.Flush(); ferr != nil { | |
| @@ -2844,15 +2850,16 @@ func (p *stdinPump) run() { | |||
| 2844 | 2850 | // runResize forwards coalesced terminal resize events to the daemon. It | |
| 2845 | 2851 | // tolerates an already-closed resize channel (which the terminal adapter | |
| 2846 | 2852 | // hands back when restore ran before ResizeEvents was first called). | |
| 2847 | - | func runResize(ctx context.Context, events <-chan domain.Size, out chan<- ports.Frame, sendLease *foregroundSendLease, log *slog.Logger) { | |
| 2853 | + | func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- ports.Frame, sendLease *foregroundSendLease, log *slog.Logger) { | |
| 2848 | 2854 | defer log.Debug("resize pump exited") | |
| 2849 | 2855 | for { | |
| 2850 | 2856 | select { | |
| 2851 | - | case sz, ok := <-events: | |
| 2857 | + | case geometry, ok := <-events: | |
| 2852 | 2858 | if !ok { | |
| 2853 | 2859 | return | |
| 2854 | 2860 | } | |
| 2855 | - | payload, err := ports.MarshalResize(ports.Resize{Size: sz}) | |
| 2861 | + | geometry = geometry.NormalizePixels() | |
| 2862 | + | payload, err := ports.MarshalResize(ports.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}) | |
| 2856 | 2863 | if err != nil { | |
| 2857 | 2864 | log.Error("encoding terminal resize", "error", err) | |
| 2858 | 2865 | continue | |
| @@ -863,11 +863,11 @@ func (u *reconnectUI) redraw(size domain.Size) error { | |||||
| 863 | func (u *reconnectUI) drawStage(stage reconnectStage) error { | 863 | func (u *reconnectUI) drawStage(stage reconnectStage) error { | ||
| 864 | u.stage = stage | 864 | u.stage = stage | ||
| 865 | if u.showing && u.remote { | 865 | if u.showing && u.remote { | ||
| 866 | - | size, err := u.term.Size() | 866 | + | geometry, err := u.term.Geometry() |
| 867 | if err != nil { | 867 | if err != nil { | ||
| 868 | - | return fmt.Errorf("reading terminal size for reconnect toast: %w", err) | 868 | + | return fmt.Errorf("reading terminal geometry for reconnect toast: %w", err) |
| 869 | } | 869 | } | ||
| 870 | - | return u.redraw(size) | 870 | + | return u.redraw(geometry.Size) |
| 871 | } | 871 | } | ||
| 872 | return u.draw() | 872 | return u.draw() | ||
| 873 | } | 873 | } | ||
| @@ -877,11 +877,11 @@ func (u *reconnectUI) draw() error { | |||||
| 877 | return nil | 877 | return nil | ||
| 878 | } | 878 | } | ||
| 879 | if u.remote { | 879 | if u.remote { | ||
| 880 | - | size, err := u.term.Size() | 880 | + | geometry, err := u.term.Geometry() |
| 881 | if err != nil { | 881 | if err != nil { | ||
| 882 | - | return fmt.Errorf("reading terminal size for reconnect toast: %w", err) | 882 | + | return fmt.Errorf("reading terminal geometry for reconnect toast: %w", err) |
| 883 | } | 883 | } | ||
| 884 | - | return u.redraw(size) | 884 | + | return u.redraw(geometry.Size) |
| 885 | } | 885 | } | ||
| 886 | if _, err := u.term.Out().Write([]byte(statusReconnect)); err != nil { | 886 | if _, err := u.term.Out().Write([]byte(statusReconnect)); err != nil { | ||
| 887 | return fmt.Errorf("writing reconnect status: %w", err) | 887 | return fmt.Errorf("writing reconnect status: %w", err) | ||
| @@ -927,7 +927,7 @@ func sleepReconnect(ctx context.Context, clk ports.Clock, d time.Duration) bool | |||||
| 927 | } | 927 | } | ||
| 928 | } | 928 | } | ||
| 929 | 929 | ||||
| 930 | - | func sleepReconnectWithResizeEvents(ctx context.Context, clk ports.Clock, d time.Duration, resizeEvents <-chan domain.Size, onResize func(domain.Size) error) (bool, error) { | 930 | + | func sleepReconnectWithResizeEvents(ctx context.Context, clk ports.Clock, d time.Duration, resizeEvents <-chan domain.Geometry, onResize func(domain.Size) error) (bool, error) { |
| 931 | t := clk.NewTimer(d) | 931 | t := clk.NewTimer(d) | ||
| 932 | defer t.Stop() | 932 | defer t.Stop() | ||
| 933 | for { | 933 | for { | ||
| @@ -936,13 +936,13 @@ func sleepReconnectWithResizeEvents(ctx context.Context, clk ports.Clock, d time | |||||
| 936 | return true, nil | 936 | return true, nil | ||
| 937 | case <-ctx.Done(): | 937 | case <-ctx.Done(): | ||
| 938 | return false, nil | 938 | return false, nil | ||
| 939 | - | case size, ok := <-resizeEvents: | 939 | + | case geometry, ok := <-resizeEvents: |
| 940 | if !ok { | 940 | if !ok { | ||
| 941 | resizeEvents = nil | 941 | resizeEvents = nil | ||
| 942 | continue | 942 | continue | ||
| 943 | } | 943 | } | ||
| 944 | if onResize != nil { | 944 | if onResize != nil { | ||
| 945 | - | if err := onResize(size); err != nil { | 945 | + | if err := onResize(geometry.Size); err != nil { |
| 946 | return false, err | 946 | return false, err | ||
| 947 | } | 947 | } | ||
| 948 | } | 948 | } | ||
| @@ -1083,11 +1083,13 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||||
| 1083 | } | 1083 | } | ||
| 1084 | return nil | 1084 | return nil | ||
| 1085 | } | 1085 | } | ||
| 1086 | - | // 1. Handshake: send Hello with our size and TERM. | 1086 | + | // 1. Handshake: send Hello with our geometry and TERM. |
| 1087 | - | size, err := term.Size() | 1087 | + | geometry, err := term.Geometry() |
| 1088 | if err != nil { | 1088 | if err != nil { | ||
| 1089 | - | return attachResult{err: fmt.Errorf("vev: reading terminal size: %w", err)} | 1089 | + | return attachResult{err: fmt.Errorf("vev: reading terminal geometry: %w", err)} |
| 1090 | } | 1090 | } | ||
| 1091 | + | geometry = geometry.NormalizePixels() | |||
| 1092 | + | size := geometry.Size | |||
| 1091 | cwd, err := os.Getwd() | 1093 | cwd, err := os.Getwd() | ||
| 1092 | if err != nil { | 1094 | if err != nil { | ||
| 1093 | cwd = "" | 1095 | cwd = "" | ||
| @@ -1110,6 +1112,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||||
| 1110 | ResumeToken: resumeToken, | 1112 | ResumeToken: resumeToken, | ||
| 1111 | Name: name, | 1113 | Name: name, | ||
| 1112 | Size: size, | 1114 | Size: size, | ||
| 1115 | + | PixelWidth: geometry.PixelWidth, | |||
| 1116 | + | PixelHeight: geometry.PixelHeight, | |||
| 1113 | TermEnv: termEnv, | 1117 | TermEnv: termEnv, | ||
| 1114 | Cwd: cwd, | 1118 | Cwd: cwd, | ||
| 1115 | TrueColor: trueColor, | 1119 | TrueColor: trueColor, | ||
| @@ -1383,7 +1387,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||||
| 1383 | // Before the asynchronous sender starts, preserve transport ordering by | 1387 | // Before the asynchronous sender starts, preserve transport ordering by | ||
| 1384 | // requesting the reset synchronously after the initial Theme publication. | 1388 | // requesting the reset synchronously after the initial Theme publication. | ||
| 1385 | resetErr := sendHandshake(func() error { | 1389 | resetErr := sendHandshake(func() error { | ||
| 1386 | - | payload, err := ports.MarshalResize(ports.Resize{Size: size}) | 1390 | + | payload, err := ports.MarshalResize(ports.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}) |
| 1387 | if err != nil { | 1391 | if err != nil { | ||
| 1388 | return err | 1392 | return err | ||
| 1389 | } | 1393 | } | ||
| @@ -1504,11 +1508,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||||
| 1504 | defer clearParkedPending() | 1508 | defer clearParkedPending() | ||
| 1505 | 1509 | ||||
| 1506 | sendParkedRouteSize := func() error { | 1510 | sendParkedRouteSize := func() error { | ||
| 1507 | - | current, err := term.Size() | 1511 | + | current, err := term.Geometry() |
| 1508 | if err != nil { | 1512 | if err != nil { | ||
| 1509 | - | return fmt.Errorf("reading terminal size for parked route: %w", err) | 1513 | + | return fmt.Errorf("reading terminal geometry for parked route: %w", err) |
| 1510 | } | 1514 | } | ||
| 1511 | - | payload, err := ports.MarshalResize(ports.Resize{Size: current}) | 1515 | + | current = current.NormalizePixels() |
| 1516 | + | payload, err := ports.MarshalResize(ports.Resize{Size: current.Size, PixelWidth: current.PixelWidth, PixelHeight: current.PixelHeight}) | |||
| 1512 | if err != nil { | 1517 | if err != nil { | ||
| 1513 | return fmt.Errorf("encoding terminal size for parked route: %w", err) | 1518 | return fmt.Errorf("encoding terminal size for parked route: %w", err) | ||
| 1514 | } | 1519 | } | ||
| @@ -1600,11 +1605,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||||
| 1600 | if awaitingReconnectReset { | 1605 | if awaitingReconnectReset { | ||
| 1601 | return nil | 1606 | return nil | ||
| 1602 | } | 1607 | } | ||
| 1603 | - | size, serr := term.Size() | 1608 | + | current, serr := term.Geometry() |
| 1604 | if serr != nil { | 1609 | if serr != nil { | ||
| 1605 | - | return fmt.Errorf("reading terminal size for reconnect reconciliation: %w", serr) | 1610 | + | return fmt.Errorf("reading terminal geometry for reconnect reconciliation: %w", serr) |
| 1606 | } | 1611 | } | ||
| 1607 | - | payload, err := ports.MarshalResize(ports.Resize{Size: size}) | 1612 | + | current = current.NormalizePixels() |
| 1613 | + | payload, err := ports.MarshalResize(ports.Resize{Size: current.Size, PixelWidth: current.PixelWidth, PixelHeight: current.PixelHeight}) | |||
| 1608 | if err != nil { | 1614 | if err != nil { | ||
| 1609 | return fmt.Errorf("encoding terminal size for reconnect reconciliation: %w", err) | 1615 | return fmt.Errorf("encoding terminal size for reconnect reconciliation: %w", err) | ||
| 1610 | } | 1616 | } | ||
| @@ -1792,11 +1798,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult { | |||||
| 1792 | awaitingReconnectReset = false | 1798 | awaitingReconnectReset = false | ||
| 1793 | } | 1799 | } | ||
| 1794 | if reconnect.showing && reconnect.remote { | 1800 | if reconnect.showing && reconnect.remote { | ||
| 1795 | - | size, serr := term.Size() | 1801 | + | current, serr := term.Geometry() |
| 1796 | if serr != nil { | 1802 | if serr != nil { | ||
| 1797 | - | return welcomedResult(fmt.Errorf("vev: reading terminal size for reconnect redraw: %w", serr)) | 1803 | + | return welcomedResult(fmt.Errorf("vev: reading terminal geometry for reconnect redraw: %w", serr)) |
| 1798 | } | 1804 | } | ||
| 1799 | - | if rerr := reconnect.redraw(size); rerr != nil { | 1805 | + | if rerr := reconnect.redraw(current.Size); rerr != nil { |
| 1800 | return welcomedResult(fmt.Errorf("vev: redrawing reconnect toast: %w", rerr)) | 1806 | return welcomedResult(fmt.Errorf("vev: redrawing reconnect toast: %w", rerr)) | ||
| 1801 | } | 1807 | } | ||
| 1802 | } else if ferr := term.Flush(); ferr != nil { | 1808 | } else if ferr := term.Flush(); ferr != nil { | ||
| @@ -2844,15 +2850,16 @@ func (p *stdinPump) run() { | |||||
| 2844 | // runResize forwards coalesced terminal resize events to the daemon. It | 2850 | // runResize forwards coalesced terminal resize events to the daemon. It | ||
| 2845 | // tolerates an already-closed resize channel (which the terminal adapter | 2851 | // tolerates an already-closed resize channel (which the terminal adapter | ||
| 2846 | // hands back when restore ran before ResizeEvents was first called). | 2852 | // hands back when restore ran before ResizeEvents was first called). | ||
| 2847 | - | func runResize(ctx context.Context, events <-chan domain.Size, out chan<- ports.Frame, sendLease *foregroundSendLease, log *slog.Logger) { | 2853 | + | func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- ports.Frame, sendLease *foregroundSendLease, log *slog.Logger) { |
| 2848 | defer log.Debug("resize pump exited") | 2854 | defer log.Debug("resize pump exited") | ||
| 2849 | for { | 2855 | for { | ||
| 2850 | select { | 2856 | select { | ||
| 2851 | - | case sz, ok := <-events: | 2857 | + | case geometry, ok := <-events: |
| 2852 | if !ok { | 2858 | if !ok { | ||
| 2853 | return | 2859 | return | ||
| 2854 | } | 2860 | } | ||
| 2855 | - | payload, err := ports.MarshalResize(ports.Resize{Size: sz}) | 2861 | + | geometry = geometry.NormalizePixels() |
| 2862 | + | payload, err := ports.MarshalResize(ports.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}) | |||
| 2856 | if err != nil { | 2863 | if err != nil { | ||
| 2857 | log.Error("encoding terminal resize", "error", err) | 2864 | log.Error("encoding terminal resize", "error", err) | ||
| 2858 | continue | 2865 | continue | ||
M internal/usecase/client/client_test.go +44 -43
| @@ -218,9 +218,9 @@ func (r *chunkedBlockingReader) Read(p []byte) (int, error) { | |||
| 218 | 218 | ||
| 219 | 219 | func (r *chunkedBlockingReader) unblock() { close(r.done) } | |
| 220 | 220 | ||
| 221 | - | func newHappyTerminal(t *testing.T, out *bytes.Buffer, restoreCount *atomic.Int32, resizeCh chan domain.Size) (*portsmocks.MockTerminal, *blockingReader) { | |
| 221 | + | func newHappyTerminal(t *testing.T, out *bytes.Buffer, restoreCount *atomic.Int32, resizeCh chan domain.Geometry) (*portsmocks.MockTerminal, *blockingReader) { | |
| 222 | 222 | tm := portsmocks.NewMockTerminal(t) | |
| 223 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | |
| 223 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() | |
| 224 | 224 | tm.EXPECT().EnterRaw().Return(func() error { | |
| 225 | 225 | restoreCount.Add(1) | |
| 226 | 226 | return nil | |
| @@ -320,7 +320,7 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) { | |||
| 320 | 320 | clk, createdTimers := newHandshakeClock(t, 1) | |
| 321 | 321 | ||
| 322 | 322 | term := portsmocks.NewMockTerminal(t) | |
| 323 | - | term.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | |
| 323 | + | term.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() | |
| 324 | 324 | // No EnterRaw expectation: it must not run before Welcome. | |
| 325 | 325 | ||
| 326 | 326 | started := make(chan struct{}) | |
| @@ -393,7 +393,7 @@ func TestAttachHelloIncludesTrueColor(t *testing.T) { | |||
| 393 | 393 | ||
| 394 | 394 | var out bytes.Buffer | |
| 395 | 395 | var restoreCount atomic.Int32 | |
| 396 | - | resizeCh := make(chan domain.Size) | |
| 396 | + | resizeCh := make(chan domain.Geometry) | |
| 397 | 397 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | |
| 398 | 398 | defer in.unblock() | |
| 399 | 399 | ||
| @@ -1123,7 +1123,7 @@ func mustMarshalRoutePosition(position ports.RoutePosition) []byte { | |||
| 1123 | 1123 | func TestAttachTargetHandoffReturnsValidatedTargetAndClosesTransport(t *testing.T) { | |
| 1124 | 1124 | var out bytes.Buffer | |
| 1125 | 1125 | var restoreCount atomic.Int32 | |
| 1126 | - | resizeCh := make(chan domain.Size) | |
| 1126 | + | resizeCh := make(chan domain.Geometry) | |
| 1127 | 1127 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | |
| 1128 | 1128 | defer in.unblock() | |
| 1129 | 1129 | ||
| @@ -1150,7 +1150,7 @@ func TestAttachHelloIncludesCompleteLocalEnvironment(t *testing.T) { | |||
| 1150 | 1150 | ||
| 1151 | 1151 | var out bytes.Buffer | |
| 1152 | 1152 | var restoreCount atomic.Int32 | |
| 1153 | - | resizeCh := make(chan domain.Size) | |
| 1153 | + | resizeCh := make(chan domain.Geometry) | |
| 1154 | 1154 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | |
| 1155 | 1155 | defer in.unblock() | |
| 1156 | 1156 | ||
| @@ -1180,7 +1180,7 @@ func TestAttachHelloIncludesCompleteLocalEnvironment(t *testing.T) { | |||
| 1180 | 1180 | func TestAttachHelloRequestsSingleOutputForDatagramTransport(t *testing.T) { | |
| 1181 | 1181 | var out bytes.Buffer | |
| 1182 | 1182 | var restoreCount atomic.Int32 | |
| 1183 | - | resizeCh := make(chan domain.Size) | |
| 1183 | + | resizeCh := make(chan domain.Geometry) | |
| 1184 | 1184 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | |
| 1185 | 1185 | defer in.unblock() | |
| 1186 | 1186 | ||
| @@ -1206,7 +1206,7 @@ func TestAttachHelloRequestsSingleOutputForDatagramTransport(t *testing.T) { | |||
| 1206 | 1206 | func TestAttachHappyPath(t *testing.T) { | |
| 1207 | 1207 | var out bytes.Buffer | |
| 1208 | 1208 | var restoreCount atomic.Int32 | |
| 1209 | - | resizeCh := make(chan domain.Size) | |
| 1209 | + | resizeCh := make(chan domain.Geometry) | |
| 1210 | 1210 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | |
| 1211 | 1211 | defer in.unblock() | |
| 1212 | 1212 | ||
| @@ -1230,7 +1230,7 @@ func TestAttachHappyPath(t *testing.T) { | |||
| 1230 | 1230 | ||
| 1231 | 1231 | func TestAttachVersionMismatch(t *testing.T) { | |
| 1232 | 1232 | tm := portsmocks.NewMockTerminal(t) | |
| 1233 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | |
| 1233 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() | |
| 1234 | 1234 | // EnterRaw must NOT be called on the error-before-welcome path. | |
| 1235 | 1235 | ||
| 1236 | 1236 | tr := portsmocks.NewMockTransport(t) | |
| @@ -1252,7 +1252,7 @@ func TestAttachVersionMismatch(t *testing.T) { | |||
| 1252 | 1252 | func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) { | |
| 1253 | 1253 | var out bytes.Buffer | |
| 1254 | 1254 | var restoreCount atomic.Int32 | |
| 1255 | - | resizeCh := make(chan domain.Size) | |
| 1255 | + | resizeCh := make(chan domain.Geometry) | |
| 1256 | 1256 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | |
| 1257 | 1257 | defer in.unblock() | |
| 1258 | 1258 | ||
| @@ -1276,7 +1276,7 @@ func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) { | |||
| 1276 | 1276 | func TestAttachDaemonVanishedOnEOF(t *testing.T) { | |
| 1277 | 1277 | var out bytes.Buffer | |
| 1278 | 1278 | var restoreCount atomic.Int32 | |
| 1279 | - | resizeCh := make(chan domain.Size) | |
| 1279 | + | resizeCh := make(chan domain.Geometry) | |
| 1280 | 1280 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | |
| 1281 | 1281 | defer in.unblock() | |
| 1282 | 1282 | ||
| @@ -1300,12 +1300,12 @@ func TestAttachDaemonVanishedOnEOF(t *testing.T) { | |||
| 1300 | 1300 | func TestAttachStdinForwardsSGRMouseReportAsSingleFrame(t *testing.T) { | |
| 1301 | 1301 | var out bytes.Buffer | |
| 1302 | 1302 | var restoreCount atomic.Int32 | |
| 1303 | - | resizeCh := make(chan domain.Size) | |
| 1303 | + | resizeCh := make(chan domain.Geometry) | |
| 1304 | 1304 | input := newOneShotBlockingReader([]byte("\x1b[<0;1;1M")) | |
| 1305 | 1305 | defer input.unblock() | |
| 1306 | 1306 | ||
| 1307 | 1307 | tm := portsmocks.NewMockTerminal(t) | |
| 1308 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | |
| 1308 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() | |
| 1309 | 1309 | tm.EXPECT().EnterRaw().Return(func() error { restoreCount.Add(1); return nil }, nil).Once() | |
| 1310 | 1310 | tm.EXPECT().In().Return(input).Maybe() | |
| 1311 | 1311 | tm.EXPECT().Out().Return(&out).Maybe() | |
| @@ -1361,13 +1361,13 @@ func TestAttachStdinForwardsSGRMouseReportAsSingleFrame(t *testing.T) { | |||
| 1361 | 1361 | func TestAttachStdinCoalescesSplitBracketedPaste(t *testing.T) { | |
| 1362 | 1362 | var out bytes.Buffer | |
| 1363 | 1363 | var restoreCount atomic.Int32 | |
| 1364 | - | resizeCh := make(chan domain.Size) | |
| 1364 | + | resizeCh := make(chan domain.Geometry) | |
| 1365 | 1365 | paste := []byte("\x1b[200~hello\nworld\x1b[201~") | |
| 1366 | 1366 | input := newChunkedBlockingReader([]byte("\x1b[200~hello\n"), []byte("world\x1b[201~")) | |
| 1367 | 1367 | defer input.unblock() | |
| 1368 | 1368 | ||
| 1369 | 1369 | tm := portsmocks.NewMockTerminal(t) | |
| 1370 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | |
| 1370 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() | |
| 1371 | 1371 | tm.EXPECT().EnterRaw().Return(func() error { restoreCount.Add(1); return nil }, nil).Once() | |
| 1372 | 1372 | tm.EXPECT().In().Return(input).Maybe() | |
| 1373 | 1373 | tm.EXPECT().Out().Return(&out).Maybe() | |
| @@ -1423,7 +1423,7 @@ func TestAttachStdinCoalescesSplitBracketedPaste(t *testing.T) { | |||
| 1423 | 1423 | func TestAttachForwardsResize(t *testing.T) { | |
| 1424 | 1424 | var out bytes.Buffer | |
| 1425 | 1425 | var restoreCount atomic.Int32 | |
| 1426 | - | resizeCh := make(chan domain.Size) | |
| 1426 | + | resizeCh := make(chan domain.Geometry) | |
| 1427 | 1427 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | |
| 1428 | 1428 | defer in.unblock() | |
| 1429 | 1429 | ||
| @@ -1472,7 +1472,7 @@ func TestAttachForwardsResize(t *testing.T) { | |||
| 1472 | 1472 | // Push a resize event once attach begins receiving daemon frames. | |
| 1473 | 1473 | go func() { | |
| 1474 | 1474 | <-firstRecv | |
| 1475 | - | resizeCh <- domain.Size{Cols: 120, Rows: 40} | |
| 1475 | + | resizeCh <- domain.Geometry{Size: domain.Size{Cols: 120, Rows: 40}} | |
| 1476 | 1476 | }() | |
| 1477 | 1477 | ||
| 1478 | 1478 | err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""}) | |
| @@ -1488,7 +1488,7 @@ func TestAttachForwardsResize(t *testing.T) { | |||
| 1488 | 1488 | func TestRunRestoresRawModeAfterAttachError(t *testing.T) { | |
| 1489 | 1489 | var out bytes.Buffer | |
| 1490 | 1490 | var restoreCount atomic.Int32 | |
| 1491 | - | resizeCh := make(chan domain.Size) | |
| 1491 | + | resizeCh := make(chan domain.Geometry) | |
| 1492 | 1492 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | |
| 1493 | 1493 | defer in.unblock() | |
| 1494 | 1494 | ||
| @@ -1513,7 +1513,7 @@ func TestRunRestoresRawModeAfterAttachError(t *testing.T) { | |||
| 1513 | 1513 | ||
| 1514 | 1514 | func TestRunDoesNotEnterRawBeforePreWelcomeError(t *testing.T) { | |
| 1515 | 1515 | tm := portsmocks.NewMockTerminal(t) | |
| 1516 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | |
| 1516 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() | |
| 1517 | 1517 | // EnterRaw must NOT be called when the daemon rejects Hello before Welcome. | |
| 1518 | 1518 | ||
| 1519 | 1519 | tr := portsmocks.NewMockTransport(t) | |
| @@ -1615,15 +1615,16 @@ type runTerminal struct { | |||
| 1615 | 1615 | out bytes.Buffer | |
| 1616 | 1616 | rawCount atomic.Int32 | |
| 1617 | 1617 | restoreCount atomic.Int32 | |
| 1618 | - | resizeCh chan domain.Size | |
| 1619 | - | sizeMu sync.Mutex | |
| 1620 | - | size domain.Size | |
| 1618 | + | resizeCh chan domain.Geometry | |
| 1619 | + | geometryMu sync.Mutex | |
| 1620 | + | geometry domain.Geometry | |
| 1621 | 1621 | } | |
| 1622 | 1622 | ||
| 1623 | 1623 | func newRunTerminal() *runTerminal { | |
| 1624 | 1624 | return &runTerminal{ | |
| 1625 | - | in: newBlockingReader(), resizeCh: make(chan domain.Size), | |
| 1626 | - | size: domain.Size{Cols: 80, Rows: 24}, | |
| 1625 | + | in: newBlockingReader(), | |
| 1626 | + | resizeCh: make(chan domain.Geometry), | |
| 1627 | + | geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, | |
| 1627 | 1628 | } | |
| 1628 | 1629 | } | |
| 1629 | 1630 | ||
| @@ -1631,20 +1632,20 @@ func (t *runTerminal) EnterRaw() (func() error, error) { | |||
| 1631 | 1632 | t.rawCount.Add(1) | |
| 1632 | 1633 | return func() error { t.restoreCount.Add(1); return nil }, nil | |
| 1633 | 1634 | } | |
| 1634 | - | func (t *runTerminal) Size() (domain.Size, error) { | |
| 1635 | - | t.sizeMu.Lock() | |
| 1636 | - | defer t.sizeMu.Unlock() | |
| 1637 | - | return t.size, nil | |
| 1635 | + | func (t *runTerminal) Geometry() (domain.Geometry, error) { | |
| 1636 | + | t.geometryMu.Lock() | |
| 1637 | + | defer t.geometryMu.Unlock() | |
| 1638 | + | return t.geometry, nil | |
| 1638 | 1639 | } | |
| 1639 | 1640 | func (t *runTerminal) setSize(size domain.Size) { | |
| 1640 | - | t.sizeMu.Lock() | |
| 1641 | - | t.size = size | |
| 1642 | - | t.sizeMu.Unlock() | |
| 1641 | + | t.geometryMu.Lock() | |
| 1642 | + | t.geometry.Size = size | |
| 1643 | + | t.geometryMu.Unlock() | |
| 1643 | 1644 | } | |
| 1644 | - | func (t *runTerminal) ResizeEvents() <-chan domain.Size { return t.resizeCh } | |
| 1645 | - | func (t *runTerminal) In() io.Reader { return t.in } | |
| 1646 | - | func (t *runTerminal) Out() io.Writer { return &t.out } | |
| 1647 | - | func (t *runTerminal) Flush() error { return nil } | |
| 1645 | + | func (t *runTerminal) ResizeEvents() <-chan domain.Geometry { return t.resizeCh } | |
| 1646 | + | func (t *runTerminal) In() io.Reader { return t.in } | |
| 1647 | + | func (t *runTerminal) Out() io.Writer { return &t.out } | |
| 1648 | + | func (t *runTerminal) Flush() error { return nil } | |
| 1648 | 1649 | ||
| 1649 | 1650 | func helloFromSend(t *testing.T, tr *recordingTransport) ports.Hello { | |
| 1650 | 1651 | t.Helper() | |
| @@ -1871,12 +1872,12 @@ func TestAttachRememberRemoteHost(t *testing.T) { | |||
| 1871 | 1872 | var unblockIn func() | |
| 1872 | 1873 | if tt.preWelcomeReject { | |
| 1873 | 1874 | tm = portsmocks.NewMockTerminal(t) | |
| 1874 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | |
| 1875 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() | |
| 1875 | 1876 | // EnterRaw must NOT be called when the daemon rejects Hello before Welcome. | |
| 1876 | 1877 | } else { | |
| 1877 | 1878 | var out bytes.Buffer | |
| 1878 | 1879 | var restoreCount atomic.Int32 | |
| 1879 | - | resizeCh := make(chan domain.Size) | |
| 1880 | + | resizeCh := make(chan domain.Geometry) | |
| 1880 | 1881 | var in *blockingReader | |
| 1881 | 1882 | tm, in = newHappyTerminal(t, &out, &restoreCount, resizeCh) | |
| 1882 | 1883 | unblockIn = in.unblock | |
| @@ -1971,9 +1972,9 @@ func TestRunRememberRemoteHostAtMostOnceAcrossReconnects(t *testing.T) { | |||
| 1971 | 1972 | func TestAttachRememberRemoteHostDoesNotBlockAfterWelcome(t *testing.T) { | |
| 1972 | 1973 | var out bytes.Buffer | |
| 1973 | 1974 | var restoreCount atomic.Int32 | |
| 1974 | - | resizeCh := make(chan domain.Size) | |
| 1975 | + | resizeCh := make(chan domain.Geometry) | |
| 1975 | 1976 | tm := portsmocks.NewMockTerminal(t) | |
| 1976 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | |
| 1977 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() | |
| 1977 | 1978 | enteredRaw := make(chan struct{}) | |
| 1978 | 1979 | tm.EXPECT().EnterRaw().Run(func() { close(enteredRaw) }).Return(func() error { | |
| 1979 | 1980 | restoreCount.Add(1) | |
| @@ -2052,9 +2053,9 @@ func TestRunRestoresTerminalBeforeWaitingForLearner(t *testing.T) { | |||
| 2052 | 2053 | var out bytes.Buffer | |
| 2053 | 2054 | var restoreCount atomic.Int32 | |
| 2054 | 2055 | restored := make(chan struct{}) | |
| 2055 | - | resizeCh := make(chan domain.Size) | |
| 2056 | + | resizeCh := make(chan domain.Geometry) | |
| 2056 | 2057 | tm := portsmocks.NewMockTerminal(t) | |
| 2057 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | |
| 2058 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() | |
| 2058 | 2059 | tm.EXPECT().EnterRaw().Return(func() error { | |
| 2059 | 2060 | restoreCount.Add(1) | |
| 2060 | 2061 | close(restored) | |
| @@ -2134,9 +2135,9 @@ func TestRunReturnsWhenRemoteHostLearnerStalls(t *testing.T) { | |||
| 2134 | 2135 | var out bytes.Buffer | |
| 2135 | 2136 | var restoreCount atomic.Int32 | |
| 2136 | 2137 | restored := make(chan struct{}) | |
| 2137 | - | resizeCh := make(chan domain.Size) | |
| 2138 | + | resizeCh := make(chan domain.Geometry) | |
| 2138 | 2139 | tm := portsmocks.NewMockTerminal(t) | |
| 2139 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | |
| 2140 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() | |
| 2140 | 2141 | tm.EXPECT().EnterRaw().Return(func() error { | |
| 2141 | 2142 | restoreCount.Add(1) | |
| 2142 | 2143 | close(restored) | |
| @@ -218,9 +218,9 @@ func (r *chunkedBlockingReader) Read(p []byte) (int, error) { | |||||
| 218 | 218 | ||||
| 219 | func (r *chunkedBlockingReader) unblock() { close(r.done) } | 219 | func (r *chunkedBlockingReader) unblock() { close(r.done) } | ||
| 220 | 220 | ||||
| 221 | - | func newHappyTerminal(t *testing.T, out *bytes.Buffer, restoreCount *atomic.Int32, resizeCh chan domain.Size) (*portsmocks.MockTerminal, *blockingReader) { | 221 | + | func newHappyTerminal(t *testing.T, out *bytes.Buffer, restoreCount *atomic.Int32, resizeCh chan domain.Geometry) (*portsmocks.MockTerminal, *blockingReader) { |
| 222 | tm := portsmocks.NewMockTerminal(t) | 222 | tm := portsmocks.NewMockTerminal(t) | ||
| 223 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | 223 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() |
| 224 | tm.EXPECT().EnterRaw().Return(func() error { | 224 | tm.EXPECT().EnterRaw().Return(func() error { | ||
| 225 | restoreCount.Add(1) | 225 | restoreCount.Add(1) | ||
| 226 | return nil | 226 | return nil | ||
| @@ -320,7 +320,7 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) { | |||||
| 320 | clk, createdTimers := newHandshakeClock(t, 1) | 320 | clk, createdTimers := newHandshakeClock(t, 1) | ||
| 321 | 321 | ||||
| 322 | term := portsmocks.NewMockTerminal(t) | 322 | term := portsmocks.NewMockTerminal(t) | ||
| 323 | - | term.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | 323 | + | term.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() |
| 324 | // No EnterRaw expectation: it must not run before Welcome. | 324 | // No EnterRaw expectation: it must not run before Welcome. | ||
| 325 | 325 | ||||
| 326 | started := make(chan struct{}) | 326 | started := make(chan struct{}) | ||
| @@ -393,7 +393,7 @@ func TestAttachHelloIncludesTrueColor(t *testing.T) { | |||||
| 393 | 393 | ||||
| 394 | var out bytes.Buffer | 394 | var out bytes.Buffer | ||
| 395 | var restoreCount atomic.Int32 | 395 | var restoreCount atomic.Int32 | ||
| 396 | - | resizeCh := make(chan domain.Size) | 396 | + | resizeCh := make(chan domain.Geometry) |
| 397 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | 397 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | ||
| 398 | defer in.unblock() | 398 | defer in.unblock() | ||
| 399 | 399 | ||||
| @@ -1123,7 +1123,7 @@ func mustMarshalRoutePosition(position ports.RoutePosition) []byte { | |||||
| 1123 | func TestAttachTargetHandoffReturnsValidatedTargetAndClosesTransport(t *testing.T) { | 1123 | func TestAttachTargetHandoffReturnsValidatedTargetAndClosesTransport(t *testing.T) { | ||
| 1124 | var out bytes.Buffer | 1124 | var out bytes.Buffer | ||
| 1125 | var restoreCount atomic.Int32 | 1125 | var restoreCount atomic.Int32 | ||
| 1126 | - | resizeCh := make(chan domain.Size) | 1126 | + | resizeCh := make(chan domain.Geometry) |
| 1127 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | 1127 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | ||
| 1128 | defer in.unblock() | 1128 | defer in.unblock() | ||
| 1129 | 1129 | ||||
| @@ -1150,7 +1150,7 @@ func TestAttachHelloIncludesCompleteLocalEnvironment(t *testing.T) { | |||||
| 1150 | 1150 | ||||
| 1151 | var out bytes.Buffer | 1151 | var out bytes.Buffer | ||
| 1152 | var restoreCount atomic.Int32 | 1152 | var restoreCount atomic.Int32 | ||
| 1153 | - | resizeCh := make(chan domain.Size) | 1153 | + | resizeCh := make(chan domain.Geometry) |
| 1154 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | 1154 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | ||
| 1155 | defer in.unblock() | 1155 | defer in.unblock() | ||
| 1156 | 1156 | ||||
| @@ -1180,7 +1180,7 @@ func TestAttachHelloIncludesCompleteLocalEnvironment(t *testing.T) { | |||||
| 1180 | func TestAttachHelloRequestsSingleOutputForDatagramTransport(t *testing.T) { | 1180 | func TestAttachHelloRequestsSingleOutputForDatagramTransport(t *testing.T) { | ||
| 1181 | var out bytes.Buffer | 1181 | var out bytes.Buffer | ||
| 1182 | var restoreCount atomic.Int32 | 1182 | var restoreCount atomic.Int32 | ||
| 1183 | - | resizeCh := make(chan domain.Size) | 1183 | + | resizeCh := make(chan domain.Geometry) |
| 1184 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | 1184 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | ||
| 1185 | defer in.unblock() | 1185 | defer in.unblock() | ||
| 1186 | 1186 | ||||
| @@ -1206,7 +1206,7 @@ func TestAttachHelloRequestsSingleOutputForDatagramTransport(t *testing.T) { | |||||
| 1206 | func TestAttachHappyPath(t *testing.T) { | 1206 | func TestAttachHappyPath(t *testing.T) { | ||
| 1207 | var out bytes.Buffer | 1207 | var out bytes.Buffer | ||
| 1208 | var restoreCount atomic.Int32 | 1208 | var restoreCount atomic.Int32 | ||
| 1209 | - | resizeCh := make(chan domain.Size) | 1209 | + | resizeCh := make(chan domain.Geometry) |
| 1210 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | 1210 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | ||
| 1211 | defer in.unblock() | 1211 | defer in.unblock() | ||
| 1212 | 1212 | ||||
| @@ -1230,7 +1230,7 @@ func TestAttachHappyPath(t *testing.T) { | |||||
| 1230 | 1230 | ||||
| 1231 | func TestAttachVersionMismatch(t *testing.T) { | 1231 | func TestAttachVersionMismatch(t *testing.T) { | ||
| 1232 | tm := portsmocks.NewMockTerminal(t) | 1232 | tm := portsmocks.NewMockTerminal(t) | ||
| 1233 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | 1233 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() |
| 1234 | // EnterRaw must NOT be called on the error-before-welcome path. | 1234 | // EnterRaw must NOT be called on the error-before-welcome path. | ||
| 1235 | 1235 | ||||
| 1236 | tr := portsmocks.NewMockTransport(t) | 1236 | tr := portsmocks.NewMockTransport(t) | ||
| @@ -1252,7 +1252,7 @@ func TestAttachVersionMismatch(t *testing.T) { | |||||
| 1252 | func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) { | 1252 | func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) { | ||
| 1253 | var out bytes.Buffer | 1253 | var out bytes.Buffer | ||
| 1254 | var restoreCount atomic.Int32 | 1254 | var restoreCount atomic.Int32 | ||
| 1255 | - | resizeCh := make(chan domain.Size) | 1255 | + | resizeCh := make(chan domain.Geometry) |
| 1256 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | 1256 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | ||
| 1257 | defer in.unblock() | 1257 | defer in.unblock() | ||
| 1258 | 1258 | ||||
| @@ -1276,7 +1276,7 @@ func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) { | |||||
| 1276 | func TestAttachDaemonVanishedOnEOF(t *testing.T) { | 1276 | func TestAttachDaemonVanishedOnEOF(t *testing.T) { | ||
| 1277 | var out bytes.Buffer | 1277 | var out bytes.Buffer | ||
| 1278 | var restoreCount atomic.Int32 | 1278 | var restoreCount atomic.Int32 | ||
| 1279 | - | resizeCh := make(chan domain.Size) | 1279 | + | resizeCh := make(chan domain.Geometry) |
| 1280 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | 1280 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | ||
| 1281 | defer in.unblock() | 1281 | defer in.unblock() | ||
| 1282 | 1282 | ||||
| @@ -1300,12 +1300,12 @@ func TestAttachDaemonVanishedOnEOF(t *testing.T) { | |||||
| 1300 | func TestAttachStdinForwardsSGRMouseReportAsSingleFrame(t *testing.T) { | 1300 | func TestAttachStdinForwardsSGRMouseReportAsSingleFrame(t *testing.T) { | ||
| 1301 | var out bytes.Buffer | 1301 | var out bytes.Buffer | ||
| 1302 | var restoreCount atomic.Int32 | 1302 | var restoreCount atomic.Int32 | ||
| 1303 | - | resizeCh := make(chan domain.Size) | 1303 | + | resizeCh := make(chan domain.Geometry) |
| 1304 | input := newOneShotBlockingReader([]byte("\x1b[<0;1;1M")) | 1304 | input := newOneShotBlockingReader([]byte("\x1b[<0;1;1M")) | ||
| 1305 | defer input.unblock() | 1305 | defer input.unblock() | ||
| 1306 | 1306 | ||||
| 1307 | tm := portsmocks.NewMockTerminal(t) | 1307 | tm := portsmocks.NewMockTerminal(t) | ||
| 1308 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | 1308 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() |
| 1309 | tm.EXPECT().EnterRaw().Return(func() error { restoreCount.Add(1); return nil }, nil).Once() | 1309 | tm.EXPECT().EnterRaw().Return(func() error { restoreCount.Add(1); return nil }, nil).Once() | ||
| 1310 | tm.EXPECT().In().Return(input).Maybe() | 1310 | tm.EXPECT().In().Return(input).Maybe() | ||
| 1311 | tm.EXPECT().Out().Return(&out).Maybe() | 1311 | tm.EXPECT().Out().Return(&out).Maybe() | ||
| @@ -1361,13 +1361,13 @@ func TestAttachStdinForwardsSGRMouseReportAsSingleFrame(t *testing.T) { | |||||
| 1361 | func TestAttachStdinCoalescesSplitBracketedPaste(t *testing.T) { | 1361 | func TestAttachStdinCoalescesSplitBracketedPaste(t *testing.T) { | ||
| 1362 | var out bytes.Buffer | 1362 | var out bytes.Buffer | ||
| 1363 | var restoreCount atomic.Int32 | 1363 | var restoreCount atomic.Int32 | ||
| 1364 | - | resizeCh := make(chan domain.Size) | 1364 | + | resizeCh := make(chan domain.Geometry) |
| 1365 | paste := []byte("\x1b[200~hello\nworld\x1b[201~") | 1365 | paste := []byte("\x1b[200~hello\nworld\x1b[201~") | ||
| 1366 | input := newChunkedBlockingReader([]byte("\x1b[200~hello\n"), []byte("world\x1b[201~")) | 1366 | input := newChunkedBlockingReader([]byte("\x1b[200~hello\n"), []byte("world\x1b[201~")) | ||
| 1367 | defer input.unblock() | 1367 | defer input.unblock() | ||
| 1368 | 1368 | ||||
| 1369 | tm := portsmocks.NewMockTerminal(t) | 1369 | tm := portsmocks.NewMockTerminal(t) | ||
| 1370 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | 1370 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() |
| 1371 | tm.EXPECT().EnterRaw().Return(func() error { restoreCount.Add(1); return nil }, nil).Once() | 1371 | tm.EXPECT().EnterRaw().Return(func() error { restoreCount.Add(1); return nil }, nil).Once() | ||
| 1372 | tm.EXPECT().In().Return(input).Maybe() | 1372 | tm.EXPECT().In().Return(input).Maybe() | ||
| 1373 | tm.EXPECT().Out().Return(&out).Maybe() | 1373 | tm.EXPECT().Out().Return(&out).Maybe() | ||
| @@ -1423,7 +1423,7 @@ func TestAttachStdinCoalescesSplitBracketedPaste(t *testing.T) { | |||||
| 1423 | func TestAttachForwardsResize(t *testing.T) { | 1423 | func TestAttachForwardsResize(t *testing.T) { | ||
| 1424 | var out bytes.Buffer | 1424 | var out bytes.Buffer | ||
| 1425 | var restoreCount atomic.Int32 | 1425 | var restoreCount atomic.Int32 | ||
| 1426 | - | resizeCh := make(chan domain.Size) | 1426 | + | resizeCh := make(chan domain.Geometry) |
| 1427 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | 1427 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | ||
| 1428 | defer in.unblock() | 1428 | defer in.unblock() | ||
| 1429 | 1429 | ||||
| @@ -1472,7 +1472,7 @@ func TestAttachForwardsResize(t *testing.T) { | |||||
| 1472 | // Push a resize event once attach begins receiving daemon frames. | 1472 | // Push a resize event once attach begins receiving daemon frames. | ||
| 1473 | go func() { | 1473 | go func() { | ||
| 1474 | <-firstRecv | 1474 | <-firstRecv | ||
| 1475 | - | resizeCh <- domain.Size{Cols: 120, Rows: 40} | 1475 | + | resizeCh <- domain.Geometry{Size: domain.Size{Cols: 120, Rows: 40}} |
| 1476 | }() | 1476 | }() | ||
| 1477 | 1477 | ||||
| 1478 | err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""}) | 1478 | err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""}) | ||
| @@ -1488,7 +1488,7 @@ func TestAttachForwardsResize(t *testing.T) { | |||||
| 1488 | func TestRunRestoresRawModeAfterAttachError(t *testing.T) { | 1488 | func TestRunRestoresRawModeAfterAttachError(t *testing.T) { | ||
| 1489 | var out bytes.Buffer | 1489 | var out bytes.Buffer | ||
| 1490 | var restoreCount atomic.Int32 | 1490 | var restoreCount atomic.Int32 | ||
| 1491 | - | resizeCh := make(chan domain.Size) | 1491 | + | resizeCh := make(chan domain.Geometry) |
| 1492 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | 1492 | tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh) | ||
| 1493 | defer in.unblock() | 1493 | defer in.unblock() | ||
| 1494 | 1494 | ||||
| @@ -1513,7 +1513,7 @@ func TestRunRestoresRawModeAfterAttachError(t *testing.T) { | |||||
| 1513 | 1513 | ||||
| 1514 | func TestRunDoesNotEnterRawBeforePreWelcomeError(t *testing.T) { | 1514 | func TestRunDoesNotEnterRawBeforePreWelcomeError(t *testing.T) { | ||
| 1515 | tm := portsmocks.NewMockTerminal(t) | 1515 | tm := portsmocks.NewMockTerminal(t) | ||
| 1516 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | 1516 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() |
| 1517 | // EnterRaw must NOT be called when the daemon rejects Hello before Welcome. | 1517 | // EnterRaw must NOT be called when the daemon rejects Hello before Welcome. | ||
| 1518 | 1518 | ||||
| 1519 | tr := portsmocks.NewMockTransport(t) | 1519 | tr := portsmocks.NewMockTransport(t) | ||
| @@ -1615,15 +1615,16 @@ type runTerminal struct { | |||||
| 1615 | out bytes.Buffer | 1615 | out bytes.Buffer | ||
| 1616 | rawCount atomic.Int32 | 1616 | rawCount atomic.Int32 | ||
| 1617 | restoreCount atomic.Int32 | 1617 | restoreCount atomic.Int32 | ||
| 1618 | - | resizeCh chan domain.Size | 1618 | + | resizeCh chan domain.Geometry |
| 1619 | - | sizeMu sync.Mutex | 1619 | + | geometryMu sync.Mutex |
| 1620 | - | size domain.Size | 1620 | + | geometry domain.Geometry |
| 1621 | } | 1621 | } | ||
| 1622 | 1622 | ||||
| 1623 | func newRunTerminal() *runTerminal { | 1623 | func newRunTerminal() *runTerminal { | ||
| 1624 | return &runTerminal{ | 1624 | return &runTerminal{ | ||
| 1625 | - | in: newBlockingReader(), resizeCh: make(chan domain.Size), | 1625 | + | in: newBlockingReader(), |
| 1626 | - | size: domain.Size{Cols: 80, Rows: 24}, | 1626 | + | resizeCh: make(chan domain.Geometry), |
| 1627 | + | geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, | |||
| 1627 | } | 1628 | } | ||
| 1628 | } | 1629 | } | ||
| 1629 | 1630 | ||||
| @@ -1631,20 +1632,20 @@ func (t *runTerminal) EnterRaw() (func() error, error) { | |||||
| 1631 | t.rawCount.Add(1) | 1632 | t.rawCount.Add(1) | ||
| 1632 | return func() error { t.restoreCount.Add(1); return nil }, nil | 1633 | return func() error { t.restoreCount.Add(1); return nil }, nil | ||
| 1633 | } | 1634 | } | ||
| 1634 | - | func (t *runTerminal) Size() (domain.Size, error) { | 1635 | + | func (t *runTerminal) Geometry() (domain.Geometry, error) { |
| 1635 | - | t.sizeMu.Lock() | 1636 | + | t.geometryMu.Lock() |
| 1636 | - | defer t.sizeMu.Unlock() | 1637 | + | defer t.geometryMu.Unlock() |
| 1637 | - | return t.size, nil | 1638 | + | return t.geometry, nil |
| 1638 | } | 1639 | } | ||
| 1639 | func (t *runTerminal) setSize(size domain.Size) { | 1640 | func (t *runTerminal) setSize(size domain.Size) { | ||
| 1640 | - | t.sizeMu.Lock() | 1641 | + | t.geometryMu.Lock() |
| 1641 | - | t.size = size | 1642 | + | t.geometry.Size = size |
| 1642 | - | t.sizeMu.Unlock() | 1643 | + | t.geometryMu.Unlock() |
| 1643 | } | 1644 | } | ||
| 1644 | - | func (t *runTerminal) ResizeEvents() <-chan domain.Size { return t.resizeCh } | 1645 | + | func (t *runTerminal) ResizeEvents() <-chan domain.Geometry { return t.resizeCh } |
| 1645 | - | func (t *runTerminal) In() io.Reader { return t.in } | 1646 | + | func (t *runTerminal) In() io.Reader { return t.in } |
| 1646 | - | func (t *runTerminal) Out() io.Writer { return &t.out } | 1647 | + | func (t *runTerminal) Out() io.Writer { return &t.out } |
| 1647 | - | func (t *runTerminal) Flush() error { return nil } | 1648 | + | func (t *runTerminal) Flush() error { return nil } |
| 1648 | 1649 | ||||
| 1649 | func helloFromSend(t *testing.T, tr *recordingTransport) ports.Hello { | 1650 | func helloFromSend(t *testing.T, tr *recordingTransport) ports.Hello { | ||
| 1650 | t.Helper() | 1651 | t.Helper() | ||
| @@ -1871,12 +1872,12 @@ func TestAttachRememberRemoteHost(t *testing.T) { | |||||
| 1871 | var unblockIn func() | 1872 | var unblockIn func() | ||
| 1872 | if tt.preWelcomeReject { | 1873 | if tt.preWelcomeReject { | ||
| 1873 | tm = portsmocks.NewMockTerminal(t) | 1874 | tm = portsmocks.NewMockTerminal(t) | ||
| 1874 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | 1875 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() |
| 1875 | // EnterRaw must NOT be called when the daemon rejects Hello before Welcome. | 1876 | // EnterRaw must NOT be called when the daemon rejects Hello before Welcome. | ||
| 1876 | } else { | 1877 | } else { | ||
| 1877 | var out bytes.Buffer | 1878 | var out bytes.Buffer | ||
| 1878 | var restoreCount atomic.Int32 | 1879 | var restoreCount atomic.Int32 | ||
| 1879 | - | resizeCh := make(chan domain.Size) | 1880 | + | resizeCh := make(chan domain.Geometry) |
| 1880 | var in *blockingReader | 1881 | var in *blockingReader | ||
| 1881 | tm, in = newHappyTerminal(t, &out, &restoreCount, resizeCh) | 1882 | tm, in = newHappyTerminal(t, &out, &restoreCount, resizeCh) | ||
| 1882 | unblockIn = in.unblock | 1883 | unblockIn = in.unblock | ||
| @@ -1971,9 +1972,9 @@ func TestRunRememberRemoteHostAtMostOnceAcrossReconnects(t *testing.T) { | |||||
| 1971 | func TestAttachRememberRemoteHostDoesNotBlockAfterWelcome(t *testing.T) { | 1972 | func TestAttachRememberRemoteHostDoesNotBlockAfterWelcome(t *testing.T) { | ||
| 1972 | var out bytes.Buffer | 1973 | var out bytes.Buffer | ||
| 1973 | var restoreCount atomic.Int32 | 1974 | var restoreCount atomic.Int32 | ||
| 1974 | - | resizeCh := make(chan domain.Size) | 1975 | + | resizeCh := make(chan domain.Geometry) |
| 1975 | tm := portsmocks.NewMockTerminal(t) | 1976 | tm := portsmocks.NewMockTerminal(t) | ||
| 1976 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | 1977 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() |
| 1977 | enteredRaw := make(chan struct{}) | 1978 | enteredRaw := make(chan struct{}) | ||
| 1978 | tm.EXPECT().EnterRaw().Run(func() { close(enteredRaw) }).Return(func() error { | 1979 | tm.EXPECT().EnterRaw().Run(func() { close(enteredRaw) }).Return(func() error { | ||
| 1979 | restoreCount.Add(1) | 1980 | restoreCount.Add(1) | ||
| @@ -2052,9 +2053,9 @@ func TestRunRestoresTerminalBeforeWaitingForLearner(t *testing.T) { | |||||
| 2052 | var out bytes.Buffer | 2053 | var out bytes.Buffer | ||
| 2053 | var restoreCount atomic.Int32 | 2054 | var restoreCount atomic.Int32 | ||
| 2054 | restored := make(chan struct{}) | 2055 | restored := make(chan struct{}) | ||
| 2055 | - | resizeCh := make(chan domain.Size) | 2056 | + | resizeCh := make(chan domain.Geometry) |
| 2056 | tm := portsmocks.NewMockTerminal(t) | 2057 | tm := portsmocks.NewMockTerminal(t) | ||
| 2057 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | 2058 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() |
| 2058 | tm.EXPECT().EnterRaw().Return(func() error { | 2059 | tm.EXPECT().EnterRaw().Return(func() error { | ||
| 2059 | restoreCount.Add(1) | 2060 | restoreCount.Add(1) | ||
| 2060 | close(restored) | 2061 | close(restored) | ||
| @@ -2134,9 +2135,9 @@ func TestRunReturnsWhenRemoteHostLearnerStalls(t *testing.T) { | |||||
| 2134 | var out bytes.Buffer | 2135 | var out bytes.Buffer | ||
| 2135 | var restoreCount atomic.Int32 | 2136 | var restoreCount atomic.Int32 | ||
| 2136 | restored := make(chan struct{}) | 2137 | restored := make(chan struct{}) | ||
| 2137 | - | resizeCh := make(chan domain.Size) | 2138 | + | resizeCh := make(chan domain.Geometry) |
| 2138 | tm := portsmocks.NewMockTerminal(t) | 2139 | tm := portsmocks.NewMockTerminal(t) | ||
| 2139 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | 2140 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() |
| 2140 | tm.EXPECT().EnterRaw().Return(func() error { | 2141 | tm.EXPECT().EnterRaw().Return(func() error { | ||
| 2141 | restoreCount.Add(1) | 2142 | restoreCount.Add(1) | ||
| 2142 | close(restored) | 2143 | close(restored) | ||
M internal/usecase/client/clipboard_intercept_test.go +5 -5
| @@ -26,12 +26,12 @@ func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReade | |||
| 26 | 26 | t.Helper() | |
| 27 | 27 | var out bytes.Buffer | |
| 28 | 28 | var restoreCount int32 | |
| 29 | - | resizeCh := make(chan domain.Size) | |
| 29 | + | resizeCh := make(chan domain.Geometry) | |
| 30 | 30 | input := newOneShotBlockingReader(stdin) | |
| 31 | 31 | defer input.unblock() | |
| 32 | 32 | ||
| 33 | 33 | tm := portsmocks.NewMockTerminal(t) | |
| 34 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Maybe() | |
| 34 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Maybe() | |
| 35 | 35 | tm.EXPECT().EnterRaw().Return(func() error { restoreCount++; return nil }, nil).Once() | |
| 36 | 36 | tm.EXPECT().In().Return(input).Maybe() | |
| 37 | 37 | tm.EXPECT().Out().Return(&out).Maybe() | |
| @@ -161,16 +161,16 @@ func (d clipboardToastLifecycleDialer) Dial(context.Context) (ports.Transport, e | |||
| 161 | 161 | ||
| 162 | 162 | func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testing.T) { | |
| 163 | 163 | var out bytes.Buffer | |
| 164 | - | resizeEvents := make(chan domain.Size) | |
| 164 | + | resizeEvents := make(chan domain.Geometry) | |
| 165 | 165 | input := newOneShotBlockingReader([]byte{0x16}) | |
| 166 | 166 | defer input.unblock() | |
| 167 | 167 | term := portsmocks.NewMockTerminal(t) | |
| 168 | - | term.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Maybe() | |
| 168 | + | term.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Maybe() | |
| 169 | 169 | term.EXPECT().EnterRaw().Return(func() error { return nil }, nil).Once() | |
| 170 | 170 | term.EXPECT().In().Return(input).Maybe() | |
| 171 | 171 | term.EXPECT().Out().Return(&out).Maybe() | |
| 172 | 172 | term.EXPECT().Flush().Return(nil).Maybe() | |
| 173 | - | term.EXPECT().ResizeEvents().Return((<-chan domain.Size)(resizeEvents)).Maybe() | |
| 173 | + | term.EXPECT().ResizeEvents().Return((<-chan domain.Geometry)(resizeEvents)).Maybe() | |
| 174 | 174 | ||
| 175 | 175 | clipboard := portsmocks.NewMockClipboardReader(t) | |
| 176 | 176 | clipboard.EXPECT().ReadImage(mock.Anything).Return("", nil, errors.New("read failed")).Once() | |
| @@ -26,12 +26,12 @@ func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReade | |||||
| 26 | t.Helper() | 26 | t.Helper() | ||
| 27 | var out bytes.Buffer | 27 | var out bytes.Buffer | ||
| 28 | var restoreCount int32 | 28 | var restoreCount int32 | ||
| 29 | - | resizeCh := make(chan domain.Size) | 29 | + | resizeCh := make(chan domain.Geometry) |
| 30 | input := newOneShotBlockingReader(stdin) | 30 | input := newOneShotBlockingReader(stdin) | ||
| 31 | defer input.unblock() | 31 | defer input.unblock() | ||
| 32 | 32 | ||||
| 33 | tm := portsmocks.NewMockTerminal(t) | 33 | tm := portsmocks.NewMockTerminal(t) | ||
| 34 | - | tm.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Maybe() | 34 | + | tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Maybe() |
| 35 | tm.EXPECT().EnterRaw().Return(func() error { restoreCount++; return nil }, nil).Once() | 35 | tm.EXPECT().EnterRaw().Return(func() error { restoreCount++; return nil }, nil).Once() | ||
| 36 | tm.EXPECT().In().Return(input).Maybe() | 36 | tm.EXPECT().In().Return(input).Maybe() | ||
| 37 | tm.EXPECT().Out().Return(&out).Maybe() | 37 | tm.EXPECT().Out().Return(&out).Maybe() | ||
| @@ -161,16 +161,16 @@ func (d clipboardToastLifecycleDialer) Dial(context.Context) (ports.Transport, e | |||||
| 161 | 161 | ||||
| 162 | func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testing.T) { | 162 | func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testing.T) { | ||
| 163 | var out bytes.Buffer | 163 | var out bytes.Buffer | ||
| 164 | - | resizeEvents := make(chan domain.Size) | 164 | + | resizeEvents := make(chan domain.Geometry) |
| 165 | input := newOneShotBlockingReader([]byte{0x16}) | 165 | input := newOneShotBlockingReader([]byte{0x16}) | ||
| 166 | defer input.unblock() | 166 | defer input.unblock() | ||
| 167 | term := portsmocks.NewMockTerminal(t) | 167 | term := portsmocks.NewMockTerminal(t) | ||
| 168 | - | term.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Maybe() | 168 | + | term.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Maybe() |
| 169 | term.EXPECT().EnterRaw().Return(func() error { return nil }, nil).Once() | 169 | term.EXPECT().EnterRaw().Return(func() error { return nil }, nil).Once() | ||
| 170 | term.EXPECT().In().Return(input).Maybe() | 170 | term.EXPECT().In().Return(input).Maybe() | ||
| 171 | term.EXPECT().Out().Return(&out).Maybe() | 171 | term.EXPECT().Out().Return(&out).Maybe() | ||
| 172 | term.EXPECT().Flush().Return(nil).Maybe() | 172 | term.EXPECT().Flush().Return(nil).Maybe() | ||
| 173 | - | term.EXPECT().ResizeEvents().Return((<-chan domain.Size)(resizeEvents)).Maybe() | 173 | + | term.EXPECT().ResizeEvents().Return((<-chan domain.Geometry)(resizeEvents)).Maybe() |
| 174 | 174 | ||||
| 175 | clipboard := portsmocks.NewMockClipboardReader(t) | 175 | clipboard := portsmocks.NewMockClipboardReader(t) | ||
| 176 | clipboard.EXPECT().ReadImage(mock.Anything).Return("", nil, errors.New("read failed")).Once() | 176 | clipboard.EXPECT().ReadImage(mock.Anything).Return("", nil, errors.New("read failed")).Once() | ||
M internal/usecase/client/palette_attach_generation_test.go +8 -8
| @@ -98,19 +98,19 @@ func (w *attachPaletteWriter) Write(data []byte) (int, error) { | |||
| 98 | 98 | type attachPaletteTerminalHarness struct { | |
| 99 | 99 | in io.Reader | |
| 100 | 100 | out *attachPaletteWriter | |
| 101 | - | resize chan domain.Size | |
| 101 | + | resize chan domain.Geometry | |
| 102 | 102 | } | |
| 103 | 103 | ||
| 104 | 104 | func (t *attachPaletteTerminalHarness) EnterRaw() (func() error, error) { | |
| 105 | 105 | return func() error { return nil }, nil | |
| 106 | 106 | } | |
| 107 | - | func (*attachPaletteTerminalHarness) Size() (domain.Size, error) { | |
| 108 | - | return domain.Size{Cols: 80, Rows: 24}, nil | |
| 107 | + | func (*attachPaletteTerminalHarness) Geometry() (domain.Geometry, error) { | |
| 108 | + | return domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil | |
| 109 | 109 | } | |
| 110 | - | func (t *attachPaletteTerminalHarness) ResizeEvents() <-chan domain.Size { return t.resize } | |
| 111 | - | func (t *attachPaletteTerminalHarness) In() io.Reader { return t.in } | |
| 112 | - | func (t *attachPaletteTerminalHarness) Out() io.Writer { return t.out } | |
| 113 | - | func (*attachPaletteTerminalHarness) Flush() error { return nil } | |
| 110 | + | func (t *attachPaletteTerminalHarness) ResizeEvents() <-chan domain.Geometry { return t.resize } | |
| 111 | + | func (t *attachPaletteTerminalHarness) In() io.Reader { return t.in } | |
| 112 | + | func (t *attachPaletteTerminalHarness) Out() io.Writer { return t.out } | |
| 113 | + | func (*attachPaletteTerminalHarness) Flush() error { return nil } | |
| 114 | 114 | ||
| 115 | 115 | type attachPaletteTransport struct { | |
| 116 | 116 | mu sync.Mutex | |
| @@ -200,7 +200,7 @@ func startAttachPaletteHarnessWithInputAndWriteError(state *terminalThemeState, | |||
| 200 | 200 | reader := newAttachPaletteReader() | |
| 201 | 201 | writer := newAttachPaletteWriter() | |
| 202 | 202 | writer.err = writeErr | |
| 203 | - | term := &attachPaletteTerminalHarness{in: reader, out: writer, resize: make(chan domain.Size)} | |
| 203 | + | term := &attachPaletteTerminalHarness{in: reader, out: writer, resize: make(chan domain.Geometry)} | |
| 204 | 204 | ms := &milestones{} | |
| 205 | 205 | runner := &Runner{term: term, clock: clock, logger: slog.New(slog.DiscardHandler)} | |
| 206 | 206 | attempt := &attachAttempt{ | |
| @@ -98,19 +98,19 @@ func (w *attachPaletteWriter) Write(data []byte) (int, error) { | |||||
| 98 | type attachPaletteTerminalHarness struct { | 98 | type attachPaletteTerminalHarness struct { | ||
| 99 | in io.Reader | 99 | in io.Reader | ||
| 100 | out *attachPaletteWriter | 100 | out *attachPaletteWriter | ||
| 101 | - | resize chan domain.Size | 101 | + | resize chan domain.Geometry |
| 102 | } | 102 | } | ||
| 103 | 103 | ||||
| 104 | func (t *attachPaletteTerminalHarness) EnterRaw() (func() error, error) { | 104 | func (t *attachPaletteTerminalHarness) EnterRaw() (func() error, error) { | ||
| 105 | return func() error { return nil }, nil | 105 | return func() error { return nil }, nil | ||
| 106 | } | 106 | } | ||
| 107 | - | func (*attachPaletteTerminalHarness) Size() (domain.Size, error) { | 107 | + | func (*attachPaletteTerminalHarness) Geometry() (domain.Geometry, error) { |
| 108 | - | return domain.Size{Cols: 80, Rows: 24}, nil | 108 | + | return domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil |
| 109 | } | 109 | } | ||
| 110 | - | func (t *attachPaletteTerminalHarness) ResizeEvents() <-chan domain.Size { return t.resize } | 110 | + | func (t *attachPaletteTerminalHarness) ResizeEvents() <-chan domain.Geometry { return t.resize } |
| 111 | - | func (t *attachPaletteTerminalHarness) In() io.Reader { return t.in } | 111 | + | func (t *attachPaletteTerminalHarness) In() io.Reader { return t.in } |
| 112 | - | func (t *attachPaletteTerminalHarness) Out() io.Writer { return t.out } | 112 | + | func (t *attachPaletteTerminalHarness) Out() io.Writer { return t.out } |
| 113 | - | func (*attachPaletteTerminalHarness) Flush() error { return nil } | 113 | + | func (*attachPaletteTerminalHarness) Flush() error { return nil } |
| 114 | 114 | ||||
| 115 | type attachPaletteTransport struct { | 115 | type attachPaletteTransport struct { | ||
| 116 | mu sync.Mutex | 116 | mu sync.Mutex | ||
| @@ -200,7 +200,7 @@ func startAttachPaletteHarnessWithInputAndWriteError(state *terminalThemeState, | |||||
| 200 | reader := newAttachPaletteReader() | 200 | reader := newAttachPaletteReader() | ||
| 201 | writer := newAttachPaletteWriter() | 201 | writer := newAttachPaletteWriter() | ||
| 202 | writer.err = writeErr | 202 | writer.err = writeErr | ||
| 203 | - | term := &attachPaletteTerminalHarness{in: reader, out: writer, resize: make(chan domain.Size)} | 203 | + | term := &attachPaletteTerminalHarness{in: reader, out: writer, resize: make(chan domain.Geometry)} |
| 204 | ms := &milestones{} | 204 | ms := &milestones{} | ||
| 205 | runner := &Runner{term: term, clock: clock, logger: slog.New(slog.DiscardHandler)} | 205 | runner := &Runner{term: term, clock: clock, logger: slog.New(slog.DiscardHandler)} | ||
| 206 | attempt := &attachAttempt{ | 206 | attempt := &attachAttempt{ | ||
M internal/usecase/client/palette_attach_test.go +8 -8
| @@ -61,19 +61,19 @@ func (r *paletteAttachReader) close() { r.once.Do(func() { close(r.done) }) } | |||
| 61 | 61 | type paletteAttachTerminal struct { | |
| 62 | 62 | in io.Reader | |
| 63 | 63 | out bytes.Buffer | |
| 64 | - | resize chan domain.Size | |
| 64 | + | resize chan domain.Geometry | |
| 65 | 65 | } | |
| 66 | 66 | ||
| 67 | 67 | func (t *paletteAttachTerminal) EnterRaw() (func() error, error) { | |
| 68 | 68 | return func() error { return nil }, nil | |
| 69 | 69 | } | |
| 70 | - | func (t *paletteAttachTerminal) Size() (domain.Size, error) { | |
| 71 | - | return domain.Size{Cols: 80, Rows: 24}, nil | |
| 70 | + | func (t *paletteAttachTerminal) Geometry() (domain.Geometry, error) { | |
| 71 | + | return domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil | |
| 72 | 72 | } | |
| 73 | - | func (t *paletteAttachTerminal) ResizeEvents() <-chan domain.Size { return t.resize } | |
| 74 | - | func (t *paletteAttachTerminal) In() io.Reader { return t.in } | |
| 75 | - | func (t *paletteAttachTerminal) Out() io.Writer { return &t.out } | |
| 76 | - | func (t *paletteAttachTerminal) Flush() error { return nil } | |
| 73 | + | func (t *paletteAttachTerminal) ResizeEvents() <-chan domain.Geometry { return t.resize } | |
| 74 | + | func (t *paletteAttachTerminal) In() io.Reader { return t.in } | |
| 75 | + | func (t *paletteAttachTerminal) Out() io.Writer { return &t.out } | |
| 76 | + | func (t *paletteAttachTerminal) Flush() error { return nil } | |
| 77 | 77 | ||
| 78 | 78 | type paletteAttachTransport struct { | |
| 79 | 79 | mu sync.Mutex | |
| @@ -136,7 +136,7 @@ func TestPaletteAttachReaderPreservesShortReadRemainderAndCloses(t *testing.T) { | |||
| 136 | 136 | func TestAttachPublishesOnlyClearedAndDefinitiveInitialPalette(t *testing.T) { | |
| 137 | 137 | input := newPaletteAttachReader([]byte("\x1b]10;#010203\a\x1b]11;#040506\a\x1b]4;2;#102030\a\x1b[?2031;1$y")) | |
| 138 | 138 | t.Cleanup(input.close) | |
| 139 | - | term := &paletteAttachTerminal{in: input, resize: make(chan domain.Size)} | |
| 139 | + | term := &paletteAttachTerminal{in: input, resize: make(chan domain.Geometry)} | |
| 140 | 140 | transport := &paletteAttachTransport{finalSet: make(chan struct{})} | |
| 141 | 141 | ms := &milestones{} | |
| 142 | 142 | runner := &Runner{term: term, clock: paletteAttachClock{}, logger: slog.New(slog.DiscardHandler)} | |
| @@ -61,19 +61,19 @@ func (r *paletteAttachReader) close() { r.once.Do(func() { close(r.done) }) } | |||||
| 61 | type paletteAttachTerminal struct { | 61 | type paletteAttachTerminal struct { | ||
| 62 | in io.Reader | 62 | in io.Reader | ||
| 63 | out bytes.Buffer | 63 | out bytes.Buffer | ||
| 64 | - | resize chan domain.Size | 64 | + | resize chan domain.Geometry |
| 65 | } | 65 | } | ||
| 66 | 66 | ||||
| 67 | func (t *paletteAttachTerminal) EnterRaw() (func() error, error) { | 67 | func (t *paletteAttachTerminal) EnterRaw() (func() error, error) { | ||
| 68 | return func() error { return nil }, nil | 68 | return func() error { return nil }, nil | ||
| 69 | } | 69 | } | ||
| 70 | - | func (t *paletteAttachTerminal) Size() (domain.Size, error) { | 70 | + | func (t *paletteAttachTerminal) Geometry() (domain.Geometry, error) { |
| 71 | - | return domain.Size{Cols: 80, Rows: 24}, nil | 71 | + | return domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil |
| 72 | } | 72 | } | ||
| 73 | - | func (t *paletteAttachTerminal) ResizeEvents() <-chan domain.Size { return t.resize } | 73 | + | func (t *paletteAttachTerminal) ResizeEvents() <-chan domain.Geometry { return t.resize } |
| 74 | - | func (t *paletteAttachTerminal) In() io.Reader { return t.in } | 74 | + | func (t *paletteAttachTerminal) In() io.Reader { return t.in } |
| 75 | - | func (t *paletteAttachTerminal) Out() io.Writer { return &t.out } | 75 | + | func (t *paletteAttachTerminal) Out() io.Writer { return &t.out } |
| 76 | - | func (t *paletteAttachTerminal) Flush() error { return nil } | 76 | + | func (t *paletteAttachTerminal) Flush() error { return nil } |
| 77 | 77 | ||||
| 78 | type paletteAttachTransport struct { | 78 | type paletteAttachTransport struct { | ||
| 79 | mu sync.Mutex | 79 | mu sync.Mutex | ||
| @@ -136,7 +136,7 @@ func TestPaletteAttachReaderPreservesShortReadRemainderAndCloses(t *testing.T) { | |||||
| 136 | func TestAttachPublishesOnlyClearedAndDefinitiveInitialPalette(t *testing.T) { | 136 | func TestAttachPublishesOnlyClearedAndDefinitiveInitialPalette(t *testing.T) { | ||
| 137 | input := newPaletteAttachReader([]byte("\x1b]10;#010203\a\x1b]11;#040506\a\x1b]4;2;#102030\a\x1b[?2031;1$y")) | 137 | input := newPaletteAttachReader([]byte("\x1b]10;#010203\a\x1b]11;#040506\a\x1b]4;2;#102030\a\x1b[?2031;1$y")) | ||
| 138 | t.Cleanup(input.close) | 138 | t.Cleanup(input.close) | ||
| 139 | - | term := &paletteAttachTerminal{in: input, resize: make(chan domain.Size)} | 139 | + | term := &paletteAttachTerminal{in: input, resize: make(chan domain.Geometry)} |
| 140 | transport := &paletteAttachTransport{finalSet: make(chan struct{})} | 140 | transport := &paletteAttachTransport{finalSet: make(chan struct{})} | ||
| 141 | ms := &milestones{} | 141 | ms := &milestones{} | ||
| 142 | runner := &Runner{term: term, clock: paletteAttachClock{}, logger: slog.New(slog.DiscardHandler)} | 142 | runner := &Runner{term: term, clock: paletteAttachClock{}, logger: slog.New(slog.DiscardHandler)} | ||
M internal/usecase/client/reconnect_toast_test.go +25 -25
| @@ -28,7 +28,7 @@ type reconnectToastTerminalHarness struct { | |||
| 28 | 28 | inWriter *io.PipeWriter | |
| 29 | 29 | out bytes.Buffer | |
| 30 | 30 | size domain.Size | |
| 31 | - | resizeCh chan domain.Size | |
| 31 | + | resizeCh chan domain.Geometry | |
| 32 | 32 | sizeCalls atomic.Int32 | |
| 33 | 33 | sizeCalled chan int32 | |
| 34 | 34 | } | |
| @@ -41,18 +41,18 @@ func newReconnectToastTerminalHarness(t *testing.T) *reconnectToastTerminalHarne | |||
| 41 | 41 | in: in, | |
| 42 | 42 | inWriter: inWriter, | |
| 43 | 43 | size: domain.Size{Cols: 80, Rows: 24}, | |
| 44 | - | resizeCh: make(chan domain.Size, sendQueueDepth), | |
| 44 | + | resizeCh: make(chan domain.Geometry, sendQueueDepth), | |
| 45 | 45 | sizeCalled: make(chan int32, 16), | |
| 46 | 46 | } | |
| 47 | 47 | h.term.EXPECT().EnterRaw().Return(func() error { return nil }, nil).Maybe() | |
| 48 | - | h.term.EXPECT().Size().Run(func() { | |
| 48 | + | h.term.EXPECT().Geometry().Run(func() { | |
| 49 | 49 | call := h.sizeCalls.Add(1) | |
| 50 | 50 | select { | |
| 51 | 51 | case h.sizeCalled <- call: | |
| 52 | 52 | default: | |
| 53 | 53 | } | |
| 54 | - | }).Return(h.size, nil).Maybe() | |
| 55 | - | h.term.EXPECT().ResizeEvents().Return((<-chan domain.Size)(h.resizeCh)).Maybe() | |
| 54 | + | }).Return(domain.Geometry{Size: h.size}, nil).Maybe() | |
| 55 | + | h.term.EXPECT().ResizeEvents().Return((<-chan domain.Geometry)(h.resizeCh)).Maybe() | |
| 56 | 56 | h.term.EXPECT().In().Return(h.in).Maybe() | |
| 57 | 57 | h.term.EXPECT().Out().Return(&h.out).Maybe() | |
| 58 | 58 | h.term.EXPECT().Flush().Return(nil).Maybe() | |
| @@ -72,18 +72,18 @@ func newReconnectToastTerminalHarnessWithOutputAndFlush(t *testing.T, out io.Wri | |||
| 72 | 72 | in: in, | |
| 73 | 73 | inWriter: inWriter, | |
| 74 | 74 | size: domain.Size{Cols: 80, Rows: 24}, | |
| 75 | - | resizeCh: make(chan domain.Size, sendQueueDepth), | |
| 75 | + | resizeCh: make(chan domain.Geometry, sendQueueDepth), | |
| 76 | 76 | sizeCalled: make(chan int32, 16), | |
| 77 | 77 | } | |
| 78 | 78 | h.term.EXPECT().EnterRaw().Return(func() error { return nil }, nil).Maybe() | |
| 79 | - | h.term.EXPECT().Size().Run(func() { | |
| 79 | + | h.term.EXPECT().Geometry().Run(func() { | |
| 80 | 80 | call := h.sizeCalls.Add(1) | |
| 81 | 81 | select { | |
| 82 | 82 | case h.sizeCalled <- call: | |
| 83 | 83 | default: | |
| 84 | 84 | } | |
| 85 | - | }).Return(h.size, nil).Maybe() | |
| 86 | - | h.term.EXPECT().ResizeEvents().Return((<-chan domain.Size)(h.resizeCh)).Maybe() | |
| 85 | + | }).Return(domain.Geometry{Size: h.size}, nil).Maybe() | |
| 86 | + | h.term.EXPECT().ResizeEvents().Return((<-chan domain.Geometry)(h.resizeCh)).Maybe() | |
| 87 | 87 | h.term.EXPECT().In().Return(h.in).Maybe() | |
| 88 | 88 | h.term.EXPECT().Out().Return(out).Maybe() | |
| 89 | 89 | h.term.EXPECT().Flush().RunAndReturn(flush).Maybe() | |
| @@ -1071,14 +1071,14 @@ func TestReconnectResetEnqueueCancellationExitsCleanly(t *testing.T) { | |||
| 1071 | 1071 | requireTerminalSizeCalls(t, term, 2) | |
| 1072 | 1072 | tr.blockType = ports.MsgResize | |
| 1073 | 1073 | tr.armed.Store(true) | |
| 1074 | - | term.resizeCh <- term.size | |
| 1074 | + | term.resizeCh <- domain.Geometry{Size: term.size} | |
| 1075 | 1075 | select { | |
| 1076 | 1076 | case <-tr.entered: | |
| 1077 | 1077 | case <-time.After(time.Second): | |
| 1078 | 1078 | t.Fatal("timed out waiting for sender to block") | |
| 1079 | 1079 | } | |
| 1080 | 1080 | for range sendQueueDepth { | |
| 1081 | - | term.resizeCh <- term.size | |
| 1081 | + | term.resizeCh <- domain.Geometry{Size: term.size} | |
| 1082 | 1082 | } | |
| 1083 | 1083 | require.Eventually(t, func() bool { return len(term.resizeCh) == 0 }, time.Second, time.Millisecond) | |
| 1084 | 1084 | ||
| @@ -1111,14 +1111,14 @@ func TestReconnectResetCancellationPreservesQueuedSenderError(t *testing.T) { | |||
| 1111 | 1111 | requireTerminalSizeCalls(t, term, 2) | |
| 1112 | 1112 | tr.blockType = ports.MsgResize | |
| 1113 | 1113 | tr.armed.Store(true) | |
| 1114 | - | term.resizeCh <- term.size | |
| 1114 | + | term.resizeCh <- domain.Geometry{Size: term.size} | |
| 1115 | 1115 | select { | |
| 1116 | 1116 | case <-tr.entered: | |
| 1117 | 1117 | case <-time.After(time.Second): | |
| 1118 | 1118 | t.Fatal("timed out waiting for sender to block") | |
| 1119 | 1119 | } | |
| 1120 | 1120 | for range sendQueueDepth { | |
| 1121 | - | term.resizeCh <- term.size | |
| 1121 | + | term.resizeCh <- domain.Geometry{Size: term.size} | |
| 1122 | 1122 | } | |
| 1123 | 1123 | require.Eventually(t, func() bool { return len(term.resizeCh) == 0 }, time.Second, time.Millisecond) | |
| 1124 | 1124 | ||
| @@ -1261,9 +1261,9 @@ func TestReconnectSleepWithResizeEventsRedrawsUntilTimerFires(t *testing.T) { | |||
| 1261 | 1261 | clk.EXPECT().NewTimer(time.Hour).Return(timer).Once() | |
| 1262 | 1262 | timer.EXPECT().C().Return((<-chan time.Time)(timerCh)).Maybe() | |
| 1263 | 1263 | timer.EXPECT().Stop().Return(true).Once() | |
| 1264 | - | resizeCh := make(chan domain.Size, 2) | |
| 1265 | - | resizeCh <- domain.Size{Cols: 100, Rows: 30} | |
| 1266 | - | resizeCh <- domain.Size{Cols: 120, Rows: 40} | |
| 1264 | + | resizeCh := make(chan domain.Geometry, 2) | |
| 1265 | + | resizeCh <- domain.Geometry{Size: domain.Size{Cols: 100, Rows: 30}} | |
| 1266 | + | resizeCh <- domain.Geometry{Size: domain.Size{Cols: 120, Rows: 40}} | |
| 1267 | 1267 | got := make(chan domain.Size, 2) | |
| 1268 | 1268 | type sleepResult struct { | |
| 1269 | 1269 | slept bool | |
| @@ -1293,8 +1293,8 @@ func TestReconnectSleepWithResizeEventsReturnsRedrawError(t *testing.T) { | |||
| 1293 | 1293 | clk.EXPECT().NewTimer(time.Hour).Return(timer).Once() | |
| 1294 | 1294 | timer.EXPECT().C().Return((<-chan time.Time)(make(chan time.Time))).Maybe() | |
| 1295 | 1295 | timer.EXPECT().Stop().Return(true).Once() | |
| 1296 | - | resizeCh := make(chan domain.Size, 1) | |
| 1297 | - | resizeCh <- domain.Size{Cols: 100, Rows: 30} | |
| 1296 | + | resizeCh := make(chan domain.Geometry, 1) | |
| 1297 | + | resizeCh <- domain.Geometry{Size: domain.Size{Cols: 100, Rows: 30}} | |
| 1298 | 1298 | redrawErr := errors.New("redraw failed") | |
| 1299 | 1299 | ||
| 1300 | 1300 | slept, err := sleepReconnectWithResizeEvents(context.Background(), clk, time.Hour, resizeCh, func(domain.Size) error { | |
| @@ -1349,7 +1349,7 @@ func TestReconnectRetryFailuresPreserveAttachError(t *testing.T) { | |||
| 1349 | 1349 | for _, tt := range tests { | |
| 1350 | 1350 | t.Run(tt.name, func(t *testing.T) { | |
| 1351 | 1351 | oldSleepWithResize := reconnectSleepWithResize | |
| 1352 | - | reconnectSleepWithResize = func(_ context.Context, _ ports.Clock, _ time.Duration, _ <-chan domain.Size, redraw func(domain.Size) error) (bool, error) { | |
| 1352 | + | reconnectSleepWithResize = func(_ context.Context, _ ports.Clock, _ time.Duration, _ <-chan domain.Geometry, redraw func(domain.Size) error) (bool, error) { | |
| 1353 | 1353 | return tt.retrySleep(redraw) | |
| 1354 | 1354 | } | |
| 1355 | 1355 | defer func() { reconnectSleepWithResize = oldSleepWithResize }() | |
| @@ -1394,7 +1394,7 @@ func TestReconnectRetrySleepFailuresPreserveDialError(t *testing.T) { | |||
| 1394 | 1394 | t.Run(tt.name, func(t *testing.T) { | |
| 1395 | 1395 | oldSleepWithResize := reconnectSleepWithResize | |
| 1396 | 1396 | calls := 0 | |
| 1397 | - | reconnectSleepWithResize = func(_ context.Context, _ ports.Clock, _ time.Duration, _ <-chan domain.Size, redraw func(domain.Size) error) (bool, error) { | |
| 1397 | + | reconnectSleepWithResize = func(_ context.Context, _ ports.Clock, _ time.Duration, _ <-chan domain.Geometry, redraw func(domain.Size) error) (bool, error) { | |
| 1398 | 1398 | calls++ | |
| 1399 | 1399 | if calls == 1 { | |
| 1400 | 1400 | return true, nil | |
| @@ -1431,7 +1431,7 @@ func TestRemoteReconnectToastFailedDrawDoesNotBlankBounds(t *testing.T) { | |||
| 1431 | 1431 | oldSleepWithResize := reconnectSleepWithResize | |
| 1432 | 1432 | ctx, cancel := context.WithCancel(context.Background()) | |
| 1433 | 1433 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return false } | |
| 1434 | - | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Size, func(domain.Size) error) (bool, error) { | |
| 1434 | + | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Geometry, func(domain.Size) error) (bool, error) { | |
| 1435 | 1435 | cancel() | |
| 1436 | 1436 | return false, nil | |
| 1437 | 1437 | } | |
| @@ -1508,7 +1508,7 @@ func TestReconnectCancellationPreservesContextWhenStatusClearFails(t *testing.T) | |||
| 1508 | 1508 | oldSleepWithResize := reconnectSleepWithResize | |
| 1509 | 1509 | ctx, cancel := context.WithCancel(context.Background()) | |
| 1510 | 1510 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return tt.sleep(cancel) } | |
| 1511 | - | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Size, func(domain.Size) error) (bool, error) { | |
| 1511 | + | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Geometry, func(domain.Size) error) (bool, error) { | |
| 1512 | 1512 | return tt.sleep(cancel), nil | |
| 1513 | 1513 | } | |
| 1514 | 1514 | defer func() { | |
| @@ -1542,7 +1542,7 @@ func TestRemoteReconnectToastLifecycleWithWrappedTransportError(t *testing.T) { | |||
| 1542 | 1542 | oldSleep := reconnectSleep | |
| 1543 | 1543 | oldSleepWithResize := reconnectSleepWithResize | |
| 1544 | 1544 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return true } | |
| 1545 | - | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Size, func(domain.Size) error) (bool, error) { | |
| 1545 | + | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Geometry, func(domain.Size) error) (bool, error) { | |
| 1546 | 1546 | return true, nil | |
| 1547 | 1547 | } | |
| 1548 | 1548 | defer func() { | |
| @@ -1586,7 +1586,7 @@ func TestRemoteEphemeralReconnectUsesAssignedSessionName(t *testing.T) { | |||
| 1586 | 1586 | oldSleep := reconnectSleep | |
| 1587 | 1587 | oldSleepWithResize := reconnectSleepWithResize | |
| 1588 | 1588 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return true } | |
| 1589 | - | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Size, func(domain.Size) error) (bool, error) { | |
| 1589 | + | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Geometry, func(domain.Size) error) (bool, error) { | |
| 1590 | 1590 | return true, nil | |
| 1591 | 1591 | } | |
| 1592 | 1592 | defer func() { | |
| @@ -1677,7 +1677,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) { | |||
| 1677 | 1677 | oldSleepWithResize := reconnectSleepWithResize | |
| 1678 | 1678 | ctx, cancel := context.WithCancel(context.Background()) | |
| 1679 | 1679 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return tt.sleep(cancel) } | |
| 1680 | - | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Size, func(domain.Size) error) (bool, error) { | |
| 1680 | + | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Geometry, func(domain.Size) error) (bool, error) { | |
| 1681 | 1681 | return tt.sleep(cancel), nil | |
| 1682 | 1682 | } | |
| 1683 | 1683 | defer func() { | |
| @@ -28,7 +28,7 @@ type reconnectToastTerminalHarness struct { | |||||
| 28 | inWriter *io.PipeWriter | 28 | inWriter *io.PipeWriter | ||
| 29 | out bytes.Buffer | 29 | out bytes.Buffer | ||
| 30 | size domain.Size | 30 | size domain.Size | ||
| 31 | - | resizeCh chan domain.Size | 31 | + | resizeCh chan domain.Geometry |
| 32 | sizeCalls atomic.Int32 | 32 | sizeCalls atomic.Int32 | ||
| 33 | sizeCalled chan int32 | 33 | sizeCalled chan int32 | ||
| 34 | } | 34 | } | ||
| @@ -41,18 +41,18 @@ func newReconnectToastTerminalHarness(t *testing.T) *reconnectToastTerminalHarne | |||||
| 41 | in: in, | 41 | in: in, | ||
| 42 | inWriter: inWriter, | 42 | inWriter: inWriter, | ||
| 43 | size: domain.Size{Cols: 80, Rows: 24}, | 43 | size: domain.Size{Cols: 80, Rows: 24}, | ||
| 44 | - | resizeCh: make(chan domain.Size, sendQueueDepth), | 44 | + | resizeCh: make(chan domain.Geometry, sendQueueDepth), |
| 45 | sizeCalled: make(chan int32, 16), | 45 | sizeCalled: make(chan int32, 16), | ||
| 46 | } | 46 | } | ||
| 47 | h.term.EXPECT().EnterRaw().Return(func() error { return nil }, nil).Maybe() | 47 | h.term.EXPECT().EnterRaw().Return(func() error { return nil }, nil).Maybe() | ||
| 48 | - | h.term.EXPECT().Size().Run(func() { | 48 | + | h.term.EXPECT().Geometry().Run(func() { |
| 49 | call := h.sizeCalls.Add(1) | 49 | call := h.sizeCalls.Add(1) | ||
| 50 | select { | 50 | select { | ||
| 51 | case h.sizeCalled <- call: | 51 | case h.sizeCalled <- call: | ||
| 52 | default: | 52 | default: | ||
| 53 | } | 53 | } | ||
| 54 | - | }).Return(h.size, nil).Maybe() | 54 | + | }).Return(domain.Geometry{Size: h.size}, nil).Maybe() |
| 55 | - | h.term.EXPECT().ResizeEvents().Return((<-chan domain.Size)(h.resizeCh)).Maybe() | 55 | + | h.term.EXPECT().ResizeEvents().Return((<-chan domain.Geometry)(h.resizeCh)).Maybe() |
| 56 | h.term.EXPECT().In().Return(h.in).Maybe() | 56 | h.term.EXPECT().In().Return(h.in).Maybe() | ||
| 57 | h.term.EXPECT().Out().Return(&h.out).Maybe() | 57 | h.term.EXPECT().Out().Return(&h.out).Maybe() | ||
| 58 | h.term.EXPECT().Flush().Return(nil).Maybe() | 58 | h.term.EXPECT().Flush().Return(nil).Maybe() | ||
| @@ -72,18 +72,18 @@ func newReconnectToastTerminalHarnessWithOutputAndFlush(t *testing.T, out io.Wri | |||||
| 72 | in: in, | 72 | in: in, | ||
| 73 | inWriter: inWriter, | 73 | inWriter: inWriter, | ||
| 74 | size: domain.Size{Cols: 80, Rows: 24}, | 74 | size: domain.Size{Cols: 80, Rows: 24}, | ||
| 75 | - | resizeCh: make(chan domain.Size, sendQueueDepth), | 75 | + | resizeCh: make(chan domain.Geometry, sendQueueDepth), |
| 76 | sizeCalled: make(chan int32, 16), | 76 | sizeCalled: make(chan int32, 16), | ||
| 77 | } | 77 | } | ||
| 78 | h.term.EXPECT().EnterRaw().Return(func() error { return nil }, nil).Maybe() | 78 | h.term.EXPECT().EnterRaw().Return(func() error { return nil }, nil).Maybe() | ||
| 79 | - | h.term.EXPECT().Size().Run(func() { | 79 | + | h.term.EXPECT().Geometry().Run(func() { |
| 80 | call := h.sizeCalls.Add(1) | 80 | call := h.sizeCalls.Add(1) | ||
| 81 | select { | 81 | select { | ||
| 82 | case h.sizeCalled <- call: | 82 | case h.sizeCalled <- call: | ||
| 83 | default: | 83 | default: | ||
| 84 | } | 84 | } | ||
| 85 | - | }).Return(h.size, nil).Maybe() | 85 | + | }).Return(domain.Geometry{Size: h.size}, nil).Maybe() |
| 86 | - | h.term.EXPECT().ResizeEvents().Return((<-chan domain.Size)(h.resizeCh)).Maybe() | 86 | + | h.term.EXPECT().ResizeEvents().Return((<-chan domain.Geometry)(h.resizeCh)).Maybe() |
| 87 | h.term.EXPECT().In().Return(h.in).Maybe() | 87 | h.term.EXPECT().In().Return(h.in).Maybe() | ||
| 88 | h.term.EXPECT().Out().Return(out).Maybe() | 88 | h.term.EXPECT().Out().Return(out).Maybe() | ||
| 89 | h.term.EXPECT().Flush().RunAndReturn(flush).Maybe() | 89 | h.term.EXPECT().Flush().RunAndReturn(flush).Maybe() | ||
| @@ -1071,14 +1071,14 @@ func TestReconnectResetEnqueueCancellationExitsCleanly(t *testing.T) { | |||||
| 1071 | requireTerminalSizeCalls(t, term, 2) | 1071 | requireTerminalSizeCalls(t, term, 2) | ||
| 1072 | tr.blockType = ports.MsgResize | 1072 | tr.blockType = ports.MsgResize | ||
| 1073 | tr.armed.Store(true) | 1073 | tr.armed.Store(true) | ||
| 1074 | - | term.resizeCh <- term.size | 1074 | + | term.resizeCh <- domain.Geometry{Size: term.size} |
| 1075 | select { | 1075 | select { | ||
| 1076 | case <-tr.entered: | 1076 | case <-tr.entered: | ||
| 1077 | case <-time.After(time.Second): | 1077 | case <-time.After(time.Second): | ||
| 1078 | t.Fatal("timed out waiting for sender to block") | 1078 | t.Fatal("timed out waiting for sender to block") | ||
| 1079 | } | 1079 | } | ||
| 1080 | for range sendQueueDepth { | 1080 | for range sendQueueDepth { | ||
| 1081 | - | term.resizeCh <- term.size | 1081 | + | term.resizeCh <- domain.Geometry{Size: term.size} |
| 1082 | } | 1082 | } | ||
| 1083 | require.Eventually(t, func() bool { return len(term.resizeCh) == 0 }, time.Second, time.Millisecond) | 1083 | require.Eventually(t, func() bool { return len(term.resizeCh) == 0 }, time.Second, time.Millisecond) | ||
| 1084 | 1084 | ||||
| @@ -1111,14 +1111,14 @@ func TestReconnectResetCancellationPreservesQueuedSenderError(t *testing.T) { | |||||
| 1111 | requireTerminalSizeCalls(t, term, 2) | 1111 | requireTerminalSizeCalls(t, term, 2) | ||
| 1112 | tr.blockType = ports.MsgResize | 1112 | tr.blockType = ports.MsgResize | ||
| 1113 | tr.armed.Store(true) | 1113 | tr.armed.Store(true) | ||
| 1114 | - | term.resizeCh <- term.size | 1114 | + | term.resizeCh <- domain.Geometry{Size: term.size} |
| 1115 | select { | 1115 | select { | ||
| 1116 | case <-tr.entered: | 1116 | case <-tr.entered: | ||
| 1117 | case <-time.After(time.Second): | 1117 | case <-time.After(time.Second): | ||
| 1118 | t.Fatal("timed out waiting for sender to block") | 1118 | t.Fatal("timed out waiting for sender to block") | ||
| 1119 | } | 1119 | } | ||
| 1120 | for range sendQueueDepth { | 1120 | for range sendQueueDepth { | ||
| 1121 | - | term.resizeCh <- term.size | 1121 | + | term.resizeCh <- domain.Geometry{Size: term.size} |
| 1122 | } | 1122 | } | ||
| 1123 | require.Eventually(t, func() bool { return len(term.resizeCh) == 0 }, time.Second, time.Millisecond) | 1123 | require.Eventually(t, func() bool { return len(term.resizeCh) == 0 }, time.Second, time.Millisecond) | ||
| 1124 | 1124 | ||||
| @@ -1261,9 +1261,9 @@ func TestReconnectSleepWithResizeEventsRedrawsUntilTimerFires(t *testing.T) { | |||||
| 1261 | clk.EXPECT().NewTimer(time.Hour).Return(timer).Once() | 1261 | clk.EXPECT().NewTimer(time.Hour).Return(timer).Once() | ||
| 1262 | timer.EXPECT().C().Return((<-chan time.Time)(timerCh)).Maybe() | 1262 | timer.EXPECT().C().Return((<-chan time.Time)(timerCh)).Maybe() | ||
| 1263 | timer.EXPECT().Stop().Return(true).Once() | 1263 | timer.EXPECT().Stop().Return(true).Once() | ||
| 1264 | - | resizeCh := make(chan domain.Size, 2) | 1264 | + | resizeCh := make(chan domain.Geometry, 2) |
| 1265 | - | resizeCh <- domain.Size{Cols: 100, Rows: 30} | 1265 | + | resizeCh <- domain.Geometry{Size: domain.Size{Cols: 100, Rows: 30}} |
| 1266 | - | resizeCh <- domain.Size{Cols: 120, Rows: 40} | 1266 | + | resizeCh <- domain.Geometry{Size: domain.Size{Cols: 120, Rows: 40}} |
| 1267 | got := make(chan domain.Size, 2) | 1267 | got := make(chan domain.Size, 2) | ||
| 1268 | type sleepResult struct { | 1268 | type sleepResult struct { | ||
| 1269 | slept bool | 1269 | slept bool | ||
| @@ -1293,8 +1293,8 @@ func TestReconnectSleepWithResizeEventsReturnsRedrawError(t *testing.T) { | |||||
| 1293 | clk.EXPECT().NewTimer(time.Hour).Return(timer).Once() | 1293 | clk.EXPECT().NewTimer(time.Hour).Return(timer).Once() | ||
| 1294 | timer.EXPECT().C().Return((<-chan time.Time)(make(chan time.Time))).Maybe() | 1294 | timer.EXPECT().C().Return((<-chan time.Time)(make(chan time.Time))).Maybe() | ||
| 1295 | timer.EXPECT().Stop().Return(true).Once() | 1295 | timer.EXPECT().Stop().Return(true).Once() | ||
| 1296 | - | resizeCh := make(chan domain.Size, 1) | 1296 | + | resizeCh := make(chan domain.Geometry, 1) |
| 1297 | - | resizeCh <- domain.Size{Cols: 100, Rows: 30} | 1297 | + | resizeCh <- domain.Geometry{Size: domain.Size{Cols: 100, Rows: 30}} |
| 1298 | redrawErr := errors.New("redraw failed") | 1298 | redrawErr := errors.New("redraw failed") | ||
| 1299 | 1299 | ||||
| 1300 | slept, err := sleepReconnectWithResizeEvents(context.Background(), clk, time.Hour, resizeCh, func(domain.Size) error { | 1300 | slept, err := sleepReconnectWithResizeEvents(context.Background(), clk, time.Hour, resizeCh, func(domain.Size) error { | ||
| @@ -1349,7 +1349,7 @@ func TestReconnectRetryFailuresPreserveAttachError(t *testing.T) { | |||||
| 1349 | for _, tt := range tests { | 1349 | for _, tt := range tests { | ||
| 1350 | t.Run(tt.name, func(t *testing.T) { | 1350 | t.Run(tt.name, func(t *testing.T) { | ||
| 1351 | oldSleepWithResize := reconnectSleepWithResize | 1351 | oldSleepWithResize := reconnectSleepWithResize | ||
| 1352 | - | reconnectSleepWithResize = func(_ context.Context, _ ports.Clock, _ time.Duration, _ <-chan domain.Size, redraw func(domain.Size) error) (bool, error) { | 1352 | + | reconnectSleepWithResize = func(_ context.Context, _ ports.Clock, _ time.Duration, _ <-chan domain.Geometry, redraw func(domain.Size) error) (bool, error) { |
| 1353 | return tt.retrySleep(redraw) | 1353 | return tt.retrySleep(redraw) | ||
| 1354 | } | 1354 | } | ||
| 1355 | defer func() { reconnectSleepWithResize = oldSleepWithResize }() | 1355 | defer func() { reconnectSleepWithResize = oldSleepWithResize }() | ||
| @@ -1394,7 +1394,7 @@ func TestReconnectRetrySleepFailuresPreserveDialError(t *testing.T) { | |||||
| 1394 | t.Run(tt.name, func(t *testing.T) { | 1394 | t.Run(tt.name, func(t *testing.T) { | ||
| 1395 | oldSleepWithResize := reconnectSleepWithResize | 1395 | oldSleepWithResize := reconnectSleepWithResize | ||
| 1396 | calls := 0 | 1396 | calls := 0 | ||
| 1397 | - | reconnectSleepWithResize = func(_ context.Context, _ ports.Clock, _ time.Duration, _ <-chan domain.Size, redraw func(domain.Size) error) (bool, error) { | 1397 | + | reconnectSleepWithResize = func(_ context.Context, _ ports.Clock, _ time.Duration, _ <-chan domain.Geometry, redraw func(domain.Size) error) (bool, error) { |
| 1398 | calls++ | 1398 | calls++ | ||
| 1399 | if calls == 1 { | 1399 | if calls == 1 { | ||
| 1400 | return true, nil | 1400 | return true, nil | ||
| @@ -1431,7 +1431,7 @@ func TestRemoteReconnectToastFailedDrawDoesNotBlankBounds(t *testing.T) { | |||||
| 1431 | oldSleepWithResize := reconnectSleepWithResize | 1431 | oldSleepWithResize := reconnectSleepWithResize | ||
| 1432 | ctx, cancel := context.WithCancel(context.Background()) | 1432 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 1433 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return false } | 1433 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return false } | ||
| 1434 | - | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Size, func(domain.Size) error) (bool, error) { | 1434 | + | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Geometry, func(domain.Size) error) (bool, error) { |
| 1435 | cancel() | 1435 | cancel() | ||
| 1436 | return false, nil | 1436 | return false, nil | ||
| 1437 | } | 1437 | } | ||
| @@ -1508,7 +1508,7 @@ func TestReconnectCancellationPreservesContextWhenStatusClearFails(t *testing.T) | |||||
| 1508 | oldSleepWithResize := reconnectSleepWithResize | 1508 | oldSleepWithResize := reconnectSleepWithResize | ||
| 1509 | ctx, cancel := context.WithCancel(context.Background()) | 1509 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 1510 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return tt.sleep(cancel) } | 1510 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return tt.sleep(cancel) } | ||
| 1511 | - | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Size, func(domain.Size) error) (bool, error) { | 1511 | + | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Geometry, func(domain.Size) error) (bool, error) { |
| 1512 | return tt.sleep(cancel), nil | 1512 | return tt.sleep(cancel), nil | ||
| 1513 | } | 1513 | } | ||
| 1514 | defer func() { | 1514 | defer func() { | ||
| @@ -1542,7 +1542,7 @@ func TestRemoteReconnectToastLifecycleWithWrappedTransportError(t *testing.T) { | |||||
| 1542 | oldSleep := reconnectSleep | 1542 | oldSleep := reconnectSleep | ||
| 1543 | oldSleepWithResize := reconnectSleepWithResize | 1543 | oldSleepWithResize := reconnectSleepWithResize | ||
| 1544 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return true } | 1544 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return true } | ||
| 1545 | - | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Size, func(domain.Size) error) (bool, error) { | 1545 | + | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Geometry, func(domain.Size) error) (bool, error) { |
| 1546 | return true, nil | 1546 | return true, nil | ||
| 1547 | } | 1547 | } | ||
| 1548 | defer func() { | 1548 | defer func() { | ||
| @@ -1586,7 +1586,7 @@ func TestRemoteEphemeralReconnectUsesAssignedSessionName(t *testing.T) { | |||||
| 1586 | oldSleep := reconnectSleep | 1586 | oldSleep := reconnectSleep | ||
| 1587 | oldSleepWithResize := reconnectSleepWithResize | 1587 | oldSleepWithResize := reconnectSleepWithResize | ||
| 1588 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return true } | 1588 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return true } | ||
| 1589 | - | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Size, func(domain.Size) error) (bool, error) { | 1589 | + | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Geometry, func(domain.Size) error) (bool, error) { |
| 1590 | return true, nil | 1590 | return true, nil | ||
| 1591 | } | 1591 | } | ||
| 1592 | defer func() { | 1592 | defer func() { | ||
| @@ -1677,7 +1677,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) { | |||||
| 1677 | oldSleepWithResize := reconnectSleepWithResize | 1677 | oldSleepWithResize := reconnectSleepWithResize | ||
| 1678 | ctx, cancel := context.WithCancel(context.Background()) | 1678 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 1679 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return tt.sleep(cancel) } | 1679 | reconnectSleep = func(context.Context, ports.Clock, time.Duration) bool { return tt.sleep(cancel) } | ||
| 1680 | - | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Size, func(domain.Size) error) (bool, error) { | 1680 | + | reconnectSleepWithResize = func(context.Context, ports.Clock, time.Duration, <-chan domain.Geometry, func(domain.Size) error) (bool, error) { |
| 1681 | return tt.sleep(cancel), nil | 1681 | return tt.sleep(cancel), nil | ||
| 1682 | } | 1682 | } | ||
| 1683 | defer func() { | 1683 | defer func() { | ||
M internal/usecase/client/terminal_input_pump_test.go +1 -1
| @@ -152,7 +152,7 @@ func TestTerminalInputPumpStopDropsLateReadWithoutStartingAnother(t *testing.T) | |||
| 152 | 152 | func TestRunnerReusesBlockedTerminalInputPumpAcrossRuns(t *testing.T) { | |
| 153 | 153 | reader := newLateStopReader() | |
| 154 | 154 | runner := &Runner{term: &attachPaletteTerminalHarness{ | |
| 155 | - | in: reader, out: newAttachPaletteWriter(), resize: make(chan domain.Size), | |
| 155 | + | in: reader, out: newAttachPaletteWriter(), resize: make(chan domain.Geometry), | |
| 156 | 156 | }} | |
| 157 | 157 | ||
| 158 | 158 | first := runner.terminalInput() | |
| @@ -152,7 +152,7 @@ func TestTerminalInputPumpStopDropsLateReadWithoutStartingAnother(t *testing.T) | |||||
| 152 | func TestRunnerReusesBlockedTerminalInputPumpAcrossRuns(t *testing.T) { | 152 | func TestRunnerReusesBlockedTerminalInputPumpAcrossRuns(t *testing.T) { | ||
| 153 | reader := newLateStopReader() | 153 | reader := newLateStopReader() | ||
| 154 | runner := &Runner{term: &attachPaletteTerminalHarness{ | 154 | runner := &Runner{term: &attachPaletteTerminalHarness{ | ||
| 155 | - | in: reader, out: newAttachPaletteWriter(), resize: make(chan domain.Size), | 155 | + | in: reader, out: newAttachPaletteWriter(), resize: make(chan domain.Geometry), |
| 156 | }} | 156 | }} | ||
| 157 | 157 | ||||
| 158 | first := runner.terminalInput() | 158 | first := runner.terminalInput() | ||
M internal/usecase/client/transition_ui.go +3 -3
| @@ -79,11 +79,11 @@ func (u *transitionUI) advance() error { | |||
| 79 | 79 | if u == nil || !u.active { | |
| 80 | 80 | return nil | |
| 81 | 81 | } | |
| 82 | - | size, err := u.term.Size() | |
| 82 | + | geometry, err := u.term.Geometry() | |
| 83 | 83 | if err != nil { | |
| 84 | - | return fmt.Errorf("reading terminal size for transition toast: %w", err) | |
| 84 | + | return fmt.Errorf("reading terminal geometry for transition toast: %w", err) | |
| 85 | 85 | } | |
| 86 | - | if err := u.redraw(size); err != nil { | |
| 86 | + | if err := u.redraw(geometry.Size); err != nil { | |
| 87 | 87 | return err | |
| 88 | 88 | } | |
| 89 | 89 | u.frame = (u.frame + 1) % len(transitionSpinnerFrames) | |
| @@ -79,11 +79,11 @@ func (u *transitionUI) advance() error { | |||||
| 79 | if u == nil || !u.active { | 79 | if u == nil || !u.active { | ||
| 80 | return nil | 80 | return nil | ||
| 81 | } | 81 | } | ||
| 82 | - | size, err := u.term.Size() | 82 | + | geometry, err := u.term.Geometry() |
| 83 | if err != nil { | 83 | if err != nil { | ||
| 84 | - | return fmt.Errorf("reading terminal size for transition toast: %w", err) | 84 | + | return fmt.Errorf("reading terminal geometry for transition toast: %w", err) |
| 85 | } | 85 | } | ||
| 86 | - | if err := u.redraw(size); err != nil { | 86 | + | if err := u.redraw(geometry.Size); err != nil { |
| 87 | return err | 87 | return err | ||
| 88 | } | 88 | } | ||
| 89 | u.frame = (u.frame + 1) % len(transitionSpinnerFrames) | 89 | u.frame = (u.frame + 1) % len(transitionSpinnerFrames) | ||
M internal/usecase/client/transition_ui_test.go +6 -6
| @@ -72,13 +72,13 @@ type transitionTestTerminal struct { | |||
| 72 | 72 | func (*transitionTestTerminal) EnterRaw() (func() error, error) { | |
| 73 | 73 | return func() error { return nil }, nil | |
| 74 | 74 | } | |
| 75 | - | func (*transitionTestTerminal) Size() (domain.Size, error) { | |
| 76 | - | return domain.Size{Cols: 80, Rows: 24}, nil | |
| 75 | + | func (*transitionTestTerminal) Geometry() (domain.Geometry, error) { | |
| 76 | + | return domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil | |
| 77 | 77 | } | |
| 78 | - | func (*transitionTestTerminal) ResizeEvents() <-chan domain.Size { return nil } | |
| 79 | - | func (*transitionTestTerminal) In() io.Reader { return strings.NewReader("") } | |
| 80 | - | func (t *transitionTestTerminal) Out() io.Writer { return &t.out } | |
| 81 | - | func (*transitionTestTerminal) Flush() error { return nil } | |
| 78 | + | func (*transitionTestTerminal) ResizeEvents() <-chan domain.Geometry { return nil } | |
| 79 | + | func (*transitionTestTerminal) In() io.Reader { return strings.NewReader("") } | |
| 80 | + | func (t *transitionTestTerminal) Out() io.Writer { return &t.out } | |
| 81 | + | func (*transitionTestTerminal) Flush() error { return nil } | |
| 82 | 82 | ||
| 83 | 83 | func TestTransitionUIStartsAfterDelayTicksResizesAndStops(t *testing.T) { | |
| 84 | 84 | for _, tt := range []struct { | |
| @@ -72,13 +72,13 @@ type transitionTestTerminal struct { | |||||
| 72 | func (*transitionTestTerminal) EnterRaw() (func() error, error) { | 72 | func (*transitionTestTerminal) EnterRaw() (func() error, error) { | ||
| 73 | return func() error { return nil }, nil | 73 | return func() error { return nil }, nil | ||
| 74 | } | 74 | } | ||
| 75 | - | func (*transitionTestTerminal) Size() (domain.Size, error) { | 75 | + | func (*transitionTestTerminal) Geometry() (domain.Geometry, error) { |
| 76 | - | return domain.Size{Cols: 80, Rows: 24}, nil | 76 | + | return domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil |
| 77 | } | 77 | } | ||
| 78 | - | func (*transitionTestTerminal) ResizeEvents() <-chan domain.Size { return nil } | 78 | + | func (*transitionTestTerminal) ResizeEvents() <-chan domain.Geometry { return nil } |
| 79 | - | func (*transitionTestTerminal) In() io.Reader { return strings.NewReader("") } | 79 | + | func (*transitionTestTerminal) In() io.Reader { return strings.NewReader("") } |
| 80 | - | func (t *transitionTestTerminal) Out() io.Writer { return &t.out } | 80 | + | func (t *transitionTestTerminal) Out() io.Writer { return &t.out } |
| 81 | - | func (*transitionTestTerminal) Flush() error { return nil } | 81 | + | func (*transitionTestTerminal) Flush() error { return nil } |
| 82 | 82 | ||||
| 83 | func TestTransitionUIStartsAfterDelayTicksResizesAndStops(t *testing.T) { | 83 | func TestTransitionUIStartsAfterDelayTicksResizesAndStops(t *testing.T) { | ||
| 84 | for _, tt := range []struct { | 84 | for _, tt := range []struct { | ||
M internal/usecase/client/transport_observability_test.go +3 -3
| @@ -28,10 +28,10 @@ func TestRuntimeObserverDependencyUsesSerializedContract(t *testing.T) { | |||
| 28 | 28 | func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) { | |
| 29 | 29 | var out bytes.Buffer | |
| 30 | 30 | var restores atomic.Int32 | |
| 31 | - | resizeCh := make(chan domain.Size) | |
| 31 | + | resizeCh := make(chan domain.Geometry) | |
| 32 | 32 | flushed := make(chan struct{}) | |
| 33 | 33 | term := portsmocks.NewMockTerminal(t) | |
| 34 | - | term.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | |
| 34 | + | term.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() | |
| 35 | 35 | term.EXPECT().EnterRaw().Return(func() error { restores.Add(1); return nil }, nil).Once() | |
| 36 | 36 | in := newBlockingReader() | |
| 37 | 37 | defer in.unblock() | |
| @@ -78,7 +78,7 @@ func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) { | |||
| 78 | 78 | func TestTerminalFlushBoundaryTransportObservability(t *testing.T) { | |
| 79 | 79 | var out bytes.Buffer | |
| 80 | 80 | var restores atomic.Int32 | |
| 81 | - | resizeCh := make(chan domain.Size) | |
| 81 | + | resizeCh := make(chan domain.Geometry) | |
| 82 | 82 | term, in := newHappyTerminal(t, &out, &restores, resizeCh) | |
| 83 | 83 | defer in.unblock() | |
| 84 | 84 | ||
| @@ -28,10 +28,10 @@ func TestRuntimeObserverDependencyUsesSerializedContract(t *testing.T) { | |||||
| 28 | func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) { | 28 | func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) { | ||
| 29 | var out bytes.Buffer | 29 | var out bytes.Buffer | ||
| 30 | var restores atomic.Int32 | 30 | var restores atomic.Int32 | ||
| 31 | - | resizeCh := make(chan domain.Size) | 31 | + | resizeCh := make(chan domain.Geometry) |
| 32 | flushed := make(chan struct{}) | 32 | flushed := make(chan struct{}) | ||
| 33 | term := portsmocks.NewMockTerminal(t) | 33 | term := portsmocks.NewMockTerminal(t) | ||
| 34 | - | term.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once() | 34 | + | term.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once() |
| 35 | term.EXPECT().EnterRaw().Return(func() error { restores.Add(1); return nil }, nil).Once() | 35 | term.EXPECT().EnterRaw().Return(func() error { restores.Add(1); return nil }, nil).Once() | ||
| 36 | in := newBlockingReader() | 36 | in := newBlockingReader() | ||
| 37 | defer in.unblock() | 37 | defer in.unblock() | ||
| @@ -78,7 +78,7 @@ func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) { | |||||
| 78 | func TestTerminalFlushBoundaryTransportObservability(t *testing.T) { | 78 | func TestTerminalFlushBoundaryTransportObservability(t *testing.T) { | ||
| 79 | var out bytes.Buffer | 79 | var out bytes.Buffer | ||
| 80 | var restores atomic.Int32 | 80 | var restores atomic.Int32 | ||
| 81 | - | resizeCh := make(chan domain.Size) | 81 | + | resizeCh := make(chan domain.Geometry) |
| 82 | term, in := newHappyTerminal(t, &out, &restores, resizeCh) | 82 | term, in := newHappyTerminal(t, &out, &restores, resizeCh) | ||
| 83 | defer in.unblock() | 83 | defer in.unblock() | ||
| 84 | 84 | ||||
M internal/usecase/daemon/attachment_registry.go +24 -9
| @@ -101,7 +101,7 @@ func (c *sessionCore) registerAttachmentLocked(ac *attachedClient) bool { | |||
| 101 | 101 | c.nextAttachmentID++ | |
| 102 | 102 | c.attachments[ac] = struct{}{} | |
| 103 | 103 | c.attachmentOrder[ac] = c.nextAttachmentID | |
| 104 | - | if c.claimGeometryOwnerLocked(ac, ac.sizeSnapshot()) == 0 { | |
| 104 | + | if c.claimGeometryOwnerLocked(ac, ac.geometrySnapshot()) == 0 { | |
| 105 | 105 | c.refreshGeometryOwnerLocked() | |
| 106 | 106 | } | |
| 107 | 107 | return true | |
| @@ -114,7 +114,7 @@ func (c *sessionCore) latestValidGeometryOwnerLocked(exclude *attachedClient) *a | |||
| 114 | 114 | var owner *attachedClient | |
| 115 | 115 | var ownerClaim, ownerOrder uint64 | |
| 116 | 116 | for candidate := range c.attachments { | |
| 117 | - | if candidate == nil || candidate == exclude || !candidate.sizeSnapshot().Valid() || !candidate.geometryClaimSizeSnapshot().Valid() { | |
| 117 | + | if candidate == nil || candidate == exclude || !candidate.geometrySnapshot().Valid() || !candidate.geometryClaimSnapshot().Valid() { | |
| 118 | 118 | continue | |
| 119 | 119 | } | |
| 120 | 120 | claim := candidate.geometryClaim.Load() | |
| @@ -136,7 +136,7 @@ func (c *sessionCore) refreshGeometryOwnerLocked() { | |||
| 136 | 136 | } | |
| 137 | 137 | owner := c.geometryOwner.Load() | |
| 138 | 138 | if owner != nil { | |
| 139 | - | if _, ok := c.attachments[owner]; ok && owner.sizeSnapshot().Valid() && owner.geometryClaimSizeSnapshot().Valid() { | |
| 139 | + | if _, ok := c.attachments[owner]; ok && owner.geometrySnapshot().Valid() && owner.geometryClaimSnapshot().Valid() { | |
| 140 | 140 | return | |
| 141 | 141 | } | |
| 142 | 142 | } | |
| @@ -148,15 +148,15 @@ func (c *sessionCore) refreshGeometryOwnerLocked() { | |||
| 148 | 148 | // claimGeometryOwnerLocked records the latest attachment claim. Caller holds | |
| 149 | 149 | // the owning session's mu; the atomic publication lets resize transactions | |
| 150 | 150 | // validate the claim while tab locks are held. | |
| 151 | - | func (c *sessionCore) claimGeometryOwnerLocked(ac *attachedClient, size domain.Size) uint64 { | |
| 152 | - | if c == nil || ac == nil || !size.Valid() { | |
| 151 | + | func (c *sessionCore) claimGeometryOwnerLocked(ac *attachedClient, geometry domain.Geometry) uint64 { | |
| 152 | + | if c == nil || ac == nil || !geometry.Valid() { | |
| 153 | 153 | return 0 | |
| 154 | 154 | } | |
| 155 | 155 | c.geometryMu.Lock() | |
| 156 | 156 | defer c.geometryMu.Unlock() | |
| 157 | 157 | c.geometryClaimSeq++ | |
| 158 | 158 | claim := c.geometryClaimSeq | |
| 159 | - | ac.publishGeometryClaim(size, claim) | |
| 159 | + | ac.publishGeometryClaim(geometry, claim) | |
| 160 | 160 | c.geometryOwner.Store(ac) | |
| 161 | 161 | return claim | |
| 162 | 162 | } | |
| @@ -189,14 +189,29 @@ func (s *session) claimGeometryOwner(ac *attachedClient) (uint64, bool) { | |||
| 189 | 189 | if ac == nil { | |
| 190 | 190 | return 0, false | |
| 191 | 191 | } | |
| 192 | - | return s.claimGeometryOwnerForSize(ac, ac.sizeSnapshot()) | |
| 192 | + | return s.claimGeometryOwnerForGeometry(ac, ac.geometrySnapshot()) | |
| 193 | 193 | } | |
| 194 | 194 | ||
| 195 | 195 | // claimGeometryOwnerForSize records a requested shared geometry independently | |
| 196 | 196 | // of the attachment-local publication. Transactional resize commits use this | |
| 197 | 197 | // before the PTY phase so a stale request cannot publish after a newer claim. | |
| 198 | 198 | func (s *session) claimGeometryOwnerForSize(ac *attachedClient, size domain.Size) (uint64, bool) { | |
| 199 | - | if s == nil || ac == nil || !size.Valid() { | |
| 199 | + | if ac == nil { | |
| 200 | + | return 0, false | |
| 201 | + | } | |
| 202 | + | // Layout and floating transactions carry only their shared cell size. Keep | |
| 203 | + | // the claimant's terminal pixels when those operations re-claim authority; | |
| 204 | + | // replacing the geometry with a cell-only value would make a later fallback | |
| 205 | + | // lose the terminal's pixel dimensions. | |
| 206 | + | geometry := ac.geometrySnapshot() | |
| 207 | + | geometry.Size = size | |
| 208 | + | return s.claimGeometryOwnerForGeometry(ac, geometry) | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // claimGeometryOwnerForGeometry records a requested shared cell and pixel | |
| 212 | + | // geometry independently of attachment-local publication. | |
| 213 | + | func (s *session) claimGeometryOwnerForGeometry(ac *attachedClient, geometry domain.Geometry) (uint64, bool) { | |
| 214 | + | if s == nil || ac == nil || !geometry.Valid() { | |
| 200 | 215 | return 0, false | |
| 201 | 216 | } | |
| 202 | 217 | s.mu.Lock() | |
| @@ -204,7 +219,7 @@ func (s *session) claimGeometryOwnerForSize(ac *attachedClient, size domain.Size | |||
| 204 | 219 | if _, ok := s.attachments[ac]; !ok { | |
| 205 | 220 | return 0, false | |
| 206 | 221 | } | |
| 207 | - | return s.claimGeometryOwnerLocked(ac, size), true | |
| 222 | + | return s.claimGeometryOwnerLocked(ac, geometry.NormalizePixels()), true | |
| 208 | 223 | } | |
| 209 | 224 | ||
| 210 | 225 | // releaseGeometryClaim drops a claim that never reached the coordinator's | |
| @@ -101,7 +101,7 @@ func (c *sessionCore) registerAttachmentLocked(ac *attachedClient) bool { | |||||
| 101 | c.nextAttachmentID++ | 101 | c.nextAttachmentID++ | ||
| 102 | c.attachments[ac] = struct{}{} | 102 | c.attachments[ac] = struct{}{} | ||
| 103 | c.attachmentOrder[ac] = c.nextAttachmentID | 103 | c.attachmentOrder[ac] = c.nextAttachmentID | ||
| 104 | - | if c.claimGeometryOwnerLocked(ac, ac.sizeSnapshot()) == 0 { | 104 | + | if c.claimGeometryOwnerLocked(ac, ac.geometrySnapshot()) == 0 { |
| 105 | c.refreshGeometryOwnerLocked() | 105 | c.refreshGeometryOwnerLocked() | ||
| 106 | } | 106 | } | ||
| 107 | return true | 107 | return true | ||
| @@ -114,7 +114,7 @@ func (c *sessionCore) latestValidGeometryOwnerLocked(exclude *attachedClient) *a | |||||
| 114 | var owner *attachedClient | 114 | var owner *attachedClient | ||
| 115 | var ownerClaim, ownerOrder uint64 | 115 | var ownerClaim, ownerOrder uint64 | ||
| 116 | for candidate := range c.attachments { | 116 | for candidate := range c.attachments { | ||
| 117 | - | if candidate == nil || candidate == exclude || !candidate.sizeSnapshot().Valid() || !candidate.geometryClaimSizeSnapshot().Valid() { | 117 | + | if candidate == nil || candidate == exclude || !candidate.geometrySnapshot().Valid() || !candidate.geometryClaimSnapshot().Valid() { |
| 118 | continue | 118 | continue | ||
| 119 | } | 119 | } | ||
| 120 | claim := candidate.geometryClaim.Load() | 120 | claim := candidate.geometryClaim.Load() | ||
| @@ -136,7 +136,7 @@ func (c *sessionCore) refreshGeometryOwnerLocked() { | |||||
| 136 | } | 136 | } | ||
| 137 | owner := c.geometryOwner.Load() | 137 | owner := c.geometryOwner.Load() | ||
| 138 | if owner != nil { | 138 | if owner != nil { | ||
| 139 | - | if _, ok := c.attachments[owner]; ok && owner.sizeSnapshot().Valid() && owner.geometryClaimSizeSnapshot().Valid() { | 139 | + | if _, ok := c.attachments[owner]; ok && owner.geometrySnapshot().Valid() && owner.geometryClaimSnapshot().Valid() { |
| 140 | return | 140 | return | ||
| 141 | } | 141 | } | ||
| 142 | } | 142 | } | ||
| @@ -148,15 +148,15 @@ func (c *sessionCore) refreshGeometryOwnerLocked() { | |||||
| 148 | // claimGeometryOwnerLocked records the latest attachment claim. Caller holds | 148 | // claimGeometryOwnerLocked records the latest attachment claim. Caller holds | ||
| 149 | // the owning session's mu; the atomic publication lets resize transactions | 149 | // the owning session's mu; the atomic publication lets resize transactions | ||
| 150 | // validate the claim while tab locks are held. | 150 | // validate the claim while tab locks are held. | ||
| 151 | - | func (c *sessionCore) claimGeometryOwnerLocked(ac *attachedClient, size domain.Size) uint64 { | 151 | + | func (c *sessionCore) claimGeometryOwnerLocked(ac *attachedClient, geometry domain.Geometry) uint64 { |
| 152 | - | if c == nil || ac == nil || !size.Valid() { | 152 | + | if c == nil || ac == nil || !geometry.Valid() { |
| 153 | return 0 | 153 | return 0 | ||
| 154 | } | 154 | } | ||
| 155 | c.geometryMu.Lock() | 155 | c.geometryMu.Lock() | ||
| 156 | defer c.geometryMu.Unlock() | 156 | defer c.geometryMu.Unlock() | ||
| 157 | c.geometryClaimSeq++ | 157 | c.geometryClaimSeq++ | ||
| 158 | claim := c.geometryClaimSeq | 158 | claim := c.geometryClaimSeq | ||
| 159 | - | ac.publishGeometryClaim(size, claim) | 159 | + | ac.publishGeometryClaim(geometry, claim) |
| 160 | c.geometryOwner.Store(ac) | 160 | c.geometryOwner.Store(ac) | ||
| 161 | return claim | 161 | return claim | ||
| 162 | } | 162 | } | ||
| @@ -189,14 +189,29 @@ func (s *session) claimGeometryOwner(ac *attachedClient) (uint64, bool) { | |||||
| 189 | if ac == nil { | 189 | if ac == nil { | ||
| 190 | return 0, false | 190 | return 0, false | ||
| 191 | } | 191 | } | ||
| 192 | - | return s.claimGeometryOwnerForSize(ac, ac.sizeSnapshot()) | 192 | + | return s.claimGeometryOwnerForGeometry(ac, ac.geometrySnapshot()) |
| 193 | } | 193 | } | ||
| 194 | 194 | ||||
| 195 | // claimGeometryOwnerForSize records a requested shared geometry independently | 195 | // claimGeometryOwnerForSize records a requested shared geometry independently | ||
| 196 | // of the attachment-local publication. Transactional resize commits use this | 196 | // of the attachment-local publication. Transactional resize commits use this | ||
| 197 | // before the PTY phase so a stale request cannot publish after a newer claim. | 197 | // before the PTY phase so a stale request cannot publish after a newer claim. | ||
| 198 | func (s *session) claimGeometryOwnerForSize(ac *attachedClient, size domain.Size) (uint64, bool) { | 198 | func (s *session) claimGeometryOwnerForSize(ac *attachedClient, size domain.Size) (uint64, bool) { | ||
| 199 | - | if s == nil || ac == nil || !size.Valid() { | 199 | + | if ac == nil { |
| 200 | + | return 0, false | |||
| 201 | + | } | |||
| 202 | + | // Layout and floating transactions carry only their shared cell size. Keep | |||
| 203 | + | // the claimant's terminal pixels when those operations re-claim authority; | |||
| 204 | + | // replacing the geometry with a cell-only value would make a later fallback | |||
| 205 | + | // lose the terminal's pixel dimensions. | |||
| 206 | + | geometry := ac.geometrySnapshot() | |||
| 207 | + | geometry.Size = size | |||
| 208 | + | return s.claimGeometryOwnerForGeometry(ac, geometry) | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | // claimGeometryOwnerForGeometry records a requested shared cell and pixel | |||
| 212 | + | // geometry independently of attachment-local publication. | |||
| 213 | + | func (s *session) claimGeometryOwnerForGeometry(ac *attachedClient, geometry domain.Geometry) (uint64, bool) { | |||
| 214 | + | if s == nil || ac == nil || !geometry.Valid() { | |||
| 200 | return 0, false | 215 | return 0, false | ||
| 201 | } | 216 | } | ||
| 202 | s.mu.Lock() | 217 | s.mu.Lock() | ||
| @@ -204,7 +219,7 @@ func (s *session) claimGeometryOwnerForSize(ac *attachedClient, size domain.Size | |||||
| 204 | if _, ok := s.attachments[ac]; !ok { | 219 | if _, ok := s.attachments[ac]; !ok { | ||
| 205 | return 0, false | 220 | return 0, false | ||
| 206 | } | 221 | } | ||
| 207 | - | return s.claimGeometryOwnerLocked(ac, size), true | 222 | + | return s.claimGeometryOwnerLocked(ac, geometry.NormalizePixels()), true |
| 208 | } | 223 | } | ||
| 209 | 224 | ||||
| 210 | // releaseGeometryClaim drops a claim that never reached the coordinator's | 225 | // releaseGeometryClaim drops a claim that never reached the coordinator's | ||
M internal/usecase/daemon/attachment_transition_publish.go +1 -1
| @@ -194,7 +194,7 @@ func publishAttachmentOwnershipLocked(publication *attachmentPublication) { | |||
| 194 | 194 | if !registered && publication.source == req.target && !req.preserveAttachment { | |
| 195 | 195 | // A same-session rebind keeps membership but still represents a fresh | |
| 196 | 196 | // attachment claim with the replacement transport's current size. | |
| 197 | - | req.target.claimGeometryOwnerLocked(req.next, req.next.sizeSnapshot()) | |
| 197 | + | req.target.claimGeometryOwnerLocked(req.next, req.next.geometrySnapshot()) | |
| 198 | 198 | } | |
| 199 | 199 | publication.nextGeneration = req.next.connectionGeneration.Add(1) | |
| 200 | 200 | req.next.setSession(req.target) | |
| @@ -194,7 +194,7 @@ func publishAttachmentOwnershipLocked(publication *attachmentPublication) { | |||||
| 194 | if !registered && publication.source == req.target && !req.preserveAttachment { | 194 | if !registered && publication.source == req.target && !req.preserveAttachment { | ||
| 195 | // A same-session rebind keeps membership but still represents a fresh | 195 | // A same-session rebind keeps membership but still represents a fresh | ||
| 196 | // attachment claim with the replacement transport's current size. | 196 | // attachment claim with the replacement transport's current size. | ||
| 197 | - | req.target.claimGeometryOwnerLocked(req.next, req.next.sizeSnapshot()) | 197 | + | req.target.claimGeometryOwnerLocked(req.next, req.next.geometrySnapshot()) |
| 198 | } | 198 | } | ||
| 199 | publication.nextGeneration = req.next.connectionGeneration.Add(1) | 199 | publication.nextGeneration = req.next.connectionGeneration.Add(1) | ||
| 200 | req.next.setSession(req.target) | 200 | req.next.setSession(req.target) | ||
M internal/usecase/daemon/attention_test.go +1 -1
| @@ -227,7 +227,7 @@ func (p *scriptPTY) Close() error { | |||
| 227 | 227 | p.once.Do(func() { close(p.closed) }) | |
| 228 | 228 | return nil | |
| 229 | 229 | } | |
| 230 | - | func (p *scriptPTY) Resize(domain.Size) error { return nil } | |
| 230 | + | func (p *scriptPTY) Resize(domain.Geometry) error { return nil } | |
| 231 | 231 | func (p *scriptPTY) Pid() int { return 4242 } | |
| 232 | 232 | func (p *scriptPTY) ForegroundPgid() (int, error) { return 4242, nil } | |
| 233 | 233 | ||
| @@ -227,7 +227,7 @@ func (p *scriptPTY) Close() error { | |||||
| 227 | p.once.Do(func() { close(p.closed) }) | 227 | p.once.Do(func() { close(p.closed) }) | ||
| 228 | return nil | 228 | return nil | ||
| 229 | } | 229 | } | ||
| 230 | - | func (p *scriptPTY) Resize(domain.Size) error { return nil } | 230 | + | func (p *scriptPTY) Resize(domain.Geometry) error { return nil } |
| 231 | func (p *scriptPTY) Pid() int { return 4242 } | 231 | func (p *scriptPTY) Pid() int { return 4242 } | ||
| 232 | func (p *scriptPTY) ForegroundPgid() (int, error) { return 4242, nil } | 232 | func (p *scriptPTY) ForegroundPgid() (int, error) { return 4242, nil } | ||
| 233 | 233 | ||||
M internal/usecase/daemon/client.go +64 -23
| @@ -72,9 +72,10 @@ type attachedClient struct { | |||
| 72 | 72 | // sizeMu lets shared-geometry snapshots read attachment windows without | |
| 73 | 73 | // waiting behind a blocked transport send. | |
| 74 | 74 | sizeMu sync.RWMutex | |
| 75 | - | size domain.Size | |
| 76 | - | geometryClaimSize domain.Size // protected by sizeMu; shared geometry claim snapshot | |
| 77 | - | geometryClaimInitialized bool // protected by sizeMu; distinguishes legacy fixtures | |
| 75 | + | size domain.Size // retained for legacy fixtures; live paths publish geometry | |
| 76 | + | geometry domain.Geometry // controlling-terminal geometry for this attachment | |
| 77 | + | geometryClaimGeometry domain.Geometry // protected by sizeMu; shared geometry claim snapshot | |
| 78 | + | geometryClaimInitialized bool // protected by sizeMu; distinguishes legacy fixtures | |
| 78 | 79 | // geometryClaim is an attachment-local monotonic claim sequence. The | |
| 79 | 80 | // owning session publishes it whenever this attachment is attached, | |
| 80 | 81 | // resumed, or explicitly resized. | |
| @@ -169,36 +170,57 @@ func (ac *attachedClient) sizeSnapshot() domain.Size { | |||
| 169 | 170 | } | |
| 170 | 171 | ||
| 171 | 172 | func (ac *attachedClient) setSize(size domain.Size) { | |
| 173 | + | ac.setGeometry(domain.Geometry{Size: size}) | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | func (ac *attachedClient) geometrySnapshot() domain.Geometry { | |
| 177 | + | if ac == nil { | |
| 178 | + | return domain.Geometry{} | |
| 179 | + | } | |
| 180 | + | ac.sizeMu.RLock() | |
| 181 | + | defer ac.sizeMu.RUnlock() | |
| 182 | + | if ac.geometry.Valid() { | |
| 183 | + | return ac.geometry | |
| 184 | + | } | |
| 185 | + | return domain.Geometry{Size: ac.size} | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | func (ac *attachedClient) setGeometry(geometry domain.Geometry) { | |
| 172 | 189 | if ac == nil { | |
| 173 | 190 | return | |
| 174 | 191 | } | |
| 192 | + | geometry = geometry.NormalizePixels() | |
| 175 | 193 | ac.sizeMu.Lock() | |
| 176 | - | ac.size = size | |
| 194 | + | ac.size = geometry.Size | |
| 195 | + | ac.geometry = geometry | |
| 177 | 196 | ac.sizeMu.Unlock() | |
| 178 | 197 | } | |
| 179 | 198 | ||
| 180 | - | func (ac *attachedClient) geometryClaimSizeSnapshot() domain.Size { | |
| 199 | + | func (ac *attachedClient) geometryClaimSnapshot() domain.Geometry { | |
| 181 | 200 | if ac == nil { | |
| 182 | - | return domain.Size{} | |
| 201 | + | return domain.Geometry{} | |
| 183 | 202 | } | |
| 184 | 203 | ac.sizeMu.RLock() | |
| 185 | - | claimSize, localSize := ac.geometryClaimSize, ac.size | |
| 204 | + | claimGeometry, localGeometry, localSize := ac.geometryClaimGeometry, ac.geometry, ac.size | |
| 186 | 205 | initialized := ac.geometryClaimInitialized | |
| 187 | 206 | ac.sizeMu.RUnlock() | |
| 188 | 207 | if !initialized { | |
| 189 | - | return localSize | |
| 208 | + | if localGeometry.Valid() { | |
| 209 | + | return localGeometry | |
| 210 | + | } | |
| 211 | + | return domain.Geometry{Size: localSize} | |
| 190 | 212 | } | |
| 191 | - | return claimSize | |
| 213 | + | return claimGeometry | |
| 192 | 214 | } | |
| 193 | 215 | ||
| 194 | - | // publishGeometryClaim makes the claim size and sequence visible as one | |
| 216 | + | // publishGeometryClaim makes the claim geometry and sequence visible as one | |
| 195 | 217 | // attachment-local publication under sizeMu. | |
| 196 | - | func (ac *attachedClient) publishGeometryClaim(size domain.Size, claim uint64) { | |
| 218 | + | func (ac *attachedClient) publishGeometryClaim(geometry domain.Geometry, claim uint64) { | |
| 197 | 219 | if ac == nil { | |
| 198 | 220 | return | |
| 199 | 221 | } | |
| 200 | 222 | ac.sizeMu.Lock() | |
| 201 | - | ac.geometryClaimSize = size | |
| 223 | + | ac.geometryClaimGeometry = geometry.NormalizePixels() | |
| 202 | 224 | ac.geometryClaimInitialized = true | |
| 203 | 225 | ac.geometryClaim.Store(claim) | |
| 204 | 226 | ac.sizeMu.Unlock() | |
| @@ -209,7 +231,7 @@ func (ac *attachedClient) clearGeometryClaim() { | |||
| 209 | 231 | return | |
| 210 | 232 | } | |
| 211 | 233 | ac.sizeMu.Lock() | |
| 212 | - | ac.geometryClaimSize = domain.Size{} | |
| 234 | + | ac.geometryClaimGeometry = domain.Geometry{} | |
| 213 | 235 | ac.geometryClaimInitialized = true | |
| 214 | 236 | ac.geometryClaim.Store(0) | |
| 215 | 237 | ac.sizeMu.Unlock() | |
| @@ -640,7 +662,7 @@ type attachClientOptions struct { | |||
| 640 | 662 | ||
| 641 | 663 | func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, opts attachClientOptions) (*attachedClient, error) { | |
| 642 | 664 | d.mu.Lock() | |
| 643 | - | ac := d.prepareAttachedClientLocked(tr, sz, opts) | |
| 665 | + | ac := d.prepareAttachedClientLocked(tr, domain.Geometry{Size: sz}, opts) | |
| 644 | 666 | d.mu.Unlock() | |
| 645 | 667 | result, err := d.transitionAttachment(attachmentTransitionRequest{ | |
| 646 | 668 | target: sess, | |
| @@ -679,7 +701,7 @@ func (d *Daemon) finishAttachedClient(sess *session, ac *attachedClient, opts at | |||
| 679 | 701 | // prepareAttachedClientLocked allocates one detached attachment. Caller holds | |
| 680 | 702 | // d.mu only to allocate its resume token; attachment publication happens later after | |
| 681 | 703 | // the caller releases every architecture lock. | |
| 682 | - | func (d *Daemon) prepareAttachedClientLocked(tr ports.Transport, sz domain.Size, opts attachClientOptions) *attachedClient { | |
| 704 | + | func (d *Daemon) prepareAttachedClientLocked(tr ports.Transport, geometry domain.Geometry, opts attachClientOptions) *attachedClient { | |
| 683 | 705 | resumeToken := uint64(0) | |
| 684 | 706 | if opts.resumeCapable { | |
| 685 | 707 | resumeToken = d.nextResumeTokenLocked() | |
| @@ -688,11 +710,13 @@ func (d *Daemon) prepareAttachedClientLocked(tr ports.Transport, sz domain.Size, | |||
| 688 | 710 | opts.terminalCapabilities = ports.TerminalCapabilities{ColorMode: ports.TerminalColorTrueColor} | |
| 689 | 711 | } | |
| 690 | 712 | output := newOutputStateStreamForCapabilities(opts.terminalCapabilities, opts.maxOutputInFlight) | |
| 713 | + | geometry = geometry.NormalizePixels() | |
| 691 | 714 | ac := &attachedClient{ | |
| 692 | 715 | tr: tr, | |
| 693 | 716 | output: output, | |
| 694 | - | size: sz, | |
| 695 | - | view: attachmentView{windowRows: sz.Rows, windowSet: true}, | |
| 717 | + | size: geometry.Size, | |
| 718 | + | geometry: geometry, | |
| 719 | + | view: attachmentView{windowRows: geometry.Rows, windowSet: true}, | |
| 696 | 720 | clientID: opts.clientID, | |
| 697 | 721 | terminalCapabilities: opts.terminalCapabilities, | |
| 698 | 722 | navigationCapabilities: opts.navigationCapabilities, | |
| @@ -941,7 +965,20 @@ func (d *Daemon) resize(sess *session, ac *attachedClient, sz domain.Size) { | |||
| 941 | 965 | // attachment effect admission; output state is rebased before any redraw so | |
| 942 | 966 | // every subsequent frame starts a new epoch. | |
| 943 | 967 | func (d *Daemon) resizeAttachmentForLease(token attachmentConnectionToken, size domain.Size) bool { | |
| 944 | - | if token.ac == nil || !size.Valid() || !token.attachmentEffectCurrent() { | |
| 968 | + | geometry := domain.Geometry{Size: size} | |
| 969 | + | if token.ac != nil { | |
| 970 | + | // Direct size-only callers do not have a fresh pixel pair. Preserve the | |
| 971 | + | // attachment's last complete pair instead of downgrading its claim to | |
| 972 | + | // cell-only geometry. | |
| 973 | + | geometry = token.ac.geometrySnapshot() | |
| 974 | + | geometry.Size = size | |
| 975 | + | } | |
| 976 | + | return d.resizeAttachmentGeometryForLease(token, geometry) | |
| 977 | + | } | |
| 978 | + | ||
| 979 | + | func (d *Daemon) resizeAttachmentGeometryForLease(token attachmentConnectionToken, geometry domain.Geometry) bool { | |
| 980 | + | geometry = geometry.NormalizePixels() | |
| 981 | + | if token.ac == nil || !geometry.Valid() || !token.attachmentEffectCurrent() { | |
| 945 | 982 | return false | |
| 946 | 983 | } | |
| 947 | 984 | ac := token.ac | |
| @@ -950,11 +987,15 @@ func (d *Daemon) resizeAttachmentForLease(token attachmentConnectionToken, size | |||
| 950 | 987 | ac.sendMu.Unlock() | |
| 951 | 988 | return false | |
| 952 | 989 | } | |
| 953 | - | sameSize := ac.sizeSnapshot() == size | |
| 990 | + | previous := ac.geometrySnapshot() | |
| 991 | + | sameSize := previous.Size == geometry.Size | |
| 992 | + | sameGeometry := previous == geometry | |
| 993 | + | if !sameGeometry { | |
| 994 | + | ac.setGeometry(geometry) | |
| 995 | + | } | |
| 954 | 996 | if !sameSize { | |
| 955 | - | ac.setSize(size) | |
| 956 | 997 | view := ac.viewSnapshot() | |
| 957 | - | view.windowRows = size.Rows | |
| 998 | + | view.windowRows = geometry.Rows | |
| 958 | 999 | view.windowSet = true | |
| 959 | 1000 | view.windowTop = 0 | |
| 960 | 1001 | view.revision++ | |
| @@ -983,7 +1024,7 @@ func (d *Daemon) resizeAttachmentForLease(token attachmentConnectionToken, size | |||
| 983 | 1024 | if d.recalculateSessionGeometryAndInvalidate(sess, ac, "client.go") { | |
| 984 | 1025 | return true | |
| 985 | 1026 | } | |
| 986 | - | if sameSize { | |
| 1027 | + | if sameGeometry { | |
| 987 | 1028 | return false | |
| 988 | 1029 | } | |
| 989 | 1030 | rc := sess.renderCoordinator() | |
| @@ -992,7 +1033,7 @@ func (d *Daemon) resizeAttachmentForLease(token attachmentConnectionToken, size | |||
| 992 | 1033 | } | |
| 993 | 1034 | // Rendering is attachment-scoped. Schedule it through the coordinator so | |
| 994 | 1035 | // rapid resize frames coalesce under the attachment lease and deadline. | |
| 995 | - | return rc.scheduleResizeForLease(size, ac, token.lease, func(uint64) { | |
| 1036 | + | return rc.scheduleResizeForLease(geometry.Size, ac, token.lease, func(uint64) { | |
| 996 | 1037 | go d.paint(sess, ac, true, token.lease) | |
| 997 | 1038 | }) != 0 | |
| 998 | 1039 | } | |
| @@ -72,9 +72,10 @@ type attachedClient struct { | |||||
| 72 | // sizeMu lets shared-geometry snapshots read attachment windows without | 72 | // sizeMu lets shared-geometry snapshots read attachment windows without | ||
| 73 | // waiting behind a blocked transport send. | 73 | // waiting behind a blocked transport send. | ||
| 74 | sizeMu sync.RWMutex | 74 | sizeMu sync.RWMutex | ||
| 75 | - | size domain.Size | 75 | + | size domain.Size // retained for legacy fixtures; live paths publish geometry |
| 76 | - | geometryClaimSize domain.Size // protected by sizeMu; shared geometry claim snapshot | 76 | + | geometry domain.Geometry // controlling-terminal geometry for this attachment |
| 77 | - | geometryClaimInitialized bool // protected by sizeMu; distinguishes legacy fixtures | 77 | + | geometryClaimGeometry domain.Geometry // protected by sizeMu; shared geometry claim snapshot |
| 78 | + | geometryClaimInitialized bool // protected by sizeMu; distinguishes legacy fixtures | |||
| 78 | // geometryClaim is an attachment-local monotonic claim sequence. The | 79 | // geometryClaim is an attachment-local monotonic claim sequence. The | ||
| 79 | // owning session publishes it whenever this attachment is attached, | 80 | // owning session publishes it whenever this attachment is attached, | ||
| 80 | // resumed, or explicitly resized. | 81 | // resumed, or explicitly resized. | ||
| @@ -169,36 +170,57 @@ func (ac *attachedClient) sizeSnapshot() domain.Size { | |||||
| 169 | } | 170 | } | ||
| 170 | 171 | ||||
| 171 | func (ac *attachedClient) setSize(size domain.Size) { | 172 | func (ac *attachedClient) setSize(size domain.Size) { | ||
| 173 | + | ac.setGeometry(domain.Geometry{Size: size}) | |||
| 174 | + | } | |||
| 175 | + | ||||
| 176 | + | func (ac *attachedClient) geometrySnapshot() domain.Geometry { | |||
| 177 | + | if ac == nil { | |||
| 178 | + | return domain.Geometry{} | |||
| 179 | + | } | |||
| 180 | + | ac.sizeMu.RLock() | |||
| 181 | + | defer ac.sizeMu.RUnlock() | |||
| 182 | + | if ac.geometry.Valid() { | |||
| 183 | + | return ac.geometry | |||
| 184 | + | } | |||
| 185 | + | return domain.Geometry{Size: ac.size} | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | func (ac *attachedClient) setGeometry(geometry domain.Geometry) { | |||
| 172 | if ac == nil { | 189 | if ac == nil { | ||
| 173 | return | 190 | return | ||
| 174 | } | 191 | } | ||
| 192 | + | geometry = geometry.NormalizePixels() | |||
| 175 | ac.sizeMu.Lock() | 193 | ac.sizeMu.Lock() | ||
| 176 | - | ac.size = size | 194 | + | ac.size = geometry.Size |
| 195 | + | ac.geometry = geometry | |||
| 177 | ac.sizeMu.Unlock() | 196 | ac.sizeMu.Unlock() | ||
| 178 | } | 197 | } | ||
| 179 | 198 | ||||
| 180 | - | func (ac *attachedClient) geometryClaimSizeSnapshot() domain.Size { | 199 | + | func (ac *attachedClient) geometryClaimSnapshot() domain.Geometry { |
| 181 | if ac == nil { | 200 | if ac == nil { | ||
| 182 | - | return domain.Size{} | 201 | + | return domain.Geometry{} |
| 183 | } | 202 | } | ||
| 184 | ac.sizeMu.RLock() | 203 | ac.sizeMu.RLock() | ||
| 185 | - | claimSize, localSize := ac.geometryClaimSize, ac.size | 204 | + | claimGeometry, localGeometry, localSize := ac.geometryClaimGeometry, ac.geometry, ac.size |
| 186 | initialized := ac.geometryClaimInitialized | 205 | initialized := ac.geometryClaimInitialized | ||
| 187 | ac.sizeMu.RUnlock() | 206 | ac.sizeMu.RUnlock() | ||
| 188 | if !initialized { | 207 | if !initialized { | ||
| 189 | - | return localSize | 208 | + | if localGeometry.Valid() { |
| 209 | + | return localGeometry | |||
| 210 | + | } | |||
| 211 | + | return domain.Geometry{Size: localSize} | |||
| 190 | } | 212 | } | ||
| 191 | - | return claimSize | 213 | + | return claimGeometry |
| 192 | } | 214 | } | ||
| 193 | 215 | ||||
| 194 | - | // publishGeometryClaim makes the claim size and sequence visible as one | 216 | + | // publishGeometryClaim makes the claim geometry and sequence visible as one |
| 195 | // attachment-local publication under sizeMu. | 217 | // attachment-local publication under sizeMu. | ||
| 196 | - | func (ac *attachedClient) publishGeometryClaim(size domain.Size, claim uint64) { | 218 | + | func (ac *attachedClient) publishGeometryClaim(geometry domain.Geometry, claim uint64) { |
| 197 | if ac == nil { | 219 | if ac == nil { | ||
| 198 | return | 220 | return | ||
| 199 | } | 221 | } | ||
| 200 | ac.sizeMu.Lock() | 222 | ac.sizeMu.Lock() | ||
| 201 | - | ac.geometryClaimSize = size | 223 | + | ac.geometryClaimGeometry = geometry.NormalizePixels() |
| 202 | ac.geometryClaimInitialized = true | 224 | ac.geometryClaimInitialized = true | ||
| 203 | ac.geometryClaim.Store(claim) | 225 | ac.geometryClaim.Store(claim) | ||
| 204 | ac.sizeMu.Unlock() | 226 | ac.sizeMu.Unlock() | ||
| @@ -209,7 +231,7 @@ func (ac *attachedClient) clearGeometryClaim() { | |||||
| 209 | return | 231 | return | ||
| 210 | } | 232 | } | ||
| 211 | ac.sizeMu.Lock() | 233 | ac.sizeMu.Lock() | ||
| 212 | - | ac.geometryClaimSize = domain.Size{} | 234 | + | ac.geometryClaimGeometry = domain.Geometry{} |
| 213 | ac.geometryClaimInitialized = true | 235 | ac.geometryClaimInitialized = true | ||
| 214 | ac.geometryClaim.Store(0) | 236 | ac.geometryClaim.Store(0) | ||
| 215 | ac.sizeMu.Unlock() | 237 | ac.sizeMu.Unlock() | ||
| @@ -640,7 +662,7 @@ type attachClientOptions struct { | |||||
| 640 | 662 | ||||
| 641 | func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, opts attachClientOptions) (*attachedClient, error) { | 663 | func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, opts attachClientOptions) (*attachedClient, error) { | ||
| 642 | d.mu.Lock() | 664 | d.mu.Lock() | ||
| 643 | - | ac := d.prepareAttachedClientLocked(tr, sz, opts) | 665 | + | ac := d.prepareAttachedClientLocked(tr, domain.Geometry{Size: sz}, opts) |
| 644 | d.mu.Unlock() | 666 | d.mu.Unlock() | ||
| 645 | result, err := d.transitionAttachment(attachmentTransitionRequest{ | 667 | result, err := d.transitionAttachment(attachmentTransitionRequest{ | ||
| 646 | target: sess, | 668 | target: sess, | ||
| @@ -679,7 +701,7 @@ func (d *Daemon) finishAttachedClient(sess *session, ac *attachedClient, opts at | |||||
| 679 | // prepareAttachedClientLocked allocates one detached attachment. Caller holds | 701 | // prepareAttachedClientLocked allocates one detached attachment. Caller holds | ||
| 680 | // d.mu only to allocate its resume token; attachment publication happens later after | 702 | // d.mu only to allocate its resume token; attachment publication happens later after | ||
| 681 | // the caller releases every architecture lock. | 703 | // the caller releases every architecture lock. | ||
| 682 | - | func (d *Daemon) prepareAttachedClientLocked(tr ports.Transport, sz domain.Size, opts attachClientOptions) *attachedClient { | 704 | + | func (d *Daemon) prepareAttachedClientLocked(tr ports.Transport, geometry domain.Geometry, opts attachClientOptions) *attachedClient { |
| 683 | resumeToken := uint64(0) | 705 | resumeToken := uint64(0) | ||
| 684 | if opts.resumeCapable { | 706 | if opts.resumeCapable { | ||
| 685 | resumeToken = d.nextResumeTokenLocked() | 707 | resumeToken = d.nextResumeTokenLocked() | ||
| @@ -688,11 +710,13 @@ func (d *Daemon) prepareAttachedClientLocked(tr ports.Transport, sz domain.Size, | |||||
| 688 | opts.terminalCapabilities = ports.TerminalCapabilities{ColorMode: ports.TerminalColorTrueColor} | 710 | opts.terminalCapabilities = ports.TerminalCapabilities{ColorMode: ports.TerminalColorTrueColor} | ||
| 689 | } | 711 | } | ||
| 690 | output := newOutputStateStreamForCapabilities(opts.terminalCapabilities, opts.maxOutputInFlight) | 712 | output := newOutputStateStreamForCapabilities(opts.terminalCapabilities, opts.maxOutputInFlight) | ||
| 713 | + | geometry = geometry.NormalizePixels() | |||
| 691 | ac := &attachedClient{ | 714 | ac := &attachedClient{ | ||
| 692 | tr: tr, | 715 | tr: tr, | ||
| 693 | output: output, | 716 | output: output, | ||
| 694 | - | size: sz, | 717 | + | size: geometry.Size, |
| 695 | - | view: attachmentView{windowRows: sz.Rows, windowSet: true}, | 718 | + | geometry: geometry, |
| 719 | + | view: attachmentView{windowRows: geometry.Rows, windowSet: true}, | |||
| 696 | clientID: opts.clientID, | 720 | clientID: opts.clientID, | ||
| 697 | terminalCapabilities: opts.terminalCapabilities, | 721 | terminalCapabilities: opts.terminalCapabilities, | ||
| 698 | navigationCapabilities: opts.navigationCapabilities, | 722 | navigationCapabilities: opts.navigationCapabilities, | ||
| @@ -941,7 +965,20 @@ func (d *Daemon) resize(sess *session, ac *attachedClient, sz domain.Size) { | |||||
| 941 | // attachment effect admission; output state is rebased before any redraw so | 965 | // attachment effect admission; output state is rebased before any redraw so | ||
| 942 | // every subsequent frame starts a new epoch. | 966 | // every subsequent frame starts a new epoch. | ||
| 943 | func (d *Daemon) resizeAttachmentForLease(token attachmentConnectionToken, size domain.Size) bool { | 967 | func (d *Daemon) resizeAttachmentForLease(token attachmentConnectionToken, size domain.Size) bool { | ||
| 944 | - | if token.ac == nil || !size.Valid() || !token.attachmentEffectCurrent() { | 968 | + | geometry := domain.Geometry{Size: size} |
| 969 | + | if token.ac != nil { | |||
| 970 | + | // Direct size-only callers do not have a fresh pixel pair. Preserve the | |||
| 971 | + | // attachment's last complete pair instead of downgrading its claim to | |||
| 972 | + | // cell-only geometry. | |||
| 973 | + | geometry = token.ac.geometrySnapshot() | |||
| 974 | + | geometry.Size = size | |||
| 975 | + | } | |||
| 976 | + | return d.resizeAttachmentGeometryForLease(token, geometry) | |||
| 977 | + | } | |||
| 978 | + | ||||
| 979 | + | func (d *Daemon) resizeAttachmentGeometryForLease(token attachmentConnectionToken, geometry domain.Geometry) bool { | |||
| 980 | + | geometry = geometry.NormalizePixels() | |||
| 981 | + | if token.ac == nil || !geometry.Valid() || !token.attachmentEffectCurrent() { | |||
| 945 | return false | 982 | return false | ||
| 946 | } | 983 | } | ||
| 947 | ac := token.ac | 984 | ac := token.ac | ||
| @@ -950,11 +987,15 @@ func (d *Daemon) resizeAttachmentForLease(token attachmentConnectionToken, size | |||||
| 950 | ac.sendMu.Unlock() | 987 | ac.sendMu.Unlock() | ||
| 951 | return false | 988 | return false | ||
| 952 | } | 989 | } | ||
| 953 | - | sameSize := ac.sizeSnapshot() == size | 990 | + | previous := ac.geometrySnapshot() |
| 991 | + | sameSize := previous.Size == geometry.Size | |||
| 992 | + | sameGeometry := previous == geometry | |||
| 993 | + | if !sameGeometry { | |||
| 994 | + | ac.setGeometry(geometry) | |||
| 995 | + | } | |||
| 954 | if !sameSize { | 996 | if !sameSize { | ||
| 955 | - | ac.setSize(size) | |||
| 956 | view := ac.viewSnapshot() | 997 | view := ac.viewSnapshot() | ||
| 957 | - | view.windowRows = size.Rows | 998 | + | view.windowRows = geometry.Rows |
| 958 | view.windowSet = true | 999 | view.windowSet = true | ||
| 959 | view.windowTop = 0 | 1000 | view.windowTop = 0 | ||
| 960 | view.revision++ | 1001 | view.revision++ | ||
| @@ -983,7 +1024,7 @@ func (d *Daemon) resizeAttachmentForLease(token attachmentConnectionToken, size | |||||
| 983 | if d.recalculateSessionGeometryAndInvalidate(sess, ac, "client.go") { | 1024 | if d.recalculateSessionGeometryAndInvalidate(sess, ac, "client.go") { | ||
| 984 | return true | 1025 | return true | ||
| 985 | } | 1026 | } | ||
| 986 | - | if sameSize { | 1027 | + | if sameGeometry { |
| 987 | return false | 1028 | return false | ||
| 988 | } | 1029 | } | ||
| 989 | rc := sess.renderCoordinator() | 1030 | rc := sess.renderCoordinator() | ||
| @@ -992,7 +1033,7 @@ func (d *Daemon) resizeAttachmentForLease(token attachmentConnectionToken, size | |||||
| 992 | } | 1033 | } | ||
| 993 | // Rendering is attachment-scoped. Schedule it through the coordinator so | 1034 | // Rendering is attachment-scoped. Schedule it through the coordinator so | ||
| 994 | // rapid resize frames coalesce under the attachment lease and deadline. | 1035 | // rapid resize frames coalesce under the attachment lease and deadline. | ||
| 995 | - | return rc.scheduleResizeForLease(size, ac, token.lease, func(uint64) { | 1036 | + | return rc.scheduleResizeForLease(geometry.Size, ac, token.lease, func(uint64) { |
| 996 | go d.paint(sess, ac, true, token.lease) | 1037 | go d.paint(sess, ac, true, token.lease) | ||
| 997 | }) != 0 | 1038 | }) != 0 | ||
| 998 | } | 1039 | } | ||
M internal/usecase/daemon/client_frame_routing.go +1 -1
| @@ -97,7 +97,7 @@ func (d *Daemon) handleAttachmentClientFrame(token attachmentConnectionToken, f | |||
| 97 | 97 | } | |
| 98 | 98 | case ports.MsgResize: | |
| 99 | 99 | if rz, derr := ports.UnmarshalResize(f.Payload); derr == nil && token.attachmentEffectCurrent() { | |
| 100 | - | d.resizeAttachmentForLease(token, rz.Size) | |
| 100 | + | d.resizeAttachmentGeometryForLease(token, rz.Geometry()) | |
| 101 | 101 | } | |
| 102 | 102 | case ports.MsgTheme: | |
| 103 | 103 | if th, derr := ports.UnmarshalTheme(f.Payload); derr == nil { | |
| @@ -97,7 +97,7 @@ func (d *Daemon) handleAttachmentClientFrame(token attachmentConnectionToken, f | |||||
| 97 | } | 97 | } | ||
| 98 | case ports.MsgResize: | 98 | case ports.MsgResize: | ||
| 99 | if rz, derr := ports.UnmarshalResize(f.Payload); derr == nil && token.attachmentEffectCurrent() { | 99 | if rz, derr := ports.UnmarshalResize(f.Payload); derr == nil && token.attachmentEffectCurrent() { | ||
| 100 | - | d.resizeAttachmentForLease(token, rz.Size) | 100 | + | d.resizeAttachmentGeometryForLease(token, rz.Geometry()) |
| 101 | } | 101 | } | ||
| 102 | case ports.MsgTheme: | 102 | case ports.MsgTheme: | ||
| 103 | if th, derr := ports.UnmarshalTheme(f.Payload); derr == nil { | 103 | if th, derr := ports.UnmarshalTheme(f.Payload); derr == nil { | ||
M internal/usecase/daemon/control.go +5 -2
| @@ -482,7 +482,10 @@ func (e controlExec) CreateSessionNamed(name string) error { | |||
| 482 | 482 | e.sess.mu.Lock() | |
| 483 | 483 | cwd, env := e.sess.cwd, copyEnvironment(e.sess.env) | |
| 484 | 484 | e.sess.mu.Unlock() | |
| 485 | - | size := e.sess.fullViewportSize() | |
| 485 | + | geometry := domain.Geometry{Size: e.sess.fullViewportSize()} | |
| 486 | + | if source, ok := e.sess.geometrySourceSnapshot(); ok { | |
| 487 | + | geometry = source.geometry | |
| 488 | + | } | |
| 486 | 489 | e.d.mu.Lock() | |
| 487 | 490 | defer e.d.mu.Unlock() | |
| 488 | 491 | if e.d.closing { | |
| @@ -491,7 +494,7 @@ func (e controlExec) CreateSessionNamed(name string) error { | |||
| 491 | 494 | if e.d.nameLiveOrStoppedLocked(name) { | |
| 492 | 495 | return errSessionNameInUse | |
| 493 | 496 | } | |
| 494 | - | _, err := e.d.createSessionLocked(name, false, cwd, size, env) | |
| 497 | + | _, err := e.d.createSessionLocked(name, false, cwd, geometry, env) | |
| 495 | 498 | return err | |
| 496 | 499 | } | |
| 497 | 500 | func (e controlExec) CloseTab() error { | |
| @@ -482,7 +482,10 @@ func (e controlExec) CreateSessionNamed(name string) error { | |||||
| 482 | e.sess.mu.Lock() | 482 | e.sess.mu.Lock() | ||
| 483 | cwd, env := e.sess.cwd, 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 | + | geometry := domain.Geometry{Size: e.sess.fullViewportSize()} |
| 486 | + | if source, ok := e.sess.geometrySourceSnapshot(); ok { | |||
| 487 | + | geometry = source.geometry | |||
| 488 | + | } | |||
| 486 | e.d.mu.Lock() | 489 | e.d.mu.Lock() | ||
| 487 | defer e.d.mu.Unlock() | 490 | defer e.d.mu.Unlock() | ||
| 488 | if e.d.closing { | 491 | if e.d.closing { | ||
| @@ -491,7 +494,7 @@ func (e controlExec) CreateSessionNamed(name string) error { | |||||
| 491 | if e.d.nameLiveOrStoppedLocked(name) { | 494 | if e.d.nameLiveOrStoppedLocked(name) { | ||
| 492 | return errSessionNameInUse | 495 | return errSessionNameInUse | ||
| 493 | } | 496 | } | ||
| 494 | - | _, err := e.d.createSessionLocked(name, false, cwd, size, env) | 497 | + | _, err := e.d.createSessionLocked(name, false, cwd, geometry, env) |
| 495 | return err | 498 | return err | ||
| 496 | } | 499 | } | ||
| 497 | func (e controlExec) CloseTab() error { | 500 | func (e controlExec) CloseTab() error { | ||
M internal/usecase/daemon/control_test.go +31 -1
| @@ -676,6 +676,36 @@ func TestHandleCommandNewSessionInheritsHeadlessIdentityAndViewport(t *testing.T | |||
| 676 | 676 | require.Equal(t, ports.ErrNameTaken, taken.Code) | |
| 677 | 677 | } | |
| 678 | 678 | ||
| 679 | + | func TestHandleCommandNewSessionPreservesSourceGeometry(t *testing.T) { | |
| 680 | + | factory := &controlPTYFactory{} | |
| 681 | + | d := newTestDaemon(t, factory, stubClock{}) | |
| 682 | + | t.Cleanup(func() { factory.close(); d.sessWg.Wait() }) | |
| 683 | + | source := addControlSession(d, "work", "t_work", "p_work") | |
| 684 | + | client := &attachedClient{} | |
| 685 | + | client.setGeometry(domain.Geometry{ | |
| 686 | + | Size: domain.Size{Cols: 120, Rows: 40}, PixelWidth: 1200, PixelHeight: 800, | |
| 687 | + | }) | |
| 688 | + | require.True(t, source.registerAttachment(client)) | |
| 689 | + | ||
| 690 | + | result := sendCommand(t, d, ports.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"}) | |
| 691 | + | ||
| 692 | + | require.True(t, result.OK, result.Text) | |
| 693 | + | d.mu.Lock() | |
| 694 | + | created := d.findByNameLocked("scripted") | |
| 695 | + | d.mu.Unlock() | |
| 696 | + | require.NotNil(t, created) | |
| 697 | + | created.mu.Lock() | |
| 698 | + | tb := created.tabs[0] | |
| 699 | + | created.mu.Unlock() | |
| 700 | + | tb.mu.Lock() | |
| 701 | + | pane := tb.focusedPane() | |
| 702 | + | require.NotNil(t, pane) | |
| 703 | + | require.Equal(t, domain.Geometry{ | |
| 704 | + | Size: domain.Size{Cols: 120, Rows: 38}, PixelWidth: 1200, PixelHeight: 760, | |
| 705 | + | }, pane.geometry) | |
| 706 | + | tb.mu.Unlock() | |
| 707 | + | } | |
| 708 | + | ||
| 679 | 709 | func TestHandleCommandValidatesToastAndQueuesForDetachedSession(t *testing.T) { | |
| 680 | 710 | d := newTestDaemon(t, nil, stubClock{}) | |
| 681 | 711 | sess := addControlSession(d, "work", "t_work", "p_work") | |
| @@ -1327,7 +1357,7 @@ type controlPTYFactory struct { | |||
| 1327 | 1357 | once sync.Once | |
| 1328 | 1358 | } | |
| 1329 | 1359 | ||
| 1330 | - | func (f *controlPTYFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1360 | + | func (f *controlPTYFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 1331 | 1361 | f.once.Do(func() { | |
| 1332 | 1362 | if f.entered != nil { | |
| 1333 | 1363 | close(f.entered) | |
| @@ -676,6 +676,36 @@ func TestHandleCommandNewSessionInheritsHeadlessIdentityAndViewport(t *testing.T | |||||
| 676 | require.Equal(t, ports.ErrNameTaken, taken.Code) | 676 | require.Equal(t, ports.ErrNameTaken, taken.Code) | ||
| 677 | } | 677 | } | ||
| 678 | 678 | ||||
| 679 | + | func TestHandleCommandNewSessionPreservesSourceGeometry(t *testing.T) { | |||
| 680 | + | factory := &controlPTYFactory{} | |||
| 681 | + | d := newTestDaemon(t, factory, stubClock{}) | |||
| 682 | + | t.Cleanup(func() { factory.close(); d.sessWg.Wait() }) | |||
| 683 | + | source := addControlSession(d, "work", "t_work", "p_work") | |||
| 684 | + | client := &attachedClient{} | |||
| 685 | + | client.setGeometry(domain.Geometry{ | |||
| 686 | + | Size: domain.Size{Cols: 120, Rows: 40}, PixelWidth: 1200, PixelHeight: 800, | |||
| 687 | + | }) | |||
| 688 | + | require.True(t, source.registerAttachment(client)) | |||
| 689 | + | ||||
| 690 | + | result := sendCommand(t, d, ports.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"}) | |||
| 691 | + | ||||
| 692 | + | require.True(t, result.OK, result.Text) | |||
| 693 | + | d.mu.Lock() | |||
| 694 | + | created := d.findByNameLocked("scripted") | |||
| 695 | + | d.mu.Unlock() | |||
| 696 | + | require.NotNil(t, created) | |||
| 697 | + | created.mu.Lock() | |||
| 698 | + | tb := created.tabs[0] | |||
| 699 | + | created.mu.Unlock() | |||
| 700 | + | tb.mu.Lock() | |||
| 701 | + | pane := tb.focusedPane() | |||
| 702 | + | require.NotNil(t, pane) | |||
| 703 | + | require.Equal(t, domain.Geometry{ | |||
| 704 | + | Size: domain.Size{Cols: 120, Rows: 38}, PixelWidth: 1200, PixelHeight: 760, | |||
| 705 | + | }, pane.geometry) | |||
| 706 | + | tb.mu.Unlock() | |||
| 707 | + | } | |||
| 708 | + | ||||
| 679 | func TestHandleCommandValidatesToastAndQueuesForDetachedSession(t *testing.T) { | 709 | func TestHandleCommandValidatesToastAndQueuesForDetachedSession(t *testing.T) { | ||
| 680 | d := newTestDaemon(t, nil, stubClock{}) | 710 | d := newTestDaemon(t, nil, stubClock{}) | ||
| 681 | sess := addControlSession(d, "work", "t_work", "p_work") | 711 | sess := addControlSession(d, "work", "t_work", "p_work") | ||
| @@ -1327,7 +1357,7 @@ type controlPTYFactory struct { | |||||
| 1327 | once sync.Once | 1357 | once sync.Once | ||
| 1328 | } | 1358 | } | ||
| 1329 | 1359 | ||||
| 1330 | - | func (f *controlPTYFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | 1360 | + | func (f *controlPTYFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 1331 | f.once.Do(func() { | 1361 | f.once.Do(func() { | ||
| 1332 | if f.entered != nil { | 1362 | if f.entered != nil { | ||
| 1333 | close(f.entered) | 1363 | close(f.entered) | ||
M internal/usecase/daemon/daemon.go +8 -4
| @@ -1320,7 +1320,11 @@ func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, | |||
| 1320 | 1320 | terminalCapabilities: terminalCapabilities, | |
| 1321 | 1321 | capabilitiesSet: true, | |
| 1322 | 1322 | } | |
| 1323 | - | ac := d.prepareAttachedClientLocked(tr, sz, opts) | |
| 1323 | + | geometry := h.Geometry() | |
| 1324 | + | if geometry.Size != sz { | |
| 1325 | + | geometry = domain.Geometry{Size: sz} | |
| 1326 | + | } | |
| 1327 | + | ac := d.prepareAttachedClientLocked(tr, geometry, opts) | |
| 1324 | 1328 | d.mu.Unlock() | |
| 1325 | 1329 | result, err := d.transitionAttachment(attachmentTransitionRequest{ | |
| 1326 | 1330 | target: sess, | |
| @@ -1535,7 +1539,7 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||
| 1535 | 1539 | switch h.Intent { | |
| 1536 | 1540 | case ports.IntentEphemeral: | |
| 1537 | 1541 | name := d.allocEphemeralNameLocked() | |
| 1538 | - | sess, err := d.createSessionLockedWithMode(name, true, h.Cwd, sz, h.Env) | |
| 1542 | + | sess, err := d.createSessionLockedWithMode(name, true, h.Cwd, h.Geometry(), h.Env) | |
| 1539 | 1543 | if err != nil { | |
| 1540 | 1544 | d.mu.Unlock() | |
| 1541 | 1545 | return nil, nil, err | |
| @@ -1556,7 +1560,7 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||
| 1556 | 1560 | d.mu.Unlock() | |
| 1557 | 1561 | return nil, nil, &protoErr{ports.ErrNameTaken, "session name already in use: " + h.Name} | |
| 1558 | 1562 | } | |
| 1559 | - | sess, err := d.createSessionLockedWithMode(h.Name, false, h.Cwd, sz, h.Env) | |
| 1563 | + | sess, err := d.createSessionLockedWithMode(h.Name, false, h.Cwd, h.Geometry(), h.Env) | |
| 1560 | 1564 | if err != nil { | |
| 1561 | 1565 | d.mu.Unlock() | |
| 1562 | 1566 | return nil, nil, err | |
| @@ -1579,7 +1583,7 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||
| 1579 | 1583 | env = copyEnvironment(d.baseEnv) | |
| 1580 | 1584 | } | |
| 1581 | 1585 | var err error | |
| 1582 | - | sess, err = d.resumeInactiveSessionLocked(h.Name, cwd, sz, env, stopped, stopped.tabNames) | |
| 1586 | + | sess, err = d.resumeInactiveSessionLocked(h.Name, cwd, h.Geometry(), env, stopped, stopped.tabNames) | |
| 1583 | 1587 | if err != nil { | |
| 1584 | 1588 | d.mu.Unlock() | |
| 1585 | 1589 | return nil, nil, err | |
| @@ -1320,7 +1320,11 @@ func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, | |||||
| 1320 | terminalCapabilities: terminalCapabilities, | 1320 | terminalCapabilities: terminalCapabilities, | ||
| 1321 | capabilitiesSet: true, | 1321 | capabilitiesSet: true, | ||
| 1322 | } | 1322 | } | ||
| 1323 | - | ac := d.prepareAttachedClientLocked(tr, sz, opts) | 1323 | + | geometry := h.Geometry() |
| 1324 | + | if geometry.Size != sz { | |||
| 1325 | + | geometry = domain.Geometry{Size: sz} | |||
| 1326 | + | } | |||
| 1327 | + | ac := d.prepareAttachedClientLocked(tr, geometry, opts) | |||
| 1324 | d.mu.Unlock() | 1328 | d.mu.Unlock() | ||
| 1325 | result, err := d.transitionAttachment(attachmentTransitionRequest{ | 1329 | result, err := d.transitionAttachment(attachmentTransitionRequest{ | ||
| 1326 | target: sess, | 1330 | target: sess, | ||
| @@ -1535,7 +1539,7 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||||
| 1535 | switch h.Intent { | 1539 | switch h.Intent { | ||
| 1536 | case ports.IntentEphemeral: | 1540 | case ports.IntentEphemeral: | ||
| 1537 | name := d.allocEphemeralNameLocked() | 1541 | name := d.allocEphemeralNameLocked() | ||
| 1538 | - | sess, err := d.createSessionLockedWithMode(name, true, h.Cwd, sz, h.Env) | 1542 | + | sess, err := d.createSessionLockedWithMode(name, true, h.Cwd, h.Geometry(), h.Env) |
| 1539 | if err != nil { | 1543 | if err != nil { | ||
| 1540 | d.mu.Unlock() | 1544 | d.mu.Unlock() | ||
| 1541 | return nil, nil, err | 1545 | return nil, nil, err | ||
| @@ -1556,7 +1560,7 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||||
| 1556 | d.mu.Unlock() | 1560 | d.mu.Unlock() | ||
| 1557 | return nil, nil, &protoErr{ports.ErrNameTaken, "session name already in use: " + h.Name} | 1561 | return nil, nil, &protoErr{ports.ErrNameTaken, "session name already in use: " + h.Name} | ||
| 1558 | } | 1562 | } | ||
| 1559 | - | sess, err := d.createSessionLockedWithMode(h.Name, false, h.Cwd, sz, h.Env) | 1563 | + | sess, err := d.createSessionLockedWithMode(h.Name, false, h.Cwd, h.Geometry(), h.Env) |
| 1560 | if err != nil { | 1564 | if err != nil { | ||
| 1561 | d.mu.Unlock() | 1565 | d.mu.Unlock() | ||
| 1562 | return nil, nil, err | 1566 | return nil, nil, err | ||
| @@ -1579,7 +1583,7 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T | |||||
| 1579 | env = copyEnvironment(d.baseEnv) | 1583 | env = copyEnvironment(d.baseEnv) | ||
| 1580 | } | 1584 | } | ||
| 1581 | var err error | 1585 | var err error | ||
| 1582 | - | sess, err = d.resumeInactiveSessionLocked(h.Name, cwd, sz, env, stopped, stopped.tabNames) | 1586 | + | sess, err = d.resumeInactiveSessionLocked(h.Name, cwd, h.Geometry(), env, stopped, stopped.tabNames) |
| 1583 | if err != nil { | 1587 | if err != nil { | ||
| 1584 | d.mu.Unlock() | 1588 | d.mu.Unlock() | ||
| 1585 | return nil, nil, err | 1589 | return nil, nil, err | ||
M internal/usecase/daemon/daemon_test.go +9 -9
| @@ -75,7 +75,7 @@ type terminalEnv struct { | |||
| 75 | 75 | func createSessionForTest(d *Daemon, name string, ephemeral bool, cwd string, sz domain.Size, _ terminalEnv, env []string, restoredTabNames ...[]string) (*session, error) { | |
| 76 | 76 | d.mu.Lock() | |
| 77 | 77 | defer d.mu.Unlock() | |
| 78 | - | return d.createSessionLocked(name, ephemeral, cwd, sz, env, restoredTabNames...) | |
| 78 | + | return d.createSessionLocked(name, ephemeral, cwd, domain.Geometry{Size: sz}, env, restoredTabNames...) | |
| 79 | 79 | } | |
| 80 | 80 | ||
| 81 | 81 | func newTestDaemonWithCleanup(t testing.TB, ptys ports.PTYFactory, clk ports.Clock, registerCleanup bool) *Daemon { | |
| @@ -158,7 +158,7 @@ func (p *quietPTY) Close() error { | |||
| 158 | 158 | } | |
| 159 | 159 | return nil | |
| 160 | 160 | } | |
| 161 | - | func (*quietPTY) Resize(domain.Size) error { return nil } | |
| 161 | + | func (*quietPTY) Resize(domain.Geometry) error { return nil } | |
| 162 | 162 | func (*quietPTY) Pid() int { return 0 } | |
| 163 | 163 | func (*quietPTY) ForegroundPgid() (int, error) { return 0, nil } | |
| 164 | 164 | ||
| @@ -179,7 +179,7 @@ func newBlockingOpenFactory(t *testing.T, d *Daemon) *blockingOpenFactory { | |||
| 179 | 179 | return f | |
| 180 | 180 | } | |
| 181 | 181 | ||
| 182 | - | func (f *blockingOpenFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 182 | + | func (f *blockingOpenFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 183 | 183 | <-f.release | |
| 184 | 184 | return nil, io.ErrClosedPipe | |
| 185 | 185 | } | |
| @@ -192,11 +192,11 @@ func newFactory(t *testing.T, p ports.PTY) *portsmocks.MockPTYFactory { | |||
| 192 | 192 | f := portsmocks.NewMockPTYFactory(t) | |
| 193 | 193 | var normal domain.Size | |
| 194 | 194 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 195 | - | func(_ context.Context, _ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { | |
| 195 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, geometry domain.Geometry) (ports.PTY, error) { | |
| 196 | 196 | if !normal.Valid() { | |
| 197 | - | normal = sz | |
| 197 | + | normal = geometry.Size | |
| 198 | 198 | } | |
| 199 | - | if sz == normal { | |
| 199 | + | if geometry.Size == normal { | |
| 200 | 200 | return p, nil | |
| 201 | 201 | } | |
| 202 | 202 | return newQuietPTY(), nil | |
| @@ -212,13 +212,13 @@ func newFactorySeq(t *testing.T, ptys ...ports.PTY) *portsmocks.MockPTYFactory { | |||
| 212 | 212 | var normal domain.Size | |
| 213 | 213 | next := 0 | |
| 214 | 214 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 215 | - | func(_ context.Context, _ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { | |
| 215 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, geometry domain.Geometry) (ports.PTY, error) { | |
| 216 | 216 | mu.Lock() | |
| 217 | 217 | defer mu.Unlock() | |
| 218 | 218 | if !normal.Valid() { | |
| 219 | - | normal = sz | |
| 219 | + | normal = geometry.Size | |
| 220 | 220 | } | |
| 221 | - | if sz != normal { | |
| 221 | + | if geometry.Size != normal { | |
| 222 | 222 | return newQuietPTY(), nil | |
| 223 | 223 | } | |
| 224 | 224 | if next >= len(ptys) { | |
| @@ -75,7 +75,7 @@ type terminalEnv struct { | |||||
| 75 | func createSessionForTest(d *Daemon, name string, ephemeral bool, cwd string, sz domain.Size, _ terminalEnv, env []string, restoredTabNames ...[]string) (*session, error) { | 75 | func createSessionForTest(d *Daemon, name string, ephemeral bool, cwd string, sz domain.Size, _ terminalEnv, env []string, restoredTabNames ...[]string) (*session, error) { | ||
| 76 | d.mu.Lock() | 76 | d.mu.Lock() | ||
| 77 | defer d.mu.Unlock() | 77 | defer d.mu.Unlock() | ||
| 78 | - | return d.createSessionLocked(name, ephemeral, cwd, sz, env, restoredTabNames...) | 78 | + | return d.createSessionLocked(name, ephemeral, cwd, domain.Geometry{Size: sz}, env, restoredTabNames...) |
| 79 | } | 79 | } | ||
| 80 | 80 | ||||
| 81 | 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 { | ||
| @@ -158,7 +158,7 @@ func (p *quietPTY) Close() error { | |||||
| 158 | } | 158 | } | ||
| 159 | return nil | 159 | return nil | ||
| 160 | } | 160 | } | ||
| 161 | - | func (*quietPTY) Resize(domain.Size) error { return nil } | 161 | + | func (*quietPTY) Resize(domain.Geometry) error { return nil } |
| 162 | func (*quietPTY) Pid() int { return 0 } | 162 | func (*quietPTY) Pid() int { return 0 } | ||
| 163 | func (*quietPTY) ForegroundPgid() (int, error) { return 0, nil } | 163 | func (*quietPTY) ForegroundPgid() (int, error) { return 0, nil } | ||
| 164 | 164 | ||||
| @@ -179,7 +179,7 @@ func newBlockingOpenFactory(t *testing.T, d *Daemon) *blockingOpenFactory { | |||||
| 179 | return f | 179 | return f | ||
| 180 | } | 180 | } | ||
| 181 | 181 | ||||
| 182 | - | func (f *blockingOpenFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | 182 | + | func (f *blockingOpenFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 183 | <-f.release | 183 | <-f.release | ||
| 184 | return nil, io.ErrClosedPipe | 184 | return nil, io.ErrClosedPipe | ||
| 185 | } | 185 | } | ||
| @@ -192,11 +192,11 @@ func newFactory(t *testing.T, p ports.PTY) *portsmocks.MockPTYFactory { | |||||
| 192 | f := portsmocks.NewMockPTYFactory(t) | 192 | f := portsmocks.NewMockPTYFactory(t) | ||
| 193 | var normal domain.Size | 193 | var normal domain.Size | ||
| 194 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 194 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | ||
| 195 | - | func(_ context.Context, _ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { | 195 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, geometry domain.Geometry) (ports.PTY, error) { |
| 196 | if !normal.Valid() { | 196 | if !normal.Valid() { | ||
| 197 | - | normal = sz | 197 | + | normal = geometry.Size |
| 198 | } | 198 | } | ||
| 199 | - | if sz == normal { | 199 | + | if geometry.Size == normal { |
| 200 | return p, nil | 200 | return p, nil | ||
| 201 | } | 201 | } | ||
| 202 | return newQuietPTY(), nil | 202 | return newQuietPTY(), nil | ||
| @@ -212,13 +212,13 @@ func newFactorySeq(t *testing.T, ptys ...ports.PTY) *portsmocks.MockPTYFactory { | |||||
| 212 | var normal domain.Size | 212 | var normal domain.Size | ||
| 213 | next := 0 | 213 | next := 0 | ||
| 214 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 214 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | ||
| 215 | - | func(_ context.Context, _ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { | 215 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, geometry domain.Geometry) (ports.PTY, error) { |
| 216 | mu.Lock() | 216 | mu.Lock() | ||
| 217 | defer mu.Unlock() | 217 | defer mu.Unlock() | ||
| 218 | if !normal.Valid() { | 218 | if !normal.Valid() { | ||
| 219 | - | normal = sz | 219 | + | normal = geometry.Size |
| 220 | } | 220 | } | ||
| 221 | - | if sz != normal { | 221 | + | if geometry.Size != normal { |
| 222 | return newQuietPTY(), nil | 222 | return newQuietPTY(), nil | ||
| 223 | } | 223 | } | ||
| 224 | if next >= len(ptys) { | 224 | if next >= len(ptys) { | ||
M internal/usecase/daemon/durable_recovery_test.go +1 -1
| @@ -520,7 +520,7 @@ type recoveryCountingPTYFactory struct { | |||
| 520 | 520 | calls atomic.Int32 | |
| 521 | 521 | } | |
| 522 | 522 | ||
| 523 | - | func (f *recoveryCountingPTYFactory) Open(context.Context, string, []string, []string, string, domain.Size) (ports.PTY, error) { | |
| 523 | + | func (f *recoveryCountingPTYFactory) Open(context.Context, string, []string, []string, string, domain.Geometry) (ports.PTY, error) { | |
| 524 | 524 | f.calls.Add(1) | |
| 525 | 525 | return nil, errors.New("unexpected PTY open") | |
| 526 | 526 | } | |
| @@ -520,7 +520,7 @@ type recoveryCountingPTYFactory struct { | |||||
| 520 | calls atomic.Int32 | 520 | calls atomic.Int32 | ||
| 521 | } | 521 | } | ||
| 522 | 522 | ||||
| 523 | - | func (f *recoveryCountingPTYFactory) Open(context.Context, string, []string, []string, string, domain.Size) (ports.PTY, error) { | 523 | + | func (f *recoveryCountingPTYFactory) Open(context.Context, string, []string, []string, string, domain.Geometry) (ports.PTY, error) { |
| 524 | f.calls.Add(1) | 524 | f.calls.Add(1) | ||
| 525 | return nil, errors.New("unexpected PTY open") | 525 | return nil, errors.New("unexpected PTY open") | ||
| 526 | } | 526 | } | ||
M internal/usecase/daemon/floating.go +6 -1
| @@ -316,6 +316,7 @@ type floatingLaunchSpec struct { | |||
| 316 | 316 | sessionName string | |
| 317 | 317 | cwd string | |
| 318 | 318 | size domain.Size | |
| 319 | + | ptyGeometry domain.Geometry | |
| 319 | 320 | geometry floatingGeometry | |
| 320 | 321 | paneStableID string | |
| 321 | 322 | env []string | |
| @@ -385,6 +386,7 @@ func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.Floati | |||
| 385 | 386 | sessionName: name, | |
| 386 | 387 | cwd: cwd, | |
| 387 | 388 | size: size, | |
| 389 | + | ptyGeometry: sess.paneGeometry(size), | |
| 388 | 390 | geometry: geometry, | |
| 389 | 391 | paneStableID: paneStableID, | |
| 390 | 392 | env: childEnvFrom(env, name, tabStableID, paneStableID), | |
| @@ -412,7 +414,7 @@ func (d *Daemon) openAndInstallFloating(sess *session, tb *tab, spec floatingLau | |||
| 412 | 414 | lifetime.abort() | |
| 413 | 415 | } | |
| 414 | 416 | }() | |
| 415 | - | pty, err := d.ptys.Open(lifetime.ctx, spec.command, spec.args, spec.env, spec.cwd, spec.size) | |
| 417 | + | pty, err := d.ptys.Open(lifetime.ctx, spec.command, spec.args, spec.env, spec.cwd, spec.ptyGeometry) | |
| 416 | 418 | if err != nil { | |
| 417 | 419 | // Open retains ownership of a nonnil PTY only on success. Some factory | |
| 418 | 420 | // implementations can return a partially opened PTY with an error, so | |
| @@ -424,7 +426,10 @@ func (d *Daemon) openAndInstallFloating(sess *session, tb *tab, spec floatingLau | |||
| 424 | 426 | d.failFloatingLaunch(sess, tb, generation, spec.userOpen, spec.sessionName, err) | |
| 425 | 427 | return | |
| 426 | 428 | } | |
| 429 | + | ptyGeometry := spec.ptyGeometry | |
| 427 | 430 | p := newPaneWithStableIDAndTitle(layout.PaneID("floating"), spec.paneStableID, pty, spec.size, spec.fallback) | |
| 431 | + | p.geometry = ptyGeometry | |
| 432 | + | setScreenGeometry(p.screen, ptyGeometry) | |
| 428 | 433 | p.rect = spec.geometry.ptyRect() | |
| 429 | 434 | p.popupGeometry = spec.geometry | |
| 430 | 435 | if !lifetime.publish(p) { | |
| @@ -316,6 +316,7 @@ type floatingLaunchSpec struct { | |||||
| 316 | sessionName string | 316 | sessionName string | ||
| 317 | cwd string | 317 | cwd string | ||
| 318 | size domain.Size | 318 | size domain.Size | ||
| 319 | + | ptyGeometry domain.Geometry | |||
| 319 | geometry floatingGeometry | 320 | geometry floatingGeometry | ||
| 320 | paneStableID string | 321 | paneStableID string | ||
| 321 | env []string | 322 | env []string | ||
| @@ -385,6 +386,7 @@ func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.Floati | |||||
| 385 | sessionName: name, | 386 | sessionName: name, | ||
| 386 | cwd: cwd, | 387 | cwd: cwd, | ||
| 387 | size: size, | 388 | size: size, | ||
| 389 | + | ptyGeometry: sess.paneGeometry(size), | |||
| 388 | geometry: geometry, | 390 | geometry: geometry, | ||
| 389 | paneStableID: paneStableID, | 391 | paneStableID: paneStableID, | ||
| 390 | env: childEnvFrom(env, name, tabStableID, paneStableID), | 392 | env: childEnvFrom(env, name, tabStableID, paneStableID), | ||
| @@ -412,7 +414,7 @@ func (d *Daemon) openAndInstallFloating(sess *session, tb *tab, spec floatingLau | |||||
| 412 | lifetime.abort() | 414 | lifetime.abort() | ||
| 413 | } | 415 | } | ||
| 414 | }() | 416 | }() | ||
| 415 | - | pty, err := d.ptys.Open(lifetime.ctx, spec.command, spec.args, spec.env, spec.cwd, spec.size) | 417 | + | pty, err := d.ptys.Open(lifetime.ctx, spec.command, spec.args, spec.env, spec.cwd, spec.ptyGeometry) |
| 416 | if err != nil { | 418 | if err != nil { | ||
| 417 | // Open retains ownership of a nonnil PTY only on success. Some factory | 419 | // Open retains ownership of a nonnil PTY only on success. Some factory | ||
| 418 | // implementations can return a partially opened PTY with an error, so | 420 | // implementations can return a partially opened PTY with an error, so | ||
| @@ -424,7 +426,10 @@ func (d *Daemon) openAndInstallFloating(sess *session, tb *tab, spec floatingLau | |||||
| 424 | d.failFloatingLaunch(sess, tb, generation, spec.userOpen, spec.sessionName, err) | 426 | d.failFloatingLaunch(sess, tb, generation, spec.userOpen, spec.sessionName, err) | ||
| 425 | return | 427 | return | ||
| 426 | } | 428 | } | ||
| 429 | + | ptyGeometry := spec.ptyGeometry | |||
| 427 | p := newPaneWithStableIDAndTitle(layout.PaneID("floating"), spec.paneStableID, pty, spec.size, spec.fallback) | 430 | p := newPaneWithStableIDAndTitle(layout.PaneID("floating"), spec.paneStableID, pty, spec.size, spec.fallback) | ||
| 431 | + | p.geometry = ptyGeometry | |||
| 432 | + | setScreenGeometry(p.screen, ptyGeometry) | |||
| 428 | p.rect = spec.geometry.ptyRect() | 433 | p.rect = spec.geometry.ptyRect() | ||
| 429 | p.popupGeometry = spec.geometry | 434 | p.popupGeometry = spec.geometry | ||
| 430 | if !lifetime.publish(p) { | 435 | if !lifetime.publish(p) { | ||
M internal/usecase/daemon/floating_open_test.go +10 -10
| @@ -38,9 +38,9 @@ func (p *contextAwareFloatingPTY) Read([]byte) (int, error) { | |||
| 38 | 38 | return 0, p.ctx.Err() | |
| 39 | 39 | } | |
| 40 | 40 | ||
| 41 | - | func (*contextAwareFloatingPTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 42 | - | func (*contextAwareFloatingPTY) Resize(domain.Size) error { return nil } | |
| 43 | - | func (*contextAwareFloatingPTY) Pid() int { return 1 } | |
| 41 | + | func (*contextAwareFloatingPTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 42 | + | func (*contextAwareFloatingPTY) Resize(domain.Geometry) error { return nil } | |
| 43 | + | func (*contextAwareFloatingPTY) Pid() int { return 1 } | |
| 44 | 44 | func (*contextAwareFloatingPTY) ForegroundPgid() (int, error) { | |
| 45 | 45 | return 1, nil | |
| 46 | 46 | } | |
| @@ -60,7 +60,7 @@ type contextAwareFloatingFactory struct { | |||
| 60 | 60 | onOpen func() | |
| 61 | 61 | } | |
| 62 | 62 | ||
| 63 | - | func (f *contextAwareFloatingFactory) Open(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 63 | + | func (f *contextAwareFloatingFactory) Open(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 64 | 64 | if f.pty != nil { | |
| 65 | 65 | f.pty.ctx = ctx | |
| 66 | 66 | } | |
| @@ -182,9 +182,9 @@ func (p *failedOpenPTY) Read([]byte) (int, error) { | |||
| 182 | 182 | } | |
| 183 | 183 | } | |
| 184 | 184 | ||
| 185 | - | func (*failedOpenPTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 186 | - | func (*failedOpenPTY) Resize(domain.Size) error { return nil } | |
| 187 | - | func (*failedOpenPTY) Pid() int { return 1 } | |
| 185 | + | func (*failedOpenPTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 186 | + | func (*failedOpenPTY) Resize(domain.Geometry) error { return nil } | |
| 187 | + | func (*failedOpenPTY) Pid() int { return 1 } | |
| 188 | 188 | func (*failedOpenPTY) ForegroundPgid() (int, error) { | |
| 189 | 189 | return 1, nil | |
| 190 | 190 | } | |
| @@ -214,7 +214,7 @@ func TestFloatingOpenErrorReleasesReturnedPTYBeforePublishingFailure(t *testing. | |||
| 214 | 214 | cancelCallbacks := atomic.Int32{} | |
| 215 | 215 | cancelled := make(chan struct{}) | |
| 216 | 216 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 217 | - | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 217 | + | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 218 | 218 | pty.ctx = ctx | |
| 219 | 219 | context.AfterFunc(ctx, func() { | |
| 220 | 220 | cancelCallbacks.Add(1) | |
| @@ -392,7 +392,7 @@ func TestFloatingOpenCancellationBoundsSessionTeardownAndClosesLatePTY(t *testin | |||
| 392 | 392 | closed := make(chan struct{}) | |
| 393 | 393 | latePTY.EXPECT().Close().RunAndReturn(func() error { close(closed); return nil }).Once() | |
| 394 | 394 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 395 | - | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 395 | + | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 396 | 396 | close(opened) | |
| 397 | 397 | <-ctx.Done() | |
| 398 | 398 | return latePTY, nil | |
| @@ -435,7 +435,7 @@ func TestFloatingOpensFromUnrelatedSessionsAreConcurrent(t *testing.T) { | |||
| 435 | 435 | release := make(chan struct{}) | |
| 436 | 436 | var once sync.Once | |
| 437 | 437 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 438 | - | func(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 438 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 439 | 439 | entered <- struct{}{} | |
| 440 | 440 | <-release | |
| 441 | 441 | return newQuietPTY(), nil | |
| @@ -38,9 +38,9 @@ func (p *contextAwareFloatingPTY) Read([]byte) (int, error) { | |||||
| 38 | return 0, p.ctx.Err() | 38 | return 0, p.ctx.Err() | ||
| 39 | } | 39 | } | ||
| 40 | 40 | ||||
| 41 | - | func (*contextAwareFloatingPTY) Write(b []byte) (int, error) { return len(b), nil } | 41 | + | func (*contextAwareFloatingPTY) Write(b []byte) (int, error) { return len(b), nil } |
| 42 | - | func (*contextAwareFloatingPTY) Resize(domain.Size) error { return nil } | 42 | + | func (*contextAwareFloatingPTY) Resize(domain.Geometry) error { return nil } |
| 43 | - | func (*contextAwareFloatingPTY) Pid() int { return 1 } | 43 | + | func (*contextAwareFloatingPTY) Pid() int { return 1 } |
| 44 | func (*contextAwareFloatingPTY) ForegroundPgid() (int, error) { | 44 | func (*contextAwareFloatingPTY) ForegroundPgid() (int, error) { | ||
| 45 | return 1, nil | 45 | return 1, nil | ||
| 46 | } | 46 | } | ||
| @@ -60,7 +60,7 @@ type contextAwareFloatingFactory struct { | |||||
| 60 | onOpen func() | 60 | onOpen func() | ||
| 61 | } | 61 | } | ||
| 62 | 62 | ||||
| 63 | - | func (f *contextAwareFloatingFactory) Open(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | 63 | + | func (f *contextAwareFloatingFactory) Open(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 64 | if f.pty != nil { | 64 | if f.pty != nil { | ||
| 65 | f.pty.ctx = ctx | 65 | f.pty.ctx = ctx | ||
| 66 | } | 66 | } | ||
| @@ -182,9 +182,9 @@ func (p *failedOpenPTY) Read([]byte) (int, error) { | |||||
| 182 | } | 182 | } | ||
| 183 | } | 183 | } | ||
| 184 | 184 | ||||
| 185 | - | func (*failedOpenPTY) Write(b []byte) (int, error) { return len(b), nil } | 185 | + | func (*failedOpenPTY) Write(b []byte) (int, error) { return len(b), nil } |
| 186 | - | func (*failedOpenPTY) Resize(domain.Size) error { return nil } | 186 | + | func (*failedOpenPTY) Resize(domain.Geometry) error { return nil } |
| 187 | - | func (*failedOpenPTY) Pid() int { return 1 } | 187 | + | func (*failedOpenPTY) Pid() int { return 1 } |
| 188 | func (*failedOpenPTY) ForegroundPgid() (int, error) { | 188 | func (*failedOpenPTY) ForegroundPgid() (int, error) { | ||
| 189 | return 1, nil | 189 | return 1, nil | ||
| 190 | } | 190 | } | ||
| @@ -214,7 +214,7 @@ func TestFloatingOpenErrorReleasesReturnedPTYBeforePublishingFailure(t *testing. | |||||
| 214 | cancelCallbacks := atomic.Int32{} | 214 | cancelCallbacks := atomic.Int32{} | ||
| 215 | cancelled := make(chan struct{}) | 215 | cancelled := make(chan struct{}) | ||
| 216 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 216 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | ||
| 217 | - | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | 217 | + | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 218 | pty.ctx = ctx | 218 | pty.ctx = ctx | ||
| 219 | context.AfterFunc(ctx, func() { | 219 | context.AfterFunc(ctx, func() { | ||
| 220 | cancelCallbacks.Add(1) | 220 | cancelCallbacks.Add(1) | ||
| @@ -392,7 +392,7 @@ func TestFloatingOpenCancellationBoundsSessionTeardownAndClosesLatePTY(t *testin | |||||
| 392 | closed := make(chan struct{}) | 392 | closed := make(chan struct{}) | ||
| 393 | latePTY.EXPECT().Close().RunAndReturn(func() error { close(closed); return nil }).Once() | 393 | latePTY.EXPECT().Close().RunAndReturn(func() error { close(closed); return nil }).Once() | ||
| 394 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 394 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | ||
| 395 | - | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | 395 | + | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 396 | close(opened) | 396 | close(opened) | ||
| 397 | <-ctx.Done() | 397 | <-ctx.Done() | ||
| 398 | return latePTY, nil | 398 | return latePTY, nil | ||
| @@ -435,7 +435,7 @@ func TestFloatingOpensFromUnrelatedSessionsAreConcurrent(t *testing.T) { | |||||
| 435 | release := make(chan struct{}) | 435 | release := make(chan struct{}) | ||
| 436 | var once sync.Once | 436 | var once sync.Once | ||
| 437 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 437 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | ||
| 438 | - | func(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | 438 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 439 | entered <- struct{}{} | 439 | entered <- struct{}{} | ||
| 440 | <-release | 440 | <-release | ||
| 441 | return newQuietPTY(), nil | 441 | return newQuietPTY(), nil | ||
M internal/usecase/daemon/floating_render_test.go +2 -2
| @@ -1012,9 +1012,9 @@ type resizePTY struct { | |||
| 1012 | 1012 | maxConcurrent int | |
| 1013 | 1013 | } | |
| 1014 | 1014 | ||
| 1015 | - | func (p *resizePTY) Resize(sz domain.Size) error { | |
| 1015 | + | func (p *resizePTY) Resize(geometry domain.Geometry) error { | |
| 1016 | 1016 | p.mu.Lock() | |
| 1017 | - | p.resizes = append(p.resizes, sz) | |
| 1017 | + | p.resizes = append(p.resizes, geometry.Size) | |
| 1018 | 1018 | p.activeCalls++ | |
| 1019 | 1019 | p.maxConcurrent = max(p.maxConcurrent, p.activeCalls) | |
| 1020 | 1020 | n := len(p.resizes) | |
| @@ -1012,9 +1012,9 @@ type resizePTY struct { | |||||
| 1012 | maxConcurrent int | 1012 | maxConcurrent int | ||
| 1013 | } | 1013 | } | ||
| 1014 | 1014 | ||||
| 1015 | - | func (p *resizePTY) Resize(sz domain.Size) error { | 1015 | + | func (p *resizePTY) Resize(geometry domain.Geometry) error { |
| 1016 | p.mu.Lock() | 1016 | p.mu.Lock() | ||
| 1017 | - | p.resizes = append(p.resizes, sz) | 1017 | + | p.resizes = append(p.resizes, geometry.Size) |
| 1018 | p.activeCalls++ | 1018 | p.activeCalls++ | ||
| 1019 | p.maxConcurrent = max(p.maxConcurrent, p.activeCalls) | 1019 | p.maxConcurrent = max(p.maxConcurrent, p.activeCalls) | ||
| 1020 | n := len(p.resizes) | 1020 | n := len(p.resizes) | ||
M internal/usecase/daemon/floating_test.go +4 -4
| @@ -39,8 +39,8 @@ func newGatedOpenFactory(t *testing.T, result ports.PTY, openErr error) (*portsm | |||
| 39 | 39 | releaseOpen := func() { releaseOnce.Do(func() { close(release) }) } | |
| 40 | 40 | t.Cleanup(releaseOpen) | |
| 41 | 41 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 42 | - | func(_ context.Context, command string, args, env []string, dir string, size domain.Size) (ports.PTY, error) { | |
| 43 | - | opened <- gatedFloatingOpen{command: command, args: append([]string(nil), args...), env: append([]string(nil), env...), dir: dir, size: size} | |
| 42 | + | func(_ context.Context, command string, args, env []string, dir string, geometry domain.Geometry) (ports.PTY, error) { | |
| 43 | + | opened <- gatedFloatingOpen{command: command, args: append([]string(nil), args...), env: append([]string(nil), env...), dir: dir, size: geometry.Size} | |
| 44 | 44 | <-release | |
| 45 | 45 | return result, openErr | |
| 46 | 46 | }).Once() | |
| @@ -223,7 +223,7 @@ func TestFloatingBlockedOpenReconcilesLatestResponsiveSize(t *testing.T) { | |||
| 223 | 223 | <-readerRelease | |
| 224 | 224 | return 0, io.EOF | |
| 225 | 225 | }).Once() | |
| 226 | - | floatingPTY.EXPECT().Resize(rectSize(latestGeometry.Inner)).Run(func(domain.Size) { close(resized) }).Return(nil).Once() | |
| 226 | + | floatingPTY.EXPECT().Resize(domain.Geometry{Size: rectSize(latestGeometry.Inner)}).Run(func(domain.Geometry) { close(resized) }).Return(nil).Once() | |
| 227 | 227 | floatingPTY.EXPECT().Close().RunAndReturn(func() error { | |
| 228 | 228 | select { | |
| 229 | 229 | case <-readerRelease: | |
| @@ -615,7 +615,7 @@ func TestFloatingLaunchUsesLiveOrValidatedSessionCwd(t *testing.T) { | |||
| 615 | 615 | factory := portsmocks.NewMockPTYFactory(t) | |
| 616 | 616 | opened := make(chan string, 1) | |
| 617 | 617 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 618 | - | func(_ context.Context, _ string, _ []string, _ []string, cwd string, _ domain.Size) (ports.PTY, error) { | |
| 618 | + | func(_ context.Context, _ string, _ []string, _ []string, cwd string, _ domain.Geometry) (ports.PTY, error) { | |
| 619 | 619 | opened <- cwd | |
| 620 | 620 | return nil, io.ErrUnexpectedEOF | |
| 621 | 621 | }).Once() | |
| @@ -39,8 +39,8 @@ func newGatedOpenFactory(t *testing.T, result ports.PTY, openErr error) (*portsm | |||||
| 39 | releaseOpen := func() { releaseOnce.Do(func() { close(release) }) } | 39 | releaseOpen := func() { releaseOnce.Do(func() { close(release) }) } | ||
| 40 | t.Cleanup(releaseOpen) | 40 | t.Cleanup(releaseOpen) | ||
| 41 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 41 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | ||
| 42 | - | func(_ context.Context, command string, args, env []string, dir string, size domain.Size) (ports.PTY, error) { | 42 | + | func(_ context.Context, command string, args, env []string, dir string, geometry domain.Geometry) (ports.PTY, error) { |
| 43 | - | opened <- gatedFloatingOpen{command: command, args: append([]string(nil), args...), env: append([]string(nil), env...), dir: dir, size: size} | 43 | + | opened <- gatedFloatingOpen{command: command, args: append([]string(nil), args...), env: append([]string(nil), env...), dir: dir, size: geometry.Size} |
| 44 | <-release | 44 | <-release | ||
| 45 | return result, openErr | 45 | return result, openErr | ||
| 46 | }).Once() | 46 | }).Once() | ||
| @@ -223,7 +223,7 @@ func TestFloatingBlockedOpenReconcilesLatestResponsiveSize(t *testing.T) { | |||||
| 223 | <-readerRelease | 223 | <-readerRelease | ||
| 224 | return 0, io.EOF | 224 | return 0, io.EOF | ||
| 225 | }).Once() | 225 | }).Once() | ||
| 226 | - | floatingPTY.EXPECT().Resize(rectSize(latestGeometry.Inner)).Run(func(domain.Size) { close(resized) }).Return(nil).Once() | 226 | + | floatingPTY.EXPECT().Resize(domain.Geometry{Size: rectSize(latestGeometry.Inner)}).Run(func(domain.Geometry) { close(resized) }).Return(nil).Once() |
| 227 | floatingPTY.EXPECT().Close().RunAndReturn(func() error { | 227 | floatingPTY.EXPECT().Close().RunAndReturn(func() error { | ||
| 228 | select { | 228 | select { | ||
| 229 | case <-readerRelease: | 229 | case <-readerRelease: | ||
| @@ -615,7 +615,7 @@ func TestFloatingLaunchUsesLiveOrValidatedSessionCwd(t *testing.T) { | |||||
| 615 | factory := portsmocks.NewMockPTYFactory(t) | 615 | factory := portsmocks.NewMockPTYFactory(t) | ||
| 616 | opened := make(chan string, 1) | 616 | opened := make(chan string, 1) | ||
| 617 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 617 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | ||
| 618 | - | func(_ context.Context, _ string, _ []string, _ []string, cwd string, _ domain.Size) (ports.PTY, error) { | 618 | + | func(_ context.Context, _ string, _ []string, _ []string, cwd string, _ domain.Geometry) (ports.PTY, error) { |
| 619 | opened <- cwd | 619 | opened <- cwd | ||
| 620 | return nil, io.ErrUnexpectedEOF | 620 | return nil, io.ErrUnexpectedEOF | ||
| 621 | }).Once() | 621 | }).Once() | ||
A internal/usecase/daemon/geometry_propagation_test.go +217 -0
| @@ -0,0 +1,217 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "io" | |
| 6 | + | "log/slog" | |
| 7 | + | "sync" | |
| 8 | + | "testing" | |
| 9 | + | ||
| 10 | + | vt "github.com/bnema/vev-vt" | |
| 11 | + | "github.com/bnema/vev/internal/domain" | |
| 12 | + | "github.com/bnema/vev/internal/ports" | |
| 13 | + | "github.com/stretchr/testify/require" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | func TestSessionPaneGeometryUsesLatestAttachmentClaimAndTruncates(t *testing.T) { | |
| 17 | + | sess := &session{} | |
| 18 | + | first := &attachedClient{} | |
| 19 | + | first.setGeometry(domain.Geometry{ | |
| 20 | + | Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480, | |
| 21 | + | }) | |
| 22 | + | second := &attachedClient{} | |
| 23 | + | second.setGeometry(domain.Geometry{ | |
| 24 | + | Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 803, PixelHeight: 487, | |
| 25 | + | }) | |
| 26 | + | ||
| 27 | + | require.True(t, sess.registerAttachment(first)) | |
| 28 | + | require.True(t, sess.registerAttachment(second)) | |
| 29 | + | require.Equal(t, domain.Geometry{ | |
| 30 | + | Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 391, PixelHeight: 466, | |
| 31 | + | }, sess.paneGeometry(domain.Size{Cols: 39, Rows: 23})) | |
| 32 | + | ||
| 33 | + | require.True(t, sess.unregisterAttachment(second)) | |
| 34 | + | require.Equal(t, domain.Geometry{ | |
| 35 | + | Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 390, PixelHeight: 460, | |
| 36 | + | }, sess.paneGeometry(domain.Size{Cols: 39, Rows: 23})) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | func TestPreparedLayoutPropagatesGeometryToPTYAndScreenQueries(t *testing.T) { | |
| 40 | + | geometry := domain.Geometry{ | |
| 41 | + | Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 391, PixelHeight: 466, | |
| 42 | + | } | |
| 43 | + | pty := &recordingGeometryPTY{} | |
| 44 | + | pane := &pane{pty: pty, screen: vt.NewScreen(1, 1), rect: domain.Rect{Width: 1, Height: 1}} | |
| 45 | + | plan := preparedTabLayout{members: []resizeMember{{pane: pane, rect: domain.Rect{Width: 39, Height: 23}, geometry: geometry}}} | |
| 46 | + | daemon := &Daemon{log: slog.New(slog.NewTextHandler(io.Discard, nil))} | |
| 47 | + | ||
| 48 | + | daemon.applyPreparedTabMembers(&plan) | |
| 49 | + | require.True(t, commitPreparedTabLayoutLocked(&plan)) | |
| 50 | + | require.Equal(t, geometry, pty.geometry) | |
| 51 | + | require.Equal(t, vt.Geometry{ | |
| 52 | + | Cols: 39, Rows: 23, PixelWidth: 391, PixelHeight: 466, | |
| 53 | + | }, pane.screen.Geometry()) | |
| 54 | + | ||
| 55 | + | var responses string | |
| 56 | + | pane.screen.OnResponse = func(response []byte) { responses += string(response) } | |
| 57 | + | pane.screen.Write([]byte("\x1b[14t\x1b[16t")) | |
| 58 | + | require.Equal(t, "\x1b[4;466;391t\x1b[6;20;10t", responses) | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | type recordingGeometryPTY struct { | |
| 62 | + | geometry domain.Geometry | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | func (p *recordingGeometryPTY) Read([]byte) (int, error) { return 0, io.EOF } | |
| 66 | + | func (p *recordingGeometryPTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 67 | + | func (p *recordingGeometryPTY) Close() error { return nil } | |
| 68 | + | func (p *recordingGeometryPTY) Resize(geometry domain.Geometry) error { | |
| 69 | + | p.geometry = geometry | |
| 70 | + | return nil | |
| 71 | + | } | |
| 72 | + | func (p *recordingGeometryPTY) Pid() int { return 1 } | |
| 73 | + | func (p *recordingGeometryPTY) ForegroundPgid() (int, error) { return 1, nil } | |
| 74 | + | ||
| 75 | + | func TestSizeOnlyGeometryClaimsPreservePixelsAndFallback(t *testing.T) { | |
| 76 | + | sess := &session{} | |
| 77 | + | first := &attachedClient{} | |
| 78 | + | first.setGeometry(domain.Geometry{ | |
| 79 | + | Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480, | |
| 80 | + | }) | |
| 81 | + | second := &attachedClient{} | |
| 82 | + | second.setGeometry(domain.Geometry{ | |
| 83 | + | Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 900, PixelHeight: 540, | |
| 84 | + | }) | |
| 85 | + | require.True(t, sess.registerAttachment(first)) | |
| 86 | + | require.True(t, sess.registerAttachment(second)) | |
| 87 | + | ||
| 88 | + | _, claimed := sess.claimGeometryOwnerForSize(second, domain.Size{Cols: 80, Rows: 24}) | |
| 89 | + | require.True(t, claimed) | |
| 90 | + | require.Equal(t, domain.Geometry{ | |
| 91 | + | Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 438, PixelHeight: 517, | |
| 92 | + | }, sess.paneGeometry(domain.Size{Cols: 39, Rows: 23})) | |
| 93 | + | ||
| 94 | + | require.True(t, sess.unregisterAttachment(second)) | |
| 95 | + | require.Equal(t, domain.Geometry{ | |
| 96 | + | Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 390, PixelHeight: 460, | |
| 97 | + | }, sess.paneGeometry(domain.Size{Cols: 39, Rows: 23})) | |
| 98 | + | ||
| 99 | + | partial := &attachedClient{} | |
| 100 | + | partial.setGeometry(domain.Geometry{ | |
| 101 | + | Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 901, | |
| 102 | + | }) | |
| 103 | + | require.True(t, sess.registerAttachment(partial)) | |
| 104 | + | require.Equal(t, domain.Geometry{Size: domain.Size{Cols: 39, Rows: 23}}, sess.paneGeometry(domain.Size{Cols: 39, Rows: 23}), "partial pixels must remain cell-only") | |
| 105 | + | require.True(t, sess.unregisterAttachment(partial)) | |
| 106 | + | require.Equal(t, domain.Geometry{ | |
| 107 | + | Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 390, PixelHeight: 460, | |
| 108 | + | }, sess.paneGeometry(domain.Size{Cols: 39, Rows: 23}), "fallback must restore the remaining complete pair") | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | func TestSizeOnlyLayoutAndFloatingRetainClaimingPixelGeometry(t *testing.T) { | |
| 112 | + | full := domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480} | |
| 113 | + | ac := &attachedClient{} | |
| 114 | + | ac.setGeometry(full) | |
| 115 | + | sess := &session{sessionCore: sessionCore{id: "geometry"}} | |
| 116 | + | require.True(t, sess.registerAttachment(ac)) | |
| 117 | + | ||
| 118 | + | layoutPTY := &recordingGeometryPTY{} | |
| 119 | + | tb := newTab(layoutPTY, domain.Size{Cols: 80, Rows: 22}) | |
| 120 | + | sess.tabs = []*tab{tb} | |
| 121 | + | d := newTestDaemon(t, nil, stubClock{}) | |
| 122 | + | require.True(t, d.applyTabLayout(sess, tb)) | |
| 123 | + | require.Equal(t, domain.Geometry{Size: domain.Size{Cols: 80, Rows: 22}, PixelWidth: 800, PixelHeight: 440}, layoutPTY.geometry) | |
| 124 | + | ||
| 125 | + | floatingPTY := &recordingGeometryPTY{} | |
| 126 | + | cfg := domain.FloatingConfig{Width: 50, Height: 50} | |
| 127 | + | d.ApplyConfig(domain.Config{Floating: cfg}) | |
| 128 | + | floatingGeometry := calculateContentFloatingGeometry(tb.size, cfg) | |
| 129 | + | floating := newPane("floating", floatingPTY, rectSize(floatingGeometry.Inner)) | |
| 130 | + | floating.popupGeometry = floatingGeometry | |
| 131 | + | tb.floating = floatingSlot{state: floatingVisible, pane: floating, generation: 1} | |
| 132 | + | failed, ok := d.applyVisibleFloatingLayout(sess, tb, nil) | |
| 133 | + | require.True(t, ok) | |
| 134 | + | require.Empty(t, failed) | |
| 135 | + | ptyRect := floatingGeometry.ptyRect() | |
| 136 | + | require.Equal(t, domain.Geometry{ | |
| 137 | + | Size: domain.Size{Cols: ptyRect.Width, Rows: ptyRect.Height}, | |
| 138 | + | PixelWidth: 800 * ptyRect.Width / 80, PixelHeight: 480 * ptyRect.Height / 24, | |
| 139 | + | }, floatingPTY.geometry) | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | type recordingPTYFactory struct { | |
| 143 | + | pty ports.PTY | |
| 144 | + | geometry domain.Geometry | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | func (f *recordingPTYFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, geometry domain.Geometry) (ports.PTY, error) { | |
| 148 | + | f.geometry = geometry | |
| 149 | + | return f.pty, nil | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | type blockingRecordingGeometryPTY struct { | |
| 153 | + | mu sync.Mutex | |
| 154 | + | geometry domain.Geometry | |
| 155 | + | done chan struct{} | |
| 156 | + | once sync.Once | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | func (p *blockingRecordingGeometryPTY) Read([]byte) (int, error) { | |
| 160 | + | <-p.done | |
| 161 | + | return 0, io.EOF | |
| 162 | + | } | |
| 163 | + | func (p *blockingRecordingGeometryPTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 164 | + | func (p *blockingRecordingGeometryPTY) Close() error { | |
| 165 | + | p.once.Do(func() { close(p.done) }) | |
| 166 | + | return nil | |
| 167 | + | } | |
| 168 | + | func (p *blockingRecordingGeometryPTY) Resize(geometry domain.Geometry) error { | |
| 169 | + | p.mu.Lock() | |
| 170 | + | p.geometry = geometry | |
| 171 | + | p.mu.Unlock() | |
| 172 | + | return nil | |
| 173 | + | } | |
| 174 | + | func (p *blockingRecordingGeometryPTY) geometrySnapshot() domain.Geometry { | |
| 175 | + | p.mu.Lock() | |
| 176 | + | defer p.mu.Unlock() | |
| 177 | + | return p.geometry | |
| 178 | + | } | |
| 179 | + | func (*blockingRecordingGeometryPTY) Pid() int { return 1 } | |
| 180 | + | func (*blockingRecordingGeometryPTY) ForegroundPgid() (int, error) { return 1, nil } | |
| 181 | + | ||
| 182 | + | func TestInitialSessionPTYReceivesClaimingPixelGeometry(t *testing.T) { | |
| 183 | + | pty := &recordingGeometryPTY{} | |
| 184 | + | factory := &recordingPTYFactory{pty: pty} | |
| 185 | + | d := newTestDaemon(t, factory, stubClock{}) | |
| 186 | + | geometry := domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480} | |
| 187 | + | ||
| 188 | + | d.mu.Lock() | |
| 189 | + | sess, err := d.createSessionLockedWithMode("pixels", true, "/tmp", geometry, nil) | |
| 190 | + | d.mu.Unlock() | |
| 191 | + | require.NoError(t, err) | |
| 192 | + | want := domain.Geometry{Size: domain.Size{Cols: 80, Rows: 22}, PixelWidth: 800, PixelHeight: 440} | |
| 193 | + | require.Equal(t, want, factory.geometry) | |
| 194 | + | require.Equal(t, want, sess.tabs[0].focusedPane().geometry) | |
| 195 | + | require.Equal(t, vt.Geometry{Cols: 80, Rows: 22, PixelWidth: 800, PixelHeight: 440}, sess.tabs[0].focusedPane().screen.Geometry()) | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | func TestResumeReappliesClaimingPixelGeometryBeforeReturn(t *testing.T) { | |
| 199 | + | pty := &blockingRecordingGeometryPTY{done: make(chan struct{})} | |
| 200 | + | t.Cleanup(func() { _ = pty.Close() }) | |
| 201 | + | d := newTestDaemon(t, &recordingPTYFactory{pty: pty}, stubClock{}) | |
| 202 | + | firstHello := helloResumeCapable(ports.IntentNew, "pixels", 0) | |
| 203 | + | firstHello.PixelWidth, firstHello.PixelHeight = 800, 480 | |
| 204 | + | firstTransport := &closeTrackingTransport{} | |
| 205 | + | sess, ac, err := d.route(firstHello, firstTransport) | |
| 206 | + | require.NoError(t, err) | |
| 207 | + | d.clientGone(sess, ac, firstTransport, false) | |
| 208 | + | ||
| 209 | + | resumeHello := helloResumeCapable(ports.IntentResume, "pixels", ac.resumeToken) | |
| 210 | + | resumeHello.PixelWidth, resumeHello.PixelHeight = 1000, 600 | |
| 211 | + | resumed, same, ok, err := d.resumeParked(resumeHello, &closeTrackingTransport{}, resumeHello.Size) | |
| 212 | + | require.NoError(t, err) | |
| 213 | + | require.True(t, ok) | |
| 214 | + | require.Same(t, sess, resumed) | |
| 215 | + | require.Same(t, ac, same) | |
| 216 | + | require.Equal(t, domain.Geometry{Size: domain.Size{Cols: 80, Rows: 22}, PixelWidth: 1000, PixelHeight: 550}, pty.geometrySnapshot()) | |
| 217 | + | } | |
| @@ -0,0 +1,217 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "io" | |||
| 6 | + | "log/slog" | |||
| 7 | + | "sync" | |||
| 8 | + | "testing" | |||
| 9 | + | ||||
| 10 | + | vt "github.com/bnema/vev-vt" | |||
| 11 | + | "github.com/bnema/vev/internal/domain" | |||
| 12 | + | "github.com/bnema/vev/internal/ports" | |||
| 13 | + | "github.com/stretchr/testify/require" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | func TestSessionPaneGeometryUsesLatestAttachmentClaimAndTruncates(t *testing.T) { | |||
| 17 | + | sess := &session{} | |||
| 18 | + | first := &attachedClient{} | |||
| 19 | + | first.setGeometry(domain.Geometry{ | |||
| 20 | + | Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480, | |||
| 21 | + | }) | |||
| 22 | + | second := &attachedClient{} | |||
| 23 | + | second.setGeometry(domain.Geometry{ | |||
| 24 | + | Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 803, PixelHeight: 487, | |||
| 25 | + | }) | |||
| 26 | + | ||||
| 27 | + | require.True(t, sess.registerAttachment(first)) | |||
| 28 | + | require.True(t, sess.registerAttachment(second)) | |||
| 29 | + | require.Equal(t, domain.Geometry{ | |||
| 30 | + | Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 391, PixelHeight: 466, | |||
| 31 | + | }, sess.paneGeometry(domain.Size{Cols: 39, Rows: 23})) | |||
| 32 | + | ||||
| 33 | + | require.True(t, sess.unregisterAttachment(second)) | |||
| 34 | + | require.Equal(t, domain.Geometry{ | |||
| 35 | + | Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 390, PixelHeight: 460, | |||
| 36 | + | }, sess.paneGeometry(domain.Size{Cols: 39, Rows: 23})) | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | func TestPreparedLayoutPropagatesGeometryToPTYAndScreenQueries(t *testing.T) { | |||
| 40 | + | geometry := domain.Geometry{ | |||
| 41 | + | Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 391, PixelHeight: 466, | |||
| 42 | + | } | |||
| 43 | + | pty := &recordingGeometryPTY{} | |||
| 44 | + | pane := &pane{pty: pty, screen: vt.NewScreen(1, 1), rect: domain.Rect{Width: 1, Height: 1}} | |||
| 45 | + | plan := preparedTabLayout{members: []resizeMember{{pane: pane, rect: domain.Rect{Width: 39, Height: 23}, geometry: geometry}}} | |||
| 46 | + | daemon := &Daemon{log: slog.New(slog.NewTextHandler(io.Discard, nil))} | |||
| 47 | + | ||||
| 48 | + | daemon.applyPreparedTabMembers(&plan) | |||
| 49 | + | require.True(t, commitPreparedTabLayoutLocked(&plan)) | |||
| 50 | + | require.Equal(t, geometry, pty.geometry) | |||
| 51 | + | require.Equal(t, vt.Geometry{ | |||
| 52 | + | Cols: 39, Rows: 23, PixelWidth: 391, PixelHeight: 466, | |||
| 53 | + | }, pane.screen.Geometry()) | |||
| 54 | + | ||||
| 55 | + | var responses string | |||
| 56 | + | pane.screen.OnResponse = func(response []byte) { responses += string(response) } | |||
| 57 | + | pane.screen.Write([]byte("\x1b[14t\x1b[16t")) | |||
| 58 | + | require.Equal(t, "\x1b[4;466;391t\x1b[6;20;10t", responses) | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | type recordingGeometryPTY struct { | |||
| 62 | + | geometry domain.Geometry | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | func (p *recordingGeometryPTY) Read([]byte) (int, error) { return 0, io.EOF } | |||
| 66 | + | func (p *recordingGeometryPTY) Write(b []byte) (int, error) { return len(b), nil } | |||
| 67 | + | func (p *recordingGeometryPTY) Close() error { return nil } | |||
| 68 | + | func (p *recordingGeometryPTY) Resize(geometry domain.Geometry) error { | |||
| 69 | + | p.geometry = geometry | |||
| 70 | + | return nil | |||
| 71 | + | } | |||
| 72 | + | func (p *recordingGeometryPTY) Pid() int { return 1 } | |||
| 73 | + | func (p *recordingGeometryPTY) ForegroundPgid() (int, error) { return 1, nil } | |||
| 74 | + | ||||
| 75 | + | func TestSizeOnlyGeometryClaimsPreservePixelsAndFallback(t *testing.T) { | |||
| 76 | + | sess := &session{} | |||
| 77 | + | first := &attachedClient{} | |||
| 78 | + | first.setGeometry(domain.Geometry{ | |||
| 79 | + | Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480, | |||
| 80 | + | }) | |||
| 81 | + | second := &attachedClient{} | |||
| 82 | + | second.setGeometry(domain.Geometry{ | |||
| 83 | + | Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 900, PixelHeight: 540, | |||
| 84 | + | }) | |||
| 85 | + | require.True(t, sess.registerAttachment(first)) | |||
| 86 | + | require.True(t, sess.registerAttachment(second)) | |||
| 87 | + | ||||
| 88 | + | _, claimed := sess.claimGeometryOwnerForSize(second, domain.Size{Cols: 80, Rows: 24}) | |||
| 89 | + | require.True(t, claimed) | |||
| 90 | + | require.Equal(t, domain.Geometry{ | |||
| 91 | + | Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 438, PixelHeight: 517, | |||
| 92 | + | }, sess.paneGeometry(domain.Size{Cols: 39, Rows: 23})) | |||
| 93 | + | ||||
| 94 | + | require.True(t, sess.unregisterAttachment(second)) | |||
| 95 | + | require.Equal(t, domain.Geometry{ | |||
| 96 | + | Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 390, PixelHeight: 460, | |||
| 97 | + | }, sess.paneGeometry(domain.Size{Cols: 39, Rows: 23})) | |||
| 98 | + | ||||
| 99 | + | partial := &attachedClient{} | |||
| 100 | + | partial.setGeometry(domain.Geometry{ | |||
| 101 | + | Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 901, | |||
| 102 | + | }) | |||
| 103 | + | require.True(t, sess.registerAttachment(partial)) | |||
| 104 | + | require.Equal(t, domain.Geometry{Size: domain.Size{Cols: 39, Rows: 23}}, sess.paneGeometry(domain.Size{Cols: 39, Rows: 23}), "partial pixels must remain cell-only") | |||
| 105 | + | require.True(t, sess.unregisterAttachment(partial)) | |||
| 106 | + | require.Equal(t, domain.Geometry{ | |||
| 107 | + | Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 390, PixelHeight: 460, | |||
| 108 | + | }, sess.paneGeometry(domain.Size{Cols: 39, Rows: 23}), "fallback must restore the remaining complete pair") | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | func TestSizeOnlyLayoutAndFloatingRetainClaimingPixelGeometry(t *testing.T) { | |||
| 112 | + | full := domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480} | |||
| 113 | + | ac := &attachedClient{} | |||
| 114 | + | ac.setGeometry(full) | |||
| 115 | + | sess := &session{sessionCore: sessionCore{id: "geometry"}} | |||
| 116 | + | require.True(t, sess.registerAttachment(ac)) | |||
| 117 | + | ||||
| 118 | + | layoutPTY := &recordingGeometryPTY{} | |||
| 119 | + | tb := newTab(layoutPTY, domain.Size{Cols: 80, Rows: 22}) | |||
| 120 | + | sess.tabs = []*tab{tb} | |||
| 121 | + | d := newTestDaemon(t, nil, stubClock{}) | |||
| 122 | + | require.True(t, d.applyTabLayout(sess, tb)) | |||
| 123 | + | require.Equal(t, domain.Geometry{Size: domain.Size{Cols: 80, Rows: 22}, PixelWidth: 800, PixelHeight: 440}, layoutPTY.geometry) | |||
| 124 | + | ||||
| 125 | + | floatingPTY := &recordingGeometryPTY{} | |||
| 126 | + | cfg := domain.FloatingConfig{Width: 50, Height: 50} | |||
| 127 | + | d.ApplyConfig(domain.Config{Floating: cfg}) | |||
| 128 | + | floatingGeometry := calculateContentFloatingGeometry(tb.size, cfg) | |||
| 129 | + | floating := newPane("floating", floatingPTY, rectSize(floatingGeometry.Inner)) | |||
| 130 | + | floating.popupGeometry = floatingGeometry | |||
| 131 | + | tb.floating = floatingSlot{state: floatingVisible, pane: floating, generation: 1} | |||
| 132 | + | failed, ok := d.applyVisibleFloatingLayout(sess, tb, nil) | |||
| 133 | + | require.True(t, ok) | |||
| 134 | + | require.Empty(t, failed) | |||
| 135 | + | ptyRect := floatingGeometry.ptyRect() | |||
| 136 | + | require.Equal(t, domain.Geometry{ | |||
| 137 | + | Size: domain.Size{Cols: ptyRect.Width, Rows: ptyRect.Height}, | |||
| 138 | + | PixelWidth: 800 * ptyRect.Width / 80, PixelHeight: 480 * ptyRect.Height / 24, | |||
| 139 | + | }, floatingPTY.geometry) | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | type recordingPTYFactory struct { | |||
| 143 | + | pty ports.PTY | |||
| 144 | + | geometry domain.Geometry | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | func (f *recordingPTYFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, geometry domain.Geometry) (ports.PTY, error) { | |||
| 148 | + | f.geometry = geometry | |||
| 149 | + | return f.pty, nil | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | type blockingRecordingGeometryPTY struct { | |||
| 153 | + | mu sync.Mutex | |||
| 154 | + | geometry domain.Geometry | |||
| 155 | + | done chan struct{} | |||
| 156 | + | once sync.Once | |||
| 157 | + | } | |||
| 158 | + | ||||
| 159 | + | func (p *blockingRecordingGeometryPTY) Read([]byte) (int, error) { | |||
| 160 | + | <-p.done | |||
| 161 | + | return 0, io.EOF | |||
| 162 | + | } | |||
| 163 | + | func (p *blockingRecordingGeometryPTY) Write(b []byte) (int, error) { return len(b), nil } | |||
| 164 | + | func (p *blockingRecordingGeometryPTY) Close() error { | |||
| 165 | + | p.once.Do(func() { close(p.done) }) | |||
| 166 | + | return nil | |||
| 167 | + | } | |||
| 168 | + | func (p *blockingRecordingGeometryPTY) Resize(geometry domain.Geometry) error { | |||
| 169 | + | p.mu.Lock() | |||
| 170 | + | p.geometry = geometry | |||
| 171 | + | p.mu.Unlock() | |||
| 172 | + | return nil | |||
| 173 | + | } | |||
| 174 | + | func (p *blockingRecordingGeometryPTY) geometrySnapshot() domain.Geometry { | |||
| 175 | + | p.mu.Lock() | |||
| 176 | + | defer p.mu.Unlock() | |||
| 177 | + | return p.geometry | |||
| 178 | + | } | |||
| 179 | + | func (*blockingRecordingGeometryPTY) Pid() int { return 1 } | |||
| 180 | + | func (*blockingRecordingGeometryPTY) ForegroundPgid() (int, error) { return 1, nil } | |||
| 181 | + | ||||
| 182 | + | func TestInitialSessionPTYReceivesClaimingPixelGeometry(t *testing.T) { | |||
| 183 | + | pty := &recordingGeometryPTY{} | |||
| 184 | + | factory := &recordingPTYFactory{pty: pty} | |||
| 185 | + | d := newTestDaemon(t, factory, stubClock{}) | |||
| 186 | + | geometry := domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480} | |||
| 187 | + | ||||
| 188 | + | d.mu.Lock() | |||
| 189 | + | sess, err := d.createSessionLockedWithMode("pixels", true, "/tmp", geometry, nil) | |||
| 190 | + | d.mu.Unlock() | |||
| 191 | + | require.NoError(t, err) | |||
| 192 | + | want := domain.Geometry{Size: domain.Size{Cols: 80, Rows: 22}, PixelWidth: 800, PixelHeight: 440} | |||
| 193 | + | require.Equal(t, want, factory.geometry) | |||
| 194 | + | require.Equal(t, want, sess.tabs[0].focusedPane().geometry) | |||
| 195 | + | require.Equal(t, vt.Geometry{Cols: 80, Rows: 22, PixelWidth: 800, PixelHeight: 440}, sess.tabs[0].focusedPane().screen.Geometry()) | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | func TestResumeReappliesClaimingPixelGeometryBeforeReturn(t *testing.T) { | |||
| 199 | + | pty := &blockingRecordingGeometryPTY{done: make(chan struct{})} | |||
| 200 | + | t.Cleanup(func() { _ = pty.Close() }) | |||
| 201 | + | d := newTestDaemon(t, &recordingPTYFactory{pty: pty}, stubClock{}) | |||
| 202 | + | firstHello := helloResumeCapable(ports.IntentNew, "pixels", 0) | |||
| 203 | + | firstHello.PixelWidth, firstHello.PixelHeight = 800, 480 | |||
| 204 | + | firstTransport := &closeTrackingTransport{} | |||
| 205 | + | sess, ac, err := d.route(firstHello, firstTransport) | |||
| 206 | + | require.NoError(t, err) | |||
| 207 | + | d.clientGone(sess, ac, firstTransport, false) | |||
| 208 | + | ||||
| 209 | + | resumeHello := helloResumeCapable(ports.IntentResume, "pixels", ac.resumeToken) | |||
| 210 | + | resumeHello.PixelWidth, resumeHello.PixelHeight = 1000, 600 | |||
| 211 | + | resumed, same, ok, err := d.resumeParked(resumeHello, &closeTrackingTransport{}, resumeHello.Size) | |||
| 212 | + | require.NoError(t, err) | |||
| 213 | + | require.True(t, ok) | |||
| 214 | + | require.Same(t, sess, resumed) | |||
| 215 | + | require.Same(t, ac, same) | |||
| 216 | + | require.Equal(t, domain.Geometry{Size: domain.Size{Cols: 80, Rows: 22}, PixelWidth: 1000, PixelHeight: 550}, pty.geometrySnapshot()) | |||
| 217 | + | } | |||
M internal/usecase/daemon/input_test.go +10 -10
| @@ -1289,9 +1289,9 @@ func TestMouseSplitReportPreservesOrder(t *testing.T) { | |||
| 1289 | 1289 | ||
| 1290 | 1290 | func TestMouseWheelOverUnfocusedPaneDoesNotFocusAndForwardsChildMouse(t *testing.T) { | |
| 1291 | 1291 | p1 := portsmocks.NewMockPTY(t) | |
| 1292 | - | p1.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe() | |
| 1292 | + | p1.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe() | |
| 1293 | 1293 | p2 := portsmocks.NewMockPTY(t) | |
| 1294 | - | p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe() | |
| 1294 | + | p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe() | |
| 1295 | 1295 | p2.EXPECT().Write([]byte("\x1b[<64;1;1M")).Return(len("\x1b[<64;1;1M"), nil).Once() | |
| 1296 | 1296 | d, sess, ac, _ := newManualSessionWithPTYs(t, p1) | |
| 1297 | 1297 | d.procComm = nil | |
| @@ -1495,9 +1495,9 @@ func TestMouseDividerAndTitleBarDoNotForwardBogusCoordinates(t *testing.T) { | |||
| 1495 | 1495 | ||
| 1496 | 1496 | func TestCopyModeDragOutsideSplitPaneClampsToPaneContent(t *testing.T) { | |
| 1497 | 1497 | p1 := portsmocks.NewMockPTY(t) | |
| 1498 | - | p1.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Maybe() | |
| 1498 | + | p1.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Maybe() | |
| 1499 | 1499 | p2 := portsmocks.NewMockPTY(t) | |
| 1500 | - | p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Maybe() | |
| 1500 | + | p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Maybe() | |
| 1501 | 1501 | d, sess, ac, sends := newManualSessionWithPTYs(t, p1) | |
| 1502 | 1502 | d.procComm = nil | |
| 1503 | 1503 | tb := testAttachmentTab(sess) | |
| @@ -1528,9 +1528,9 @@ func TestCopyModeDragOutsideSplitPaneClampsToPaneContent(t *testing.T) { | |||
| 1528 | 1528 | ||
| 1529 | 1529 | func TestMouseHitTestFocusesPaneAndTranslatesSGRColumns(t *testing.T) { | |
| 1530 | 1530 | p1 := portsmocks.NewMockPTY(t) | |
| 1531 | - | p1.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe() | |
| 1531 | + | p1.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe() | |
| 1532 | 1532 | p2 := portsmocks.NewMockPTY(t) | |
| 1533 | - | p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe() | |
| 1533 | + | p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe() | |
| 1534 | 1534 | p2.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once() | |
| 1535 | 1535 | d, sess, ac, _ := newManualSessionWithPTYs(t, p1) | |
| 1536 | 1536 | d.procComm = nil | |
| @@ -1556,9 +1556,9 @@ func TestMouseHitTestFocusesPaneAndTranslatesSGRColumns(t *testing.T) { | |||
| 1556 | 1556 | // the modal's coordinates. | |
| 1557 | 1557 | func TestMouseGatedWhileNoticesOverlayActive(t *testing.T) { | |
| 1558 | 1558 | p1 := portsmocks.NewMockPTY(t) | |
| 1559 | - | p1.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe() | |
| 1559 | + | p1.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe() | |
| 1560 | 1560 | p2 := portsmocks.NewMockPTY(t) | |
| 1561 | - | p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe() | |
| 1561 | + | p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe() | |
| 1562 | 1562 | d, sess, ac, sends := newManualSessionWithPTYs(t, p1) | |
| 1563 | 1563 | d.procComm = nil | |
| 1564 | 1564 | tb := testAttachmentTab(sess) | |
| @@ -1586,9 +1586,9 @@ func TestMouseGatedWhileNoticesOverlayActive(t *testing.T) { | |||
| 1586 | 1586 | ||
| 1587 | 1587 | func TestMouseCollapsedStackBarExpandsAndFocuses(t *testing.T) { | |
| 1588 | 1588 | p1 := portsmocks.NewMockPTY(t) | |
| 1589 | - | p1.EXPECT().Resize(domain.Size{Cols: 20, Rows: 4}).Return(nil).Maybe() | |
| 1589 | + | p1.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 4}}).Return(nil).Maybe() | |
| 1590 | 1590 | p2 := portsmocks.NewMockPTY(t) | |
| 1591 | - | p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 4}).Return(nil).Maybe() | |
| 1591 | + | p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 4}}).Return(nil).Maybe() | |
| 1592 | 1592 | d, sess, ac, _ := newManualSessionWithPTYs(t, p1) | |
| 1593 | 1593 | d.procComm = nil | |
| 1594 | 1594 | tb := testAttachmentTab(sess) | |
| @@ -1289,9 +1289,9 @@ func TestMouseSplitReportPreservesOrder(t *testing.T) { | |||||
| 1289 | 1289 | ||||
| 1290 | func TestMouseWheelOverUnfocusedPaneDoesNotFocusAndForwardsChildMouse(t *testing.T) { | 1290 | func TestMouseWheelOverUnfocusedPaneDoesNotFocusAndForwardsChildMouse(t *testing.T) { | ||
| 1291 | p1 := portsmocks.NewMockPTY(t) | 1291 | p1 := portsmocks.NewMockPTY(t) | ||
| 1292 | - | p1.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe() | 1292 | + | p1.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe() |
| 1293 | p2 := portsmocks.NewMockPTY(t) | 1293 | p2 := portsmocks.NewMockPTY(t) | ||
| 1294 | - | p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe() | 1294 | + | p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe() |
| 1295 | p2.EXPECT().Write([]byte("\x1b[<64;1;1M")).Return(len("\x1b[<64;1;1M"), nil).Once() | 1295 | p2.EXPECT().Write([]byte("\x1b[<64;1;1M")).Return(len("\x1b[<64;1;1M"), nil).Once() | ||
| 1296 | d, sess, ac, _ := newManualSessionWithPTYs(t, p1) | 1296 | d, sess, ac, _ := newManualSessionWithPTYs(t, p1) | ||
| 1297 | d.procComm = nil | 1297 | d.procComm = nil | ||
| @@ -1495,9 +1495,9 @@ func TestMouseDividerAndTitleBarDoNotForwardBogusCoordinates(t *testing.T) { | |||||
| 1495 | 1495 | ||||
| 1496 | func TestCopyModeDragOutsideSplitPaneClampsToPaneContent(t *testing.T) { | 1496 | func TestCopyModeDragOutsideSplitPaneClampsToPaneContent(t *testing.T) { | ||
| 1497 | p1 := portsmocks.NewMockPTY(t) | 1497 | p1 := portsmocks.NewMockPTY(t) | ||
| 1498 | - | p1.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Maybe() | 1498 | + | p1.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Maybe() |
| 1499 | p2 := portsmocks.NewMockPTY(t) | 1499 | p2 := portsmocks.NewMockPTY(t) | ||
| 1500 | - | p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Maybe() | 1500 | + | p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Maybe() |
| 1501 | d, sess, ac, sends := newManualSessionWithPTYs(t, p1) | 1501 | d, sess, ac, sends := newManualSessionWithPTYs(t, p1) | ||
| 1502 | d.procComm = nil | 1502 | d.procComm = nil | ||
| 1503 | tb := testAttachmentTab(sess) | 1503 | tb := testAttachmentTab(sess) | ||
| @@ -1528,9 +1528,9 @@ func TestCopyModeDragOutsideSplitPaneClampsToPaneContent(t *testing.T) { | |||||
| 1528 | 1528 | ||||
| 1529 | func TestMouseHitTestFocusesPaneAndTranslatesSGRColumns(t *testing.T) { | 1529 | func TestMouseHitTestFocusesPaneAndTranslatesSGRColumns(t *testing.T) { | ||
| 1530 | p1 := portsmocks.NewMockPTY(t) | 1530 | p1 := portsmocks.NewMockPTY(t) | ||
| 1531 | - | p1.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe() | 1531 | + | p1.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe() |
| 1532 | p2 := portsmocks.NewMockPTY(t) | 1532 | p2 := portsmocks.NewMockPTY(t) | ||
| 1533 | - | p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe() | 1533 | + | p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe() |
| 1534 | p2.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once() | 1534 | p2.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once() | ||
| 1535 | d, sess, ac, _ := newManualSessionWithPTYs(t, p1) | 1535 | d, sess, ac, _ := newManualSessionWithPTYs(t, p1) | ||
| 1536 | d.procComm = nil | 1536 | d.procComm = nil | ||
| @@ -1556,9 +1556,9 @@ func TestMouseHitTestFocusesPaneAndTranslatesSGRColumns(t *testing.T) { | |||||
| 1556 | // the modal's coordinates. | 1556 | // the modal's coordinates. | ||
| 1557 | func TestMouseGatedWhileNoticesOverlayActive(t *testing.T) { | 1557 | func TestMouseGatedWhileNoticesOverlayActive(t *testing.T) { | ||
| 1558 | p1 := portsmocks.NewMockPTY(t) | 1558 | p1 := portsmocks.NewMockPTY(t) | ||
| 1559 | - | p1.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe() | 1559 | + | p1.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe() |
| 1560 | p2 := portsmocks.NewMockPTY(t) | 1560 | p2 := portsmocks.NewMockPTY(t) | ||
| 1561 | - | p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 5}).Return(nil).Maybe() | 1561 | + | p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe() |
| 1562 | d, sess, ac, sends := newManualSessionWithPTYs(t, p1) | 1562 | d, sess, ac, sends := newManualSessionWithPTYs(t, p1) | ||
| 1563 | d.procComm = nil | 1563 | d.procComm = nil | ||
| 1564 | tb := testAttachmentTab(sess) | 1564 | tb := testAttachmentTab(sess) | ||
| @@ -1586,9 +1586,9 @@ func TestMouseGatedWhileNoticesOverlayActive(t *testing.T) { | |||||
| 1586 | 1586 | ||||
| 1587 | func TestMouseCollapsedStackBarExpandsAndFocuses(t *testing.T) { | 1587 | func TestMouseCollapsedStackBarExpandsAndFocuses(t *testing.T) { | ||
| 1588 | p1 := portsmocks.NewMockPTY(t) | 1588 | p1 := portsmocks.NewMockPTY(t) | ||
| 1589 | - | p1.EXPECT().Resize(domain.Size{Cols: 20, Rows: 4}).Return(nil).Maybe() | 1589 | + | p1.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 4}}).Return(nil).Maybe() |
| 1590 | p2 := portsmocks.NewMockPTY(t) | 1590 | p2 := portsmocks.NewMockPTY(t) | ||
| 1591 | - | p2.EXPECT().Resize(domain.Size{Cols: 20, Rows: 4}).Return(nil).Maybe() | 1591 | + | p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 4}}).Return(nil).Maybe() |
| 1592 | d, sess, ac, _ := newManualSessionWithPTYs(t, p1) | 1592 | d, sess, ac, _ := newManualSessionWithPTYs(t, p1) | ||
| 1593 | d.procComm = nil | 1593 | d.procComm = nil | ||
| 1594 | tb := testAttachmentTab(sess) | 1594 | tb := testAttachmentTab(sess) | ||
M internal/usecase/daemon/pane.go +2 -1
| @@ -110,7 +110,7 @@ type pane struct { | |||
| 110 | 110 | id layout.PaneID | |
| 111 | 111 | stableID string | |
| 112 | 112 | pty ports.PTY | |
| 113 | - | mu sync.Mutex // guards screen, history, syncGen, rect, resizeApplying, resizeRetry, resizePending, PTY side effects, title, and owner publication | |
| 113 | + | mu sync.Mutex // guards screen, history, syncGen, rect, geometry, resize state, PTY side effects, title, and owner publication | |
| 114 | 114 | owner atomic.Pointer[paneOwner] | |
| 115 | 115 | // ownerGeneration is advanced only while mu is held. Readers consume the | |
| 116 | 116 | // generation through the immutable owner pointer above. | |
| @@ -120,6 +120,7 @@ type pane struct { | |||
| 120 | 120 | history *vt.History | |
| 121 | 121 | syncGen uint64 | |
| 122 | 122 | rect domain.Rect | |
| 123 | + | geometry domain.Geometry | |
| 123 | 124 | // resizeApplying gates VT parsing across PTY.Resize. The reader continues | |
| 124 | 125 | // draining into resizePending so output is replayed against the target (or | |
| 125 | 126 | // retained old) screen only after apply resolves. | |
| @@ -110,7 +110,7 @@ type pane struct { | |||||
| 110 | id layout.PaneID | 110 | id layout.PaneID | ||
| 111 | stableID string | 111 | stableID string | ||
| 112 | pty ports.PTY | 112 | pty ports.PTY | ||
| 113 | - | mu sync.Mutex // guards screen, history, syncGen, rect, resizeApplying, resizeRetry, resizePending, PTY side effects, title, and owner publication | 113 | + | mu sync.Mutex // guards screen, history, syncGen, rect, geometry, resize state, PTY side effects, title, and owner publication |
| 114 | owner atomic.Pointer[paneOwner] | 114 | owner atomic.Pointer[paneOwner] | ||
| 115 | // ownerGeneration is advanced only while mu is held. Readers consume the | 115 | // ownerGeneration is advanced only while mu is held. Readers consume the | ||
| 116 | // generation through the immutable owner pointer above. | 116 | // generation through the immutable owner pointer above. | ||
| @@ -120,6 +120,7 @@ type pane struct { | |||||
| 120 | history *vt.History | 120 | history *vt.History | ||
| 121 | syncGen uint64 | 121 | syncGen uint64 | ||
| 122 | rect domain.Rect | 122 | rect domain.Rect | ||
| 123 | + | geometry domain.Geometry | |||
| 123 | // resizeApplying gates VT parsing across PTY.Resize. The reader continues | 124 | // resizeApplying gates VT parsing across PTY.Resize. The reader continues | ||
| 124 | // draining into resizePending so output is replayed against the target (or | 125 | // draining into resizePending so output is replayed against the target (or | ||
| 125 | // retained old) screen only after apply resolves. | 126 | // retained old) screen only after apply resolves. | ||
M internal/usecase/daemon/pane_actions.go +4 -2
| @@ -81,6 +81,7 @@ func (d *Daemon) spawnPaneOpAt( | |||
| 81 | 81 | tabStableID := tb.stableID | |
| 82 | 82 | generation := tb.layoutGeneration | |
| 83 | 83 | tb.mu.Unlock() | |
| 84 | + | initialGeometry := sess.paneGeometry(rectSize(newRect)) | |
| 84 | 85 | ||
| 85 | 86 | paneStableID, err := newStableID("p") | |
| 86 | 87 | if err != nil { | |
| @@ -88,7 +89,7 @@ func (d *Daemon) spawnPaneOpAt( | |||
| 88 | 89 | } | |
| 89 | 90 | launch := d.shellLaunch(env) | |
| 90 | 91 | lifetime := d.newPaneProcessLifetime(tb.ctx) | |
| 91 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, rectSize(newRect)) | |
| 92 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, initialGeometry) | |
| 92 | 93 | if err != nil { | |
| 93 | 94 | lifetime.abort() | |
| 94 | 95 | if pty != nil { | |
| @@ -97,8 +98,9 @@ func (d *Daemon) spawnPaneOpAt( | |||
| 97 | 98 | d.log.Warn("pty spawn failed", "err", err, "session", name, "pane", newID, "kind", "pane") | |
| 98 | 99 | return paneFocusChange{}, domain.UserErr(domain.NoticePaneSpawn, "couldn't open pane: shell failed to start", err) | |
| 99 | 100 | } | |
| 100 | - | ||
| 101 | 101 | p := newPaneWithStableIDAndTitle(newID, paneStableID, pty, rectSize(newRect), launch.title) | |
| 102 | + | p.geometry = initialGeometry | |
| 103 | + | setScreenGeometry(p.screen, initialGeometry) | |
| 102 | 104 | p.rect = newRect | |
| 103 | 105 | ||
| 104 | 106 | tb.mu.Lock() | |
| @@ -81,6 +81,7 @@ func (d *Daemon) spawnPaneOpAt( | |||||
| 81 | tabStableID := tb.stableID | 81 | tabStableID := tb.stableID | ||
| 82 | generation := tb.layoutGeneration | 82 | generation := tb.layoutGeneration | ||
| 83 | tb.mu.Unlock() | 83 | tb.mu.Unlock() | ||
| 84 | + | initialGeometry := sess.paneGeometry(rectSize(newRect)) | |||
| 84 | 85 | ||||
| 85 | paneStableID, err := newStableID("p") | 86 | paneStableID, err := newStableID("p") | ||
| 86 | if err != nil { | 87 | if err != nil { | ||
| @@ -88,7 +89,7 @@ func (d *Daemon) spawnPaneOpAt( | |||||
| 88 | } | 89 | } | ||
| 89 | launch := d.shellLaunch(env) | 90 | launch := d.shellLaunch(env) | ||
| 90 | lifetime := d.newPaneProcessLifetime(tb.ctx) | 91 | lifetime := d.newPaneProcessLifetime(tb.ctx) | ||
| 91 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, rectSize(newRect)) | 92 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, initialGeometry) |
| 92 | if err != nil { | 93 | if err != nil { | ||
| 93 | lifetime.abort() | 94 | lifetime.abort() | ||
| 94 | if pty != nil { | 95 | if pty != nil { | ||
| @@ -97,8 +98,9 @@ func (d *Daemon) spawnPaneOpAt( | |||||
| 97 | d.log.Warn("pty spawn failed", "err", err, "session", name, "pane", newID, "kind", "pane") | 98 | d.log.Warn("pty spawn failed", "err", err, "session", name, "pane", newID, "kind", "pane") | ||
| 98 | return paneFocusChange{}, domain.UserErr(domain.NoticePaneSpawn, "couldn't open pane: shell failed to start", err) | 99 | return paneFocusChange{}, domain.UserErr(domain.NoticePaneSpawn, "couldn't open pane: shell failed to start", err) | ||
| 99 | } | 100 | } | ||
| 100 | - | ||||
| 101 | p := newPaneWithStableIDAndTitle(newID, paneStableID, pty, rectSize(newRect), launch.title) | 101 | p := newPaneWithStableIDAndTitle(newID, paneStableID, pty, rectSize(newRect), launch.title) | ||
| 102 | + | p.geometry = initialGeometry | |||
| 103 | + | setScreenGeometry(p.screen, initialGeometry) | |||
| 102 | p.rect = newRect | 104 | p.rect = newRect | ||
| 103 | 105 | ||||
| 104 | tb.mu.Lock() | 106 | tb.mu.Lock() | ||
M internal/usecase/daemon/pane_actions_test.go +28 -28
| @@ -264,7 +264,7 @@ func TestLayoutApplicationDoesNotHoldTabLock(t *testing.T) { | |||
| 264 | 264 | tb.focusedPane().rect = domain.Rect{Width: 20, Height: 10} | |
| 265 | 265 | entered := make(chan struct{}) | |
| 266 | 266 | release := make(chan struct{}) | |
| 267 | - | pty.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Run(func(domain.Size) { | |
| 267 | + | pty.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Run(func(domain.Geometry) { | |
| 268 | 268 | close(entered) | |
| 269 | 269 | <-release | |
| 270 | 270 | }).Return(nil).Once() | |
| @@ -296,7 +296,7 @@ func TestLayoutApplicationRejectsStalePaneIdentity(t *testing.T) { | |||
| 296 | 296 | oldPane.screen.Resize(20, 10) | |
| 297 | 297 | entered := make(chan struct{}) | |
| 298 | 298 | release := make(chan struct{}) | |
| 299 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Run(func(domain.Size) { | |
| 299 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Run(func(domain.Geometry) { | |
| 300 | 300 | close(entered) | |
| 301 | 301 | <-release | |
| 302 | 302 | }).Return(nil).Once() | |
| @@ -310,7 +310,7 @@ func TestLayoutApplicationRejectsStalePaneIdentity(t *testing.T) { | |||
| 310 | 310 | ||
| 311 | 311 | newPTY := portsmocks.NewMockPTY(t) | |
| 312 | 312 | replacement := newPane("pane-1", newPTY, domain.Size{Cols: 20, Rows: 10}) | |
| 313 | - | newPTY.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Return(nil).Once() | |
| 313 | + | newPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Return(nil).Once() | |
| 314 | 314 | tb.mu.Lock() | |
| 315 | 315 | tb.panes["pane-1"] = replacement | |
| 316 | 316 | tb.bumpLayoutGenerationLocked() | |
| @@ -470,9 +470,9 @@ func TestSplitPaneCreatesFocusedShellInRequestedPosition(t *testing.T) { | |||
| 470 | 470 | t.Run(tt.name, func(t *testing.T) { | |
| 471 | 471 | d, sess, oldPTY, factory := newSplitTestDaemon(t, tt.size) | |
| 472 | 472 | newPTY := portsmocks.NewMockPTY(t) | |
| 473 | - | oldPTY.EXPECT().Resize(rectSize(tt.wantOld)).Return(nil).Once() | |
| 473 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: rectSize(tt.wantOld)}).Return(nil).Once() | |
| 474 | 474 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | |
| 475 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", rectSize(tt.wantNew)).Return(newPTY, nil).Once() | |
| 475 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: rectSize(tt.wantNew)}).Return(newPTY, nil).Once() | |
| 476 | 476 | ||
| 477 | 477 | require.NoError(t, d.splitPane(sess, nil, tt.dir)) | |
| 478 | 478 | ||
| @@ -497,9 +497,9 @@ func TestSplitPaneActionFocusesNewPaneForInitiatingAttachment(t *testing.T) { | |||
| 497 | 497 | require.True(t, sess.registerAttachment(ac)) | |
| 498 | 498 | require.True(t, sess.registerAttachment(other)) | |
| 499 | 499 | newPTY := portsmocks.NewMockPTY(t) | |
| 500 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | |
| 500 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Once() | |
| 501 | 501 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | |
| 502 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once() | |
| 502 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(newPTY, nil).Once() | |
| 503 | 503 | ||
| 504 | 504 | target := resolveDaemonActionTargetForAttachment(sess, ac) | |
| 505 | 505 | require.NoError(t, (daemonActions{d: d}).Run(daemonActionRequest{ | |
| @@ -535,9 +535,9 @@ func TestSplitPaneRightFromStackSplitsWholeStack(t *testing.T) { | |||
| 535 | 535 | tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Stack, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}, Expanded: "pane-2"}, Focus: "pane-2"} | |
| 536 | 536 | tb.nextPaneID = 3 | |
| 537 | 537 | newPTY := portsmocks.NewMockPTY(t) | |
| 538 | - | stackPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 3}).Return(nil).Once() | |
| 538 | + | stackPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 3}}).Return(nil).Once() | |
| 539 | 539 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | |
| 540 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 4}).Return(newPTY, nil).Once() | |
| 540 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 4}}).Return(newPTY, nil).Once() | |
| 541 | 541 | ||
| 542 | 542 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | |
| 543 | 543 | ||
| @@ -554,7 +554,7 @@ func TestSplitPaneRightFromStackSplitsWholeStack(t *testing.T) { | |||
| 554 | 554 | func TestSplitPaneOpenErrorRollsBackTreeAndPaneMap(t *testing.T) { | |
| 555 | 555 | d, sess, _, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10}) | |
| 556 | 556 | cause := errors.New("open failed") | |
| 557 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(nil, cause).Once() | |
| 557 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil, cause).Once() | |
| 558 | 558 | ||
| 559 | 559 | tb := testAttachmentTab(sess) | |
| 560 | 560 | tb.mu.Lock() | |
| @@ -588,8 +588,8 @@ func TestApplyLayoutResizesPTYsAndScreens(t *testing.T) { | |||
| 588 | 588 | newPane := newPane("pane-2", newPTY, domain.Size{Cols: 41, Rows: 10}) | |
| 589 | 589 | tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"} | |
| 590 | 590 | tb.panes["pane-2"] = newPane | |
| 591 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | |
| 592 | - | newPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | |
| 591 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Once() | |
| 592 | + | newPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Once() | |
| 593 | 593 | ||
| 594 | 594 | d.applyTabLayout(sess, tb) | |
| 595 | 595 | ||
| @@ -606,8 +606,8 @@ func TestResizeReflowsAllPanes(t *testing.T) { | |||
| 606 | 606 | tb.panes["pane-2"] = newPane("pane-2", newPTY, domain.Size{Cols: 20, Rows: 10}) | |
| 607 | 607 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | |
| 608 | 608 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | |
| 609 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 30, Rows: 18}).Return(nil).Once() | |
| 610 | - | newPTY.EXPECT().Resize(domain.Size{Cols: 29, Rows: 18}).Return(nil).Once() | |
| 609 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 30, Rows: 18}}).Return(nil).Once() | |
| 610 | + | newPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 29, Rows: 18}}).Return(nil).Once() | |
| 611 | 611 | ||
| 612 | 612 | d.resize(sess, nil, domain.Size{Cols: 60, Rows: 20}) | |
| 613 | 613 | ||
| @@ -689,7 +689,7 @@ func TestClosePaneRepaintFanoutRespectsAttachmentScope(t *testing.T) { | |||
| 689 | 689 | tb.panes["pane-2"] = newPane("pane-2", closingPTY, domain.Size{Cols: 20, Rows: 10}) | |
| 690 | 690 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | |
| 691 | 691 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | |
| 692 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Return(nil).Once() | |
| 692 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Return(nil).Once() | |
| 693 | 693 | closingPTY.EXPECT().Close().Return(nil).Once() | |
| 694 | 694 | ||
| 695 | 695 | clients := make([]*attachedClient, 2) | |
| @@ -761,7 +761,7 @@ func TestClosePaneActionDoesNotReenterDispatchBoundary(t *testing.T) { | |||
| 761 | 761 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | |
| 762 | 762 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | |
| 763 | 763 | d.sessions[sess.id] = sess | |
| 764 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Return(nil).Once() | |
| 764 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Return(nil).Once() | |
| 765 | 765 | closingPTY.EXPECT().Close().Return(nil).Once() | |
| 766 | 766 | ||
| 767 | 767 | done := make(chan error, 1) | |
| @@ -789,7 +789,7 @@ func TestCloseFocusedPaneRemovesPaneReflowsAndIsIdempotent(t *testing.T) { | |||
| 789 | 789 | tb.panes["pane-2"] = newPane("pane-2", closingPTY, domain.Size{Cols: 20, Rows: 10}) | |
| 790 | 790 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | |
| 791 | 791 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | |
| 792 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Return(nil).Once() | |
| 792 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Return(nil).Once() | |
| 793 | 793 | closingPTY.EXPECT().Close().Return(nil).Once() | |
| 794 | 794 | ||
| 795 | 795 | require.NoError(t, d.closeFocusedPane(sess, nil)) | |
| @@ -809,7 +809,7 @@ func TestReapPaneSharesClosePathAndIsIdempotent(t *testing.T) { | |||
| 809 | 809 | tb.panes["pane-2"] = reaped | |
| 810 | 810 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | |
| 811 | 811 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | |
| 812 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Return(nil).Once() | |
| 812 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Return(nil).Once() | |
| 813 | 813 | reapedPTY.EXPECT().Close().Return(nil).Once() | |
| 814 | 814 | ||
| 815 | 815 | d.reapPane(sess, tb, reaped) | |
| @@ -850,7 +850,7 @@ func TestStackPaneCreatesStackAndToggleRestoresSplit(t *testing.T) { | |||
| 850 | 850 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 20, Rows: 5}) | |
| 851 | 851 | newPTY := portsmocks.NewMockPTY(t) | |
| 852 | 852 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | |
| 853 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 4}).Return(newPTY, nil).Once() | |
| 853 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 4}}).Return(newPTY, nil).Once() | |
| 854 | 854 | ||
| 855 | 855 | require.NoError(t, d.stackPane(sess, nil)) | |
| 856 | 856 | tb := testAttachmentTab(sess) | |
| @@ -859,8 +859,8 @@ func TestStackPaneCreatesStackAndToggleRestoresSplit(t *testing.T) { | |||
| 859 | 859 | require.Equal(t, layout.PaneID("pane-2"), tb.tree.Root.Expanded) | |
| 860 | 860 | require.Len(t, tb.panes, 2) | |
| 861 | 861 | ||
| 862 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 2}).Return(nil).Once() | |
| 863 | - | newPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 2}).Return(nil).Once() | |
| 862 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 2}}).Return(nil).Once() | |
| 863 | + | newPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 2}}).Return(nil).Once() | |
| 864 | 864 | require.NoError(t, d.toggleStack(sess, nil)) | |
| 865 | 865 | require.Equal(t, layout.Split, tb.tree.Root.Kind) | |
| 866 | 866 | require.Equal(t, layout.Vertical, tb.tree.Root.Dir) | |
| @@ -870,9 +870,9 @@ func TestStackFocusWalkExpandsAndOverflowRefuses(t *testing.T) { | |||
| 870 | 870 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 20, Rows: 3}) | |
| 871 | 871 | newPTY := portsmocks.NewMockPTY(t) | |
| 872 | 872 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | |
| 873 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 2}).Return(newPTY, nil).Once() | |
| 873 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 2}}).Return(newPTY, nil).Once() | |
| 874 | 874 | require.NoError(t, d.stackPane(sess, nil)) | |
| 875 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 2}).Return(nil).Once() | |
| 875 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 2}}).Return(nil).Once() | |
| 876 | 876 | ||
| 877 | 877 | require.NoError(t, d.focusDir(sess, nil, layout.Up, nil)) | |
| 878 | 878 | ||
| @@ -916,9 +916,9 @@ func TestCloseOriginalPaneLeavesSurvivorFunctional(t *testing.T) { | |||
| 916 | 916 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10}) | |
| 917 | 917 | newPTY, releaseNew := newBlockingPTYWithWrites(t, writes) | |
| 918 | 918 | defer releaseNew() | |
| 919 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | |
| 919 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Once() | |
| 920 | 920 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | |
| 921 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once() | |
| 921 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(newPTY, nil).Once() | |
| 922 | 922 | ||
| 923 | 923 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | |
| 924 | 924 | tb := testAttachmentTab(sess) | |
| @@ -973,12 +973,12 @@ func TestSplitPaneUsesExactAuthoritativeEnvironment(t *testing.T) { | |||
| 973 | 973 | d.shellOverride = false | |
| 974 | 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"} | |
| 975 | 975 | newPTY := portsmocks.NewMockPTY(t) | |
| 976 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | |
| 976 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Once() | |
| 977 | 977 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | |
| 978 | 978 | var command string | |
| 979 | 979 | var gotEnv []string | |
| 980 | - | factory.EXPECT().Open(mock.Anything, mock.Anything, []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).RunAndReturn( | |
| 981 | - | func(_ context.Context, gotCommand string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 980 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).RunAndReturn( | |
| 981 | + | func(_ context.Context, gotCommand string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 982 | 982 | command = gotCommand | |
| 983 | 983 | gotEnv = append([]string(nil), env...) | |
| 984 | 984 | return newPTY, nil | |
| @@ -264,7 +264,7 @@ func TestLayoutApplicationDoesNotHoldTabLock(t *testing.T) { | |||||
| 264 | tb.focusedPane().rect = domain.Rect{Width: 20, Height: 10} | 264 | tb.focusedPane().rect = domain.Rect{Width: 20, Height: 10} | ||
| 265 | entered := make(chan struct{}) | 265 | entered := make(chan struct{}) | ||
| 266 | release := make(chan struct{}) | 266 | release := make(chan struct{}) | ||
| 267 | - | pty.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Run(func(domain.Size) { | 267 | + | pty.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Run(func(domain.Geometry) { |
| 268 | close(entered) | 268 | close(entered) | ||
| 269 | <-release | 269 | <-release | ||
| 270 | }).Return(nil).Once() | 270 | }).Return(nil).Once() | ||
| @@ -296,7 +296,7 @@ func TestLayoutApplicationRejectsStalePaneIdentity(t *testing.T) { | |||||
| 296 | oldPane.screen.Resize(20, 10) | 296 | oldPane.screen.Resize(20, 10) | ||
| 297 | entered := make(chan struct{}) | 297 | entered := make(chan struct{}) | ||
| 298 | release := make(chan struct{}) | 298 | release := make(chan struct{}) | ||
| 299 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Run(func(domain.Size) { | 299 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Run(func(domain.Geometry) { |
| 300 | close(entered) | 300 | close(entered) | ||
| 301 | <-release | 301 | <-release | ||
| 302 | }).Return(nil).Once() | 302 | }).Return(nil).Once() | ||
| @@ -310,7 +310,7 @@ func TestLayoutApplicationRejectsStalePaneIdentity(t *testing.T) { | |||||
| 310 | 310 | ||||
| 311 | newPTY := portsmocks.NewMockPTY(t) | 311 | newPTY := portsmocks.NewMockPTY(t) | ||
| 312 | replacement := newPane("pane-1", newPTY, domain.Size{Cols: 20, Rows: 10}) | 312 | replacement := newPane("pane-1", newPTY, domain.Size{Cols: 20, Rows: 10}) | ||
| 313 | - | newPTY.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Return(nil).Once() | 313 | + | newPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Return(nil).Once() |
| 314 | tb.mu.Lock() | 314 | tb.mu.Lock() | ||
| 315 | tb.panes["pane-1"] = replacement | 315 | tb.panes["pane-1"] = replacement | ||
| 316 | tb.bumpLayoutGenerationLocked() | 316 | tb.bumpLayoutGenerationLocked() | ||
| @@ -470,9 +470,9 @@ func TestSplitPaneCreatesFocusedShellInRequestedPosition(t *testing.T) { | |||||
| 470 | t.Run(tt.name, func(t *testing.T) { | 470 | t.Run(tt.name, func(t *testing.T) { | ||
| 471 | d, sess, oldPTY, factory := newSplitTestDaemon(t, tt.size) | 471 | d, sess, oldPTY, factory := newSplitTestDaemon(t, tt.size) | ||
| 472 | newPTY := portsmocks.NewMockPTY(t) | 472 | newPTY := portsmocks.NewMockPTY(t) | ||
| 473 | - | oldPTY.EXPECT().Resize(rectSize(tt.wantOld)).Return(nil).Once() | 473 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: rectSize(tt.wantOld)}).Return(nil).Once() |
| 474 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | 474 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | ||
| 475 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", rectSize(tt.wantNew)).Return(newPTY, nil).Once() | 475 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: rectSize(tt.wantNew)}).Return(newPTY, nil).Once() |
| 476 | 476 | ||||
| 477 | require.NoError(t, d.splitPane(sess, nil, tt.dir)) | 477 | require.NoError(t, d.splitPane(sess, nil, tt.dir)) | ||
| 478 | 478 | ||||
| @@ -497,9 +497,9 @@ func TestSplitPaneActionFocusesNewPaneForInitiatingAttachment(t *testing.T) { | |||||
| 497 | require.True(t, sess.registerAttachment(ac)) | 497 | require.True(t, sess.registerAttachment(ac)) | ||
| 498 | require.True(t, sess.registerAttachment(other)) | 498 | require.True(t, sess.registerAttachment(other)) | ||
| 499 | newPTY := portsmocks.NewMockPTY(t) | 499 | newPTY := portsmocks.NewMockPTY(t) | ||
| 500 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | 500 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Once() |
| 501 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | 501 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | ||
| 502 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once() | 502 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(newPTY, nil).Once() |
| 503 | 503 | ||||
| 504 | target := resolveDaemonActionTargetForAttachment(sess, ac) | 504 | target := resolveDaemonActionTargetForAttachment(sess, ac) | ||
| 505 | require.NoError(t, (daemonActions{d: d}).Run(daemonActionRequest{ | 505 | require.NoError(t, (daemonActions{d: d}).Run(daemonActionRequest{ | ||
| @@ -535,9 +535,9 @@ func TestSplitPaneRightFromStackSplitsWholeStack(t *testing.T) { | |||||
| 535 | tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Stack, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}, Expanded: "pane-2"}, Focus: "pane-2"} | 535 | tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Stack, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}, Expanded: "pane-2"}, Focus: "pane-2"} | ||
| 536 | tb.nextPaneID = 3 | 536 | tb.nextPaneID = 3 | ||
| 537 | newPTY := portsmocks.NewMockPTY(t) | 537 | newPTY := portsmocks.NewMockPTY(t) | ||
| 538 | - | stackPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 3}).Return(nil).Once() | 538 | + | stackPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 3}}).Return(nil).Once() |
| 539 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | 539 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | ||
| 540 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 4}).Return(newPTY, nil).Once() | 540 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 4}}).Return(newPTY, nil).Once() |
| 541 | 541 | ||||
| 542 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | 542 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | ||
| 543 | 543 | ||||
| @@ -554,7 +554,7 @@ func TestSplitPaneRightFromStackSplitsWholeStack(t *testing.T) { | |||||
| 554 | func TestSplitPaneOpenErrorRollsBackTreeAndPaneMap(t *testing.T) { | 554 | func TestSplitPaneOpenErrorRollsBackTreeAndPaneMap(t *testing.T) { | ||
| 555 | d, sess, _, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10}) | 555 | d, sess, _, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10}) | ||
| 556 | cause := errors.New("open failed") | 556 | cause := errors.New("open failed") | ||
| 557 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(nil, cause).Once() | 557 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil, cause).Once() |
| 558 | 558 | ||||
| 559 | tb := testAttachmentTab(sess) | 559 | tb := testAttachmentTab(sess) | ||
| 560 | tb.mu.Lock() | 560 | tb.mu.Lock() | ||
| @@ -588,8 +588,8 @@ func TestApplyLayoutResizesPTYsAndScreens(t *testing.T) { | |||||
| 588 | newPane := newPane("pane-2", newPTY, domain.Size{Cols: 41, Rows: 10}) | 588 | newPane := newPane("pane-2", newPTY, domain.Size{Cols: 41, Rows: 10}) | ||
| 589 | tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"} | 589 | tb.tree = &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("pane-1"), layout.NewLeaf("pane-2")}}, Focus: "pane-2"} | ||
| 590 | tb.panes["pane-2"] = newPane | 590 | tb.panes["pane-2"] = newPane | ||
| 591 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | 591 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Once() |
| 592 | - | newPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | 592 | + | newPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Once() |
| 593 | 593 | ||||
| 594 | d.applyTabLayout(sess, tb) | 594 | d.applyTabLayout(sess, tb) | ||
| 595 | 595 | ||||
| @@ -606,8 +606,8 @@ func TestResizeReflowsAllPanes(t *testing.T) { | |||||
| 606 | tb.panes["pane-2"] = newPane("pane-2", newPTY, domain.Size{Cols: 20, Rows: 10}) | 606 | tb.panes["pane-2"] = newPane("pane-2", newPTY, domain.Size{Cols: 20, Rows: 10}) | ||
| 607 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | 607 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | ||
| 608 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | 608 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | ||
| 609 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 30, Rows: 18}).Return(nil).Once() | 609 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 30, Rows: 18}}).Return(nil).Once() |
| 610 | - | newPTY.EXPECT().Resize(domain.Size{Cols: 29, Rows: 18}).Return(nil).Once() | 610 | + | newPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 29, Rows: 18}}).Return(nil).Once() |
| 611 | 611 | ||||
| 612 | d.resize(sess, nil, domain.Size{Cols: 60, Rows: 20}) | 612 | d.resize(sess, nil, domain.Size{Cols: 60, Rows: 20}) | ||
| 613 | 613 | ||||
| @@ -689,7 +689,7 @@ func TestClosePaneRepaintFanoutRespectsAttachmentScope(t *testing.T) { | |||||
| 689 | tb.panes["pane-2"] = newPane("pane-2", closingPTY, domain.Size{Cols: 20, Rows: 10}) | 689 | tb.panes["pane-2"] = newPane("pane-2", closingPTY, domain.Size{Cols: 20, Rows: 10}) | ||
| 690 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | 690 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | ||
| 691 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | 691 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | ||
| 692 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Return(nil).Once() | 692 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Return(nil).Once() |
| 693 | closingPTY.EXPECT().Close().Return(nil).Once() | 693 | closingPTY.EXPECT().Close().Return(nil).Once() | ||
| 694 | 694 | ||||
| 695 | clients := make([]*attachedClient, 2) | 695 | clients := make([]*attachedClient, 2) | ||
| @@ -761,7 +761,7 @@ func TestClosePaneActionDoesNotReenterDispatchBoundary(t *testing.T) { | |||||
| 761 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | 761 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | ||
| 762 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | 762 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | ||
| 763 | d.sessions[sess.id] = sess | 763 | d.sessions[sess.id] = sess | ||
| 764 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Return(nil).Once() | 764 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Return(nil).Once() |
| 765 | closingPTY.EXPECT().Close().Return(nil).Once() | 765 | closingPTY.EXPECT().Close().Return(nil).Once() | ||
| 766 | 766 | ||||
| 767 | done := make(chan error, 1) | 767 | done := make(chan error, 1) | ||
| @@ -789,7 +789,7 @@ func TestCloseFocusedPaneRemovesPaneReflowsAndIsIdempotent(t *testing.T) { | |||||
| 789 | tb.panes["pane-2"] = newPane("pane-2", closingPTY, domain.Size{Cols: 20, Rows: 10}) | 789 | tb.panes["pane-2"] = newPane("pane-2", closingPTY, domain.Size{Cols: 20, Rows: 10}) | ||
| 790 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | 790 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | ||
| 791 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | 791 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | ||
| 792 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Return(nil).Once() | 792 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Return(nil).Once() |
| 793 | closingPTY.EXPECT().Close().Return(nil).Once() | 793 | closingPTY.EXPECT().Close().Return(nil).Once() | ||
| 794 | 794 | ||||
| 795 | require.NoError(t, d.closeFocusedPane(sess, nil)) | 795 | require.NoError(t, d.closeFocusedPane(sess, nil)) | ||
| @@ -809,7 +809,7 @@ func TestReapPaneSharesClosePathAndIsIdempotent(t *testing.T) { | |||||
| 809 | tb.panes["pane-2"] = reaped | 809 | tb.panes["pane-2"] = reaped | ||
| 810 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | 810 | tb.panes["pane-1"].rect = domain.Rect{Width: 20, Height: 10} | ||
| 811 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | 811 | tb.panes["pane-2"].rect = domain.Rect{X: 21, Width: 20, Height: 10} | ||
| 812 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 41, Rows: 10}).Return(nil).Once() | 812 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 41, Rows: 10}}).Return(nil).Once() |
| 813 | reapedPTY.EXPECT().Close().Return(nil).Once() | 813 | reapedPTY.EXPECT().Close().Return(nil).Once() | ||
| 814 | 814 | ||||
| 815 | d.reapPane(sess, tb, reaped) | 815 | d.reapPane(sess, tb, reaped) | ||
| @@ -850,7 +850,7 @@ func TestStackPaneCreatesStackAndToggleRestoresSplit(t *testing.T) { | |||||
| 850 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 20, Rows: 5}) | 850 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 20, Rows: 5}) | ||
| 851 | newPTY := portsmocks.NewMockPTY(t) | 851 | newPTY := portsmocks.NewMockPTY(t) | ||
| 852 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | 852 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | ||
| 853 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 4}).Return(newPTY, nil).Once() | 853 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 4}}).Return(newPTY, nil).Once() |
| 854 | 854 | ||||
| 855 | require.NoError(t, d.stackPane(sess, nil)) | 855 | require.NoError(t, d.stackPane(sess, nil)) | ||
| 856 | tb := testAttachmentTab(sess) | 856 | tb := testAttachmentTab(sess) | ||
| @@ -859,8 +859,8 @@ func TestStackPaneCreatesStackAndToggleRestoresSplit(t *testing.T) { | |||||
| 859 | require.Equal(t, layout.PaneID("pane-2"), tb.tree.Root.Expanded) | 859 | require.Equal(t, layout.PaneID("pane-2"), tb.tree.Root.Expanded) | ||
| 860 | require.Len(t, tb.panes, 2) | 860 | require.Len(t, tb.panes, 2) | ||
| 861 | 861 | ||||
| 862 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 2}).Return(nil).Once() | 862 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 2}}).Return(nil).Once() |
| 863 | - | newPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 2}).Return(nil).Once() | 863 | + | newPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 2}}).Return(nil).Once() |
| 864 | require.NoError(t, d.toggleStack(sess, nil)) | 864 | require.NoError(t, d.toggleStack(sess, nil)) | ||
| 865 | require.Equal(t, layout.Split, tb.tree.Root.Kind) | 865 | require.Equal(t, layout.Split, tb.tree.Root.Kind) | ||
| 866 | require.Equal(t, layout.Vertical, tb.tree.Root.Dir) | 866 | require.Equal(t, layout.Vertical, tb.tree.Root.Dir) | ||
| @@ -870,9 +870,9 @@ func TestStackFocusWalkExpandsAndOverflowRefuses(t *testing.T) { | |||||
| 870 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 20, Rows: 3}) | 870 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 20, Rows: 3}) | ||
| 871 | newPTY := portsmocks.NewMockPTY(t) | 871 | newPTY := portsmocks.NewMockPTY(t) | ||
| 872 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | 872 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | ||
| 873 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 2}).Return(newPTY, nil).Once() | 873 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 2}}).Return(newPTY, nil).Once() |
| 874 | require.NoError(t, d.stackPane(sess, nil)) | 874 | require.NoError(t, d.stackPane(sess, nil)) | ||
| 875 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 2}).Return(nil).Once() | 875 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 2}}).Return(nil).Once() |
| 876 | 876 | ||||
| 877 | require.NoError(t, d.focusDir(sess, nil, layout.Up, nil)) | 877 | require.NoError(t, d.focusDir(sess, nil, layout.Up, nil)) | ||
| 878 | 878 | ||||
| @@ -916,9 +916,9 @@ func TestCloseOriginalPaneLeavesSurvivorFunctional(t *testing.T) { | |||||
| 916 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10}) | 916 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10}) | ||
| 917 | newPTY, releaseNew := newBlockingPTYWithWrites(t, writes) | 917 | newPTY, releaseNew := newBlockingPTYWithWrites(t, writes) | ||
| 918 | defer releaseNew() | 918 | defer releaseNew() | ||
| 919 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | 919 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Once() |
| 920 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | 920 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | ||
| 921 | - | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once() | 921 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(newPTY, nil).Once() |
| 922 | 922 | ||||
| 923 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | 923 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | ||
| 924 | tb := testAttachmentTab(sess) | 924 | tb := testAttachmentTab(sess) | ||
| @@ -973,12 +973,12 @@ func TestSplitPaneUsesExactAuthoritativeEnvironment(t *testing.T) { | |||||
| 973 | d.shellOverride = false | 973 | d.shellOverride = false | ||
| 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"} | 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"} | ||
| 975 | newPTY := portsmocks.NewMockPTY(t) | 975 | newPTY := portsmocks.NewMockPTY(t) | ||
| 976 | - | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | 976 | + | oldPTY.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Once() |
| 977 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | 977 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | ||
| 978 | var command string | 978 | var command string | ||
| 979 | var gotEnv []string | 979 | var gotEnv []string | ||
| 980 | - | factory.EXPECT().Open(mock.Anything, mock.Anything, []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).RunAndReturn( | 980 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, []string(nil), mock.Anything, "/work", domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).RunAndReturn( |
| 981 | - | func(_ context.Context, gotCommand string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 981 | + | func(_ context.Context, gotCommand string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 982 | command = gotCommand | 982 | command = gotCommand | ||
| 983 | gotEnv = append([]string(nil), env...) | 983 | gotEnv = append([]string(nil), env...) | ||
| 984 | return newPTY, nil | 984 | return newPTY, nil | ||
M internal/usecase/daemon/pane_lifetime_test.go +5 -5
| @@ -36,9 +36,9 @@ func (p *lifetimePTY) Read([]byte) (int, error) { | |||
| 36 | 36 | } | |
| 37 | 37 | } | |
| 38 | 38 | ||
| 39 | - | func (*lifetimePTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 40 | - | func (*lifetimePTY) Resize(domain.Size) error { return nil } | |
| 41 | - | func (*lifetimePTY) Pid() int { return 1 } | |
| 39 | + | func (*lifetimePTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 40 | + | func (*lifetimePTY) Resize(domain.Geometry) error { return nil } | |
| 41 | + | func (*lifetimePTY) Pid() int { return 1 } | |
| 42 | 42 | func (*lifetimePTY) ForegroundPgid() (int, error) { | |
| 43 | 43 | return 1, nil | |
| 44 | 44 | } | |
| @@ -54,7 +54,7 @@ type lifetimePTYFactory struct { | |||
| 54 | 54 | contexts []context.Context | |
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | - | func (f *lifetimePTYFactory) Open(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 57 | + | func (f *lifetimePTYFactory) Open(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 58 | 58 | p := newLifetimePTY() | |
| 59 | 59 | p.ctx = ctx | |
| 60 | 60 | f.mu.Lock() | |
| @@ -199,7 +199,7 @@ type paneErrorFactory struct { | |||
| 199 | 199 | onOpen func(context.Context) | |
| 200 | 200 | } | |
| 201 | 201 | ||
| 202 | - | func (f *paneErrorFactory) Open(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 202 | + | func (f *paneErrorFactory) Open(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 203 | 203 | if f.onOpen != nil { | |
| 204 | 204 | f.onOpen(ctx) | |
| 205 | 205 | } | |
| @@ -36,9 +36,9 @@ func (p *lifetimePTY) Read([]byte) (int, error) { | |||||
| 36 | } | 36 | } | ||
| 37 | } | 37 | } | ||
| 38 | 38 | ||||
| 39 | - | func (*lifetimePTY) Write(b []byte) (int, error) { return len(b), nil } | 39 | + | func (*lifetimePTY) Write(b []byte) (int, error) { return len(b), nil } |
| 40 | - | func (*lifetimePTY) Resize(domain.Size) error { return nil } | 40 | + | func (*lifetimePTY) Resize(domain.Geometry) error { return nil } |
| 41 | - | func (*lifetimePTY) Pid() int { return 1 } | 41 | + | func (*lifetimePTY) Pid() int { return 1 } |
| 42 | func (*lifetimePTY) ForegroundPgid() (int, error) { | 42 | func (*lifetimePTY) ForegroundPgid() (int, error) { | ||
| 43 | return 1, nil | 43 | return 1, nil | ||
| 44 | } | 44 | } | ||
| @@ -54,7 +54,7 @@ type lifetimePTYFactory struct { | |||||
| 54 | contexts []context.Context | 54 | contexts []context.Context | ||
| 55 | } | 55 | } | ||
| 56 | 56 | ||||
| 57 | - | func (f *lifetimePTYFactory) Open(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | 57 | + | func (f *lifetimePTYFactory) Open(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 58 | p := newLifetimePTY() | 58 | p := newLifetimePTY() | ||
| 59 | p.ctx = ctx | 59 | p.ctx = ctx | ||
| 60 | f.mu.Lock() | 60 | f.mu.Lock() | ||
| @@ -199,7 +199,7 @@ type paneErrorFactory struct { | |||||
| 199 | onOpen func(context.Context) | 199 | onOpen func(context.Context) | ||
| 200 | } | 200 | } | ||
| 201 | 201 | ||||
| 202 | - | func (f *paneErrorFactory) Open(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | 202 | + | func (f *paneErrorFactory) Open(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 203 | if f.onOpen != nil { | 203 | if f.onOpen != nil { | ||
| 204 | f.onOpen(ctx) | 204 | f.onOpen(ctx) | ||
| 205 | } | 205 | } | ||
M internal/usecase/daemon/pane_owner_reader_test.go +4 -4
| @@ -43,10 +43,10 @@ func (p *ownerRoutingPTY) Read(buf []byte) (int, error) { | |||
| 43 | 43 | return copy(buf, step.data), step.err | |
| 44 | 44 | } | |
| 45 | 45 | ||
| 46 | - | func (*ownerRoutingPTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 47 | - | func (p *ownerRoutingPTY) Close() error { p.closes.Add(1); return nil } | |
| 48 | - | func (*ownerRoutingPTY) Resize(domain.Size) error { return nil } | |
| 49 | - | func (*ownerRoutingPTY) Pid() int { return 0 } | |
| 46 | + | func (*ownerRoutingPTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 47 | + | func (p *ownerRoutingPTY) Close() error { p.closes.Add(1); return nil } | |
| 48 | + | func (*ownerRoutingPTY) Resize(domain.Geometry) error { return nil } | |
| 49 | + | func (*ownerRoutingPTY) Pid() int { return 0 } | |
| 50 | 50 | func (*ownerRoutingPTY) ForegroundPgid() (int, error) { | |
| 51 | 51 | return 0, nil | |
| 52 | 52 | } | |
| @@ -43,10 +43,10 @@ func (p *ownerRoutingPTY) Read(buf []byte) (int, error) { | |||||
| 43 | return copy(buf, step.data), step.err | 43 | return copy(buf, step.data), step.err | ||
| 44 | } | 44 | } | ||
| 45 | 45 | ||||
| 46 | - | func (*ownerRoutingPTY) Write(b []byte) (int, error) { return len(b), nil } | 46 | + | func (*ownerRoutingPTY) Write(b []byte) (int, error) { return len(b), nil } |
| 47 | - | func (p *ownerRoutingPTY) Close() error { p.closes.Add(1); return nil } | 47 | + | func (p *ownerRoutingPTY) Close() error { p.closes.Add(1); return nil } |
| 48 | - | func (*ownerRoutingPTY) Resize(domain.Size) error { return nil } | 48 | + | func (*ownerRoutingPTY) Resize(domain.Geometry) error { return nil } |
| 49 | - | func (*ownerRoutingPTY) Pid() int { return 0 } | 49 | + | func (*ownerRoutingPTY) Pid() int { return 0 } |
| 50 | func (*ownerRoutingPTY) ForegroundPgid() (int, error) { | 50 | func (*ownerRoutingPTY) ForegroundPgid() (int, error) { | ||
| 51 | return 0, nil | 51 | return 0, nil | ||
| 52 | } | 52 | } | ||
M internal/usecase/daemon/pane_rearrange_test.go +2 -2
| @@ -462,9 +462,9 @@ type paneRearrangePTY struct { | |||
| 462 | 462 | ||
| 463 | 463 | func (*paneRearrangePTY) Read([]byte) (int, error) { return 0, io.EOF } | |
| 464 | 464 | func (*paneRearrangePTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 465 | - | func (p *paneRearrangePTY) Resize(size domain.Size) error { | |
| 465 | + | func (p *paneRearrangePTY) Resize(geometry domain.Geometry) error { | |
| 466 | 466 | p.mu.Lock() | |
| 467 | - | p.resizes = append(p.resizes, size) | |
| 467 | + | p.resizes = append(p.resizes, geometry.Size) | |
| 468 | 468 | hook := p.onResize | |
| 469 | 469 | p.mu.Unlock() | |
| 470 | 470 | if hook != nil { | |
| @@ -462,9 +462,9 @@ type paneRearrangePTY struct { | |||||
| 462 | 462 | ||||
| 463 | func (*paneRearrangePTY) Read([]byte) (int, error) { return 0, io.EOF } | 463 | func (*paneRearrangePTY) Read([]byte) (int, error) { return 0, io.EOF } | ||
| 464 | func (*paneRearrangePTY) Write(b []byte) (int, error) { return len(b), nil } | 464 | func (*paneRearrangePTY) Write(b []byte) (int, error) { return len(b), nil } | ||
| 465 | - | func (p *paneRearrangePTY) Resize(size domain.Size) error { | 465 | + | func (p *paneRearrangePTY) Resize(geometry domain.Geometry) error { |
| 466 | p.mu.Lock() | 466 | p.mu.Lock() | ||
| 467 | - | p.resizes = append(p.resizes, size) | 467 | + | p.resizes = append(p.resizes, geometry.Size) |
| 468 | hook := p.onResize | 468 | hook := p.onResize | ||
| 469 | p.mu.Unlock() | 469 | p.mu.Unlock() | ||
| 470 | if hook != nil { | 470 | if hook != nil { | ||
M internal/usecase/daemon/parked_route.go +1 -1
| @@ -470,7 +470,7 @@ func (d *Daemon) transitionParkedRouteTarget(token attachmentConnectionToken, ta | |||
| 470 | 470 | env := copyEnvironment(d.baseEnv) | |
| 471 | 471 | cwd := d.dirOrHome(current.cwd) | |
| 472 | 472 | var createErr error | |
| 473 | - | created, createErr = d.resumeRemoteInactiveSessionLocked(target, cwd, token.ac.sizeSnapshot(), env, current) | |
| 473 | + | created, createErr = d.resumeRemoteInactiveSessionLocked(target, cwd, token.ac.geometrySnapshot(), env, current) | |
| 474 | 474 | return created, createErr | |
| 475 | 475 | }, | |
| 476 | 476 | }) | |
| @@ -470,7 +470,7 @@ func (d *Daemon) transitionParkedRouteTarget(token attachmentConnectionToken, ta | |||||
| 470 | env := copyEnvironment(d.baseEnv) | 470 | env := copyEnvironment(d.baseEnv) | ||
| 471 | cwd := d.dirOrHome(current.cwd) | 471 | cwd := d.dirOrHome(current.cwd) | ||
| 472 | var createErr error | 472 | var createErr error | ||
| 473 | - | created, createErr = d.resumeRemoteInactiveSessionLocked(target, cwd, token.ac.sizeSnapshot(), env, current) | 473 | + | created, createErr = d.resumeRemoteInactiveSessionLocked(target, cwd, token.ac.geometrySnapshot(), env, current) |
| 474 | return created, createErr | 474 | return created, createErr | ||
| 475 | }, | 475 | }, | ||
| 476 | }) | 476 | }) | ||
M internal/usecase/daemon/performance_benchmark_test.go +2 -2
| @@ -956,10 +956,10 @@ type scriptedPerformancePTY struct { | |||
| 956 | 956 | fails uint64 | |
| 957 | 957 | } | |
| 958 | 958 | ||
| 959 | - | func (p *scriptedPerformancePTY) Resize(size domain.Size) error { | |
| 959 | + | func (p *scriptedPerformancePTY) Resize(geometry domain.Geometry) error { | |
| 960 | 960 | p.mu.Lock() | |
| 961 | 961 | defer p.mu.Unlock() | |
| 962 | - | p.sizes = append(p.sizes, size) | |
| 962 | + | p.sizes = append(p.sizes, geometry.Size) | |
| 963 | 963 | index := len(p.sizes) - 1 | |
| 964 | 964 | if index < len(p.errs) && p.errs[index] != nil { | |
| 965 | 965 | p.fails++ | |
| @@ -956,10 +956,10 @@ type scriptedPerformancePTY struct { | |||||
| 956 | fails uint64 | 956 | fails uint64 | ||
| 957 | } | 957 | } | ||
| 958 | 958 | ||||
| 959 | - | func (p *scriptedPerformancePTY) Resize(size domain.Size) error { | 959 | + | func (p *scriptedPerformancePTY) Resize(geometry domain.Geometry) error { |
| 960 | p.mu.Lock() | 960 | p.mu.Lock() | ||
| 961 | defer p.mu.Unlock() | 961 | defer p.mu.Unlock() | ||
| 962 | - | p.sizes = append(p.sizes, size) | 962 | + | p.sizes = append(p.sizes, geometry.Size) |
| 963 | index := len(p.sizes) - 1 | 963 | index := len(p.sizes) - 1 | ||
| 964 | if index < len(p.errs) && p.errs[index] != nil { | 964 | if index < len(p.errs) && p.errs[index] != nil { | ||
| 965 | p.fails++ | 965 | p.fails++ | ||
M internal/usecase/daemon/picker.go +2 -2
| @@ -1256,7 +1256,7 @@ func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, | |||
| 1256 | 1256 | from.mu.Lock() | |
| 1257 | 1257 | cwd, env := d.dirOrHome(current.cwd), copyEnvironment(from.env) | |
| 1258 | 1258 | from.mu.Unlock() | |
| 1259 | - | created, createErr := d.resumeInactiveSessionLocked(target.Name, cwd, ac.sizeSnapshot(), env, current, current.tabNames) | |
| 1259 | + | created, createErr := d.resumeInactiveSessionLocked(target.Name, cwd, ac.geometrySnapshot(), env, current, current.tabNames) | |
| 1260 | 1260 | targetSess = created | |
| 1261 | 1261 | return created, createErr | |
| 1262 | 1262 | }, | |
| @@ -1287,7 +1287,7 @@ func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, | |||
| 1287 | 1287 | return nil, attachmentTransitionResult{}, false, errAttachmentTransition | |
| 1288 | 1288 | } | |
| 1289 | 1289 | cwd := d.dirOrHome(current.cwd) | |
| 1290 | - | targetSess, err := d.resumeInactiveSessionLocked(target.Name, cwd, ac.sizeSnapshot(), env, current, current.tabNames) | |
| 1290 | + | targetSess, err := d.resumeInactiveSessionLocked(target.Name, cwd, ac.geometrySnapshot(), env, current, current.tabNames) | |
| 1291 | 1291 | if err != nil { | |
| 1292 | 1292 | d.log.Warn("resuming stopped session failed", "err", err, "session", target.Name) | |
| 1293 | 1293 | return nil, attachmentTransitionResult{}, false, err | |
| @@ -1256,7 +1256,7 @@ func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, | |||||
| 1256 | from.mu.Lock() | 1256 | from.mu.Lock() | ||
| 1257 | cwd, env := d.dirOrHome(current.cwd), copyEnvironment(from.env) | 1257 | cwd, env := d.dirOrHome(current.cwd), copyEnvironment(from.env) | ||
| 1258 | from.mu.Unlock() | 1258 | from.mu.Unlock() | ||
| 1259 | - | created, createErr := d.resumeInactiveSessionLocked(target.Name, cwd, ac.sizeSnapshot(), env, current, current.tabNames) | 1259 | + | created, createErr := d.resumeInactiveSessionLocked(target.Name, cwd, ac.geometrySnapshot(), env, current, current.tabNames) |
| 1260 | targetSess = created | 1260 | targetSess = created | ||
| 1261 | return created, createErr | 1261 | return created, createErr | ||
| 1262 | }, | 1262 | }, | ||
| @@ -1287,7 +1287,7 @@ func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, | |||||
| 1287 | return nil, attachmentTransitionResult{}, false, errAttachmentTransition | 1287 | return nil, attachmentTransitionResult{}, false, errAttachmentTransition | ||
| 1288 | } | 1288 | } | ||
| 1289 | cwd := d.dirOrHome(current.cwd) | 1289 | cwd := d.dirOrHome(current.cwd) | ||
| 1290 | - | targetSess, err := d.resumeInactiveSessionLocked(target.Name, cwd, ac.sizeSnapshot(), env, current, current.tabNames) | 1290 | + | targetSess, err := d.resumeInactiveSessionLocked(target.Name, cwd, ac.geometrySnapshot(), env, current, current.tabNames) |
| 1291 | if err != nil { | 1291 | if err != nil { | ||
| 1292 | d.log.Warn("resuming stopped session failed", "err", err, "session", target.Name) | 1292 | d.log.Warn("resuming stopped session failed", "err", err, "session", target.Name) | ||
| 1293 | return nil, attachmentTransitionResult{}, false, err | 1293 | return nil, attachmentTransitionResult{}, false, err | ||
M internal/usecase/daemon/picker_test.go +4 -4
| @@ -1392,8 +1392,8 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 1392 | 1392 | var opens [][]string | |
| 1393 | 1393 | f := portsmocks.NewMockPTYFactory(t) | |
| 1394 | 1394 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 1395 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1396 | - | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1395 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).RunAndReturn( | |
| 1396 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 1397 | 1397 | opens = append(opens, append([]string(nil), env...)) | |
| 1398 | 1398 | if len(opens) == 1 { | |
| 1399 | 1399 | return p1, nil | |
| @@ -1402,8 +1402,8 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 1402 | 1402 | }, | |
| 1403 | 1403 | ).Twice() | |
| 1404 | 1404 | floating := newQuietPTY() | |
| 1405 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 1406 | - | return got != normalSize && got.Valid() | |
| 1405 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { | |
| 1406 | + | return got.Size != normalSize && got.Size.Valid() | |
| 1407 | 1407 | })).Return(floating, nil).Once() | |
| 1408 | 1408 | d := newTestDaemon(t, f, stubClock{}) | |
| 1409 | 1409 | d.inactive["old"] = inactiveSession{name: "old", cwd: t.TempDir(), createdAt: 1, state: ports.SessionDown} | |
| @@ -1392,8 +1392,8 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 1392 | var opens [][]string | 1392 | var opens [][]string | ||
| 1393 | f := portsmocks.NewMockPTYFactory(t) | 1393 | f := portsmocks.NewMockPTYFactory(t) | ||
| 1394 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | 1394 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | ||
| 1395 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | 1395 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).RunAndReturn( |
| 1396 | - | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 1396 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 1397 | opens = append(opens, append([]string(nil), env...)) | 1397 | opens = append(opens, append([]string(nil), env...)) | ||
| 1398 | if len(opens) == 1 { | 1398 | if len(opens) == 1 { | ||
| 1399 | return p1, nil | 1399 | return p1, nil | ||
| @@ -1402,8 +1402,8 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 1402 | }, | 1402 | }, | ||
| 1403 | ).Twice() | 1403 | ).Twice() | ||
| 1404 | floating := newQuietPTY() | 1404 | floating := newQuietPTY() | ||
| 1405 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | 1405 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { |
| 1406 | - | return got != normalSize && got.Valid() | 1406 | + | return got.Size != normalSize && got.Size.Valid() |
| 1407 | })).Return(floating, nil).Once() | 1407 | })).Return(floating, nil).Once() | ||
| 1408 | d := newTestDaemon(t, f, stubClock{}) | 1408 | d := newTestDaemon(t, f, stubClock{}) | ||
| 1409 | d.inactive["old"] = inactiveSession{name: "old", cwd: t.TempDir(), createdAt: 1, state: ports.SessionDown} | 1409 | d.inactive["old"] = inactiveSession{name: "old", cwd: t.TempDir(), createdAt: 1, state: ports.SessionDown} | ||
M internal/usecase/daemon/prompt_test.go +1 -1
| @@ -13,7 +13,7 @@ import ( | |||
| 13 | 13 | ||
| 14 | 14 | type failingPTYFactory struct{ err error } | |
| 15 | 15 | ||
| 16 | - | func (f failingPTYFactory) Open(context.Context, string, []string, []string, string, domain.Size) (ports.PTY, error) { | |
| 16 | + | func (f failingPTYFactory) Open(context.Context, string, []string, []string, string, domain.Geometry) (ports.PTY, error) { | |
| 17 | 17 | return nil, f.err | |
| 18 | 18 | } | |
| 19 | 19 | ||
| @@ -13,7 +13,7 @@ import ( | |||||
| 13 | 13 | ||||
| 14 | type failingPTYFactory struct{ err error } | 14 | type failingPTYFactory struct{ err error } | ||
| 15 | 15 | ||||
| 16 | - | func (f failingPTYFactory) Open(context.Context, string, []string, []string, string, domain.Size) (ports.PTY, error) { | 16 | + | func (f failingPTYFactory) Open(context.Context, string, []string, []string, string, domain.Geometry) (ports.PTY, error) { |
| 17 | return nil, f.err | 17 | return nil, f.err | ||
| 18 | } | 18 | } | ||
| 19 | 19 | ||||
M internal/usecase/daemon/remote_attach.go +3 -3
| @@ -79,7 +79,7 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello | |||
| 79 | 79 | // trusted for this branch. | |
| 80 | 80 | env := copyEnvironment(d.baseEnv) | |
| 81 | 81 | cwd := d.dirOrHome(inactive.cwd) | |
| 82 | - | sess, err := d.resumeRemoteInactiveSessionLocked(target, cwd, h.Size, env, inactive) | |
| 82 | + | sess, err := d.resumeRemoteInactiveSessionLocked(target, cwd, h.Geometry(), env, inactive) | |
| 83 | 83 | if err != nil { | |
| 84 | 84 | d.mu.Unlock() | |
| 85 | 85 | if errors.Is(err, errAttachmentTransition) || errors.Is(err, errSessionNameInUse) { | |
| @@ -91,7 +91,7 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello | |||
| 91 | 91 | return sess, ac, err | |
| 92 | 92 | } | |
| 93 | 93 | ||
| 94 | - | func (d *Daemon) resumeRemoteInactiveSessionLocked(target domain.RemoteSessionTarget, cwd string, size domain.Size, env []string, expected inactiveSession) (*session, error) { | |
| 94 | + | func (d *Daemon) resumeRemoteInactiveSessionLocked(target domain.RemoteSessionTarget, cwd string, geometry domain.Geometry, env []string, expected inactiveSession) (*session, error) { | |
| 95 | 95 | validate := func(current inactiveSession, _ domain.CatalogueRecord, authoritativeExists bool) bool { | |
| 96 | 96 | if !target.Stopped || d.persistEnabled && !authoritativeExists { | |
| 97 | 97 | return false | |
| @@ -99,7 +99,7 @@ func (d *Daemon) resumeRemoteInactiveSessionLocked(target domain.RemoteSessionTa | |||
| 99 | 99 | _, ok := target.ResolveTab(stoppedTabMetadata(current)) | |
| 100 | 100 | return ok | |
| 101 | 101 | } | |
| 102 | - | return d.createSessionLockedWithModeAndInactiveFence(target.SessionName, false, cwd, size, env, &expected, validate, expected.tabNames) | |
| 102 | + | return d.createSessionLockedWithModeAndInactiveFence(target.SessionName, false, cwd, geometry, env, &expected, validate, expected.tabNames) | |
| 103 | 103 | } | |
| 104 | 104 | ||
| 105 | 105 | func (d *Daemon) sendNavigationActionForAttachment(token attachmentConnectionToken, action ports.NavigationAction) error { | |
| @@ -79,7 +79,7 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello | |||||
| 79 | // trusted for this branch. | 79 | // trusted for this branch. | ||
| 80 | env := copyEnvironment(d.baseEnv) | 80 | env := copyEnvironment(d.baseEnv) | ||
| 81 | cwd := d.dirOrHome(inactive.cwd) | 81 | cwd := d.dirOrHome(inactive.cwd) | ||
| 82 | - | sess, err := d.resumeRemoteInactiveSessionLocked(target, cwd, h.Size, env, inactive) | 82 | + | sess, err := d.resumeRemoteInactiveSessionLocked(target, cwd, h.Geometry(), env, inactive) |
| 83 | if err != nil { | 83 | if err != nil { | ||
| 84 | d.mu.Unlock() | 84 | d.mu.Unlock() | ||
| 85 | if errors.Is(err, errAttachmentTransition) || errors.Is(err, errSessionNameInUse) { | 85 | if errors.Is(err, errAttachmentTransition) || errors.Is(err, errSessionNameInUse) { | ||
| @@ -91,7 +91,7 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello | |||||
| 91 | return sess, ac, err | 91 | return sess, ac, err | ||
| 92 | } | 92 | } | ||
| 93 | 93 | ||||
| 94 | - | func (d *Daemon) resumeRemoteInactiveSessionLocked(target domain.RemoteSessionTarget, cwd string, size domain.Size, env []string, expected inactiveSession) (*session, error) { | 94 | + | func (d *Daemon) resumeRemoteInactiveSessionLocked(target domain.RemoteSessionTarget, cwd string, geometry domain.Geometry, env []string, expected inactiveSession) (*session, error) { |
| 95 | validate := func(current inactiveSession, _ domain.CatalogueRecord, authoritativeExists bool) bool { | 95 | validate := func(current inactiveSession, _ domain.CatalogueRecord, authoritativeExists bool) bool { | ||
| 96 | if !target.Stopped || d.persistEnabled && !authoritativeExists { | 96 | if !target.Stopped || d.persistEnabled && !authoritativeExists { | ||
| 97 | return false | 97 | return false | ||
| @@ -99,7 +99,7 @@ func (d *Daemon) resumeRemoteInactiveSessionLocked(target domain.RemoteSessionTa | |||||
| 99 | _, ok := target.ResolveTab(stoppedTabMetadata(current)) | 99 | _, ok := target.ResolveTab(stoppedTabMetadata(current)) | ||
| 100 | return ok | 100 | return ok | ||
| 101 | } | 101 | } | ||
| 102 | - | return d.createSessionLockedWithModeAndInactiveFence(target.SessionName, false, cwd, size, env, &expected, validate, expected.tabNames) | 102 | + | return d.createSessionLockedWithModeAndInactiveFence(target.SessionName, false, cwd, geometry, env, &expected, validate, expected.tabNames) |
| 103 | } | 103 | } | ||
| 104 | 104 | ||||
| 105 | func (d *Daemon) sendNavigationActionForAttachment(token attachmentConnectionToken, action ports.NavigationAction) error { | 105 | func (d *Daemon) sendNavigationActionForAttachment(token attachmentConnectionToken, action ports.NavigationAction) error { | ||
M internal/usecase/daemon/render_test.go +7 -7
| @@ -264,10 +264,10 @@ func (p *concurrentExitPTY) Read([]byte) (int, error) { | |||
| 264 | 264 | <-p.release | |
| 265 | 265 | return 0, io.EOF | |
| 266 | 266 | } | |
| 267 | - | func (*concurrentExitPTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 268 | - | func (*concurrentExitPTY) Close() error { return nil } | |
| 269 | - | func (*concurrentExitPTY) Resize(domain.Size) error { return nil } | |
| 270 | - | func (*concurrentExitPTY) Pid() int { return 0 } | |
| 267 | + | func (*concurrentExitPTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 268 | + | func (*concurrentExitPTY) Close() error { return nil } | |
| 269 | + | func (*concurrentExitPTY) Resize(domain.Geometry) error { return nil } | |
| 270 | + | func (*concurrentExitPTY) Pid() int { return 0 } | |
| 271 | 271 | func (*concurrentExitPTY) ForegroundPgid() (int, error) { | |
| 272 | 272 | return 0, nil | |
| 273 | 273 | } | |
| @@ -666,8 +666,8 @@ func TestPTYEOFFinalTabKillsSessionAndDetaches(t *testing.T) { | |||
| 666 | 666 | ||
| 667 | 667 | func TestResizePreservesLiveContentAndEvictsScrollback(t *testing.T) { | |
| 668 | 668 | p := portsmocks.NewMockPTY(t) | |
| 669 | - | p.EXPECT().Resize(domain.Size{Cols: 4, Rows: 2}).Return(nil).Once() | |
| 670 | - | p.EXPECT().Resize(domain.Size{Cols: 6, Rows: 4}).Return(nil).Once() | |
| 669 | + | p.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 4, Rows: 2}}).Return(nil).Once() | |
| 670 | + | p.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 6, Rows: 4}}).Return(nil).Once() | |
| 671 | 671 | ||
| 672 | 672 | win := newTab(p, domain.Size{Cols: 4, Rows: 4}) | |
| 673 | 673 | for y, text := range []string{"0000", "1111", "2222", "3333"} { | |
| @@ -922,7 +922,7 @@ func TestResizeOrdersPTYBeforeScreen(t *testing.T) { | |||
| 922 | 922 | p := portsmocks.NewMockPTY(t) | |
| 923 | 923 | var screenWidthAtResize int | |
| 924 | 924 | win := newTab(newScriptPTY(nil), domain.Size{Cols: 80, Rows: 24}) | |
| 925 | - | p.EXPECT().Resize(domain.Size{Cols: 100, Rows: 28}).RunAndReturn(func(sz domain.Size) error { | |
| 925 | + | p.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 100, Rows: 28}}).RunAndReturn(func(domain.Geometry) error { | |
| 926 | 926 | // The screen must not yet be resized when the PTY is: proves order. | |
| 927 | 927 | screenWidthAtResize = win.focusedPane().screen.Frame.Width | |
| 928 | 928 | return nil | |
| @@ -264,10 +264,10 @@ func (p *concurrentExitPTY) Read([]byte) (int, error) { | |||||
| 264 | <-p.release | 264 | <-p.release | ||
| 265 | return 0, io.EOF | 265 | return 0, io.EOF | ||
| 266 | } | 266 | } | ||
| 267 | - | func (*concurrentExitPTY) Write(b []byte) (int, error) { return len(b), nil } | 267 | + | func (*concurrentExitPTY) Write(b []byte) (int, error) { return len(b), nil } |
| 268 | - | func (*concurrentExitPTY) Close() error { return nil } | 268 | + | func (*concurrentExitPTY) Close() error { return nil } |
| 269 | - | func (*concurrentExitPTY) Resize(domain.Size) error { return nil } | 269 | + | func (*concurrentExitPTY) Resize(domain.Geometry) error { return nil } |
| 270 | - | func (*concurrentExitPTY) Pid() int { return 0 } | 270 | + | func (*concurrentExitPTY) Pid() int { return 0 } |
| 271 | func (*concurrentExitPTY) ForegroundPgid() (int, error) { | 271 | func (*concurrentExitPTY) ForegroundPgid() (int, error) { | ||
| 272 | return 0, nil | 272 | return 0, nil | ||
| 273 | } | 273 | } | ||
| @@ -666,8 +666,8 @@ func TestPTYEOFFinalTabKillsSessionAndDetaches(t *testing.T) { | |||||
| 666 | 666 | ||||
| 667 | func TestResizePreservesLiveContentAndEvictsScrollback(t *testing.T) { | 667 | func TestResizePreservesLiveContentAndEvictsScrollback(t *testing.T) { | ||
| 668 | p := portsmocks.NewMockPTY(t) | 668 | p := portsmocks.NewMockPTY(t) | ||
| 669 | - | p.EXPECT().Resize(domain.Size{Cols: 4, Rows: 2}).Return(nil).Once() | 669 | + | p.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 4, Rows: 2}}).Return(nil).Once() |
| 670 | - | p.EXPECT().Resize(domain.Size{Cols: 6, Rows: 4}).Return(nil).Once() | 670 | + | p.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 6, Rows: 4}}).Return(nil).Once() |
| 671 | 671 | ||||
| 672 | win := newTab(p, domain.Size{Cols: 4, Rows: 4}) | 672 | win := newTab(p, domain.Size{Cols: 4, Rows: 4}) | ||
| 673 | for y, text := range []string{"0000", "1111", "2222", "3333"} { | 673 | for y, text := range []string{"0000", "1111", "2222", "3333"} { | ||
| @@ -922,7 +922,7 @@ func TestResizeOrdersPTYBeforeScreen(t *testing.T) { | |||||
| 922 | p := portsmocks.NewMockPTY(t) | 922 | p := portsmocks.NewMockPTY(t) | ||
| 923 | var screenWidthAtResize int | 923 | var screenWidthAtResize int | ||
| 924 | win := newTab(newScriptPTY(nil), domain.Size{Cols: 80, Rows: 24}) | 924 | win := newTab(newScriptPTY(nil), domain.Size{Cols: 80, Rows: 24}) | ||
| 925 | - | p.EXPECT().Resize(domain.Size{Cols: 100, Rows: 28}).RunAndReturn(func(sz domain.Size) error { | 925 | + | p.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 100, Rows: 28}}).RunAndReturn(func(domain.Geometry) error { |
| 926 | // The screen must not yet be resized when the PTY is: proves order. | 926 | // The screen must not yet be resized when the PTY is: proves order. | ||
| 927 | screenWidthAtResize = win.focusedPane().screen.Frame.Width | 927 | screenWidthAtResize = win.focusedPane().screen.Frame.Width | ||
| 928 | return nil | 928 | return nil | ||
M internal/usecase/daemon/resize_mode_test.go +1 -1
| @@ -136,7 +136,7 @@ func (*resizeLockProbePTY) Write(b []byte) (int, error) { return len(b), nil } | |||
| 136 | 136 | func (*resizeLockProbePTY) Close() error { return nil } | |
| 137 | 137 | func (*resizeLockProbePTY) Pid() int { return 0 } | |
| 138 | 138 | func (*resizeLockProbePTY) ForegroundPgid() (int, error) { return 0, nil } | |
| 139 | - | func (p *resizeLockProbePTY) Resize(domain.Size) error { | |
| 139 | + | func (p *resizeLockProbePTY) Resize(domain.Geometry) error { | |
| 140 | 140 | p.once.Do(func() { | |
| 141 | 141 | func() { | |
| 142 | 142 | defer close(p.entered) | |
| @@ -136,7 +136,7 @@ func (*resizeLockProbePTY) Write(b []byte) (int, error) { return len(b), nil } | |||||
| 136 | func (*resizeLockProbePTY) Close() error { return nil } | 136 | func (*resizeLockProbePTY) Close() error { return nil } | ||
| 137 | func (*resizeLockProbePTY) Pid() int { return 0 } | 137 | func (*resizeLockProbePTY) Pid() int { return 0 } | ||
| 138 | func (*resizeLockProbePTY) ForegroundPgid() (int, error) { return 0, nil } | 138 | func (*resizeLockProbePTY) ForegroundPgid() (int, error) { return 0, nil } | ||
| 139 | - | func (p *resizeLockProbePTY) Resize(domain.Size) error { | 139 | + | func (p *resizeLockProbePTY) Resize(domain.Geometry) error { |
| 140 | p.once.Do(func() { | 140 | p.once.Do(func() { | ||
| 141 | func() { | 141 | func() { | ||
| 142 | defer close(p.entered) | 142 | defer close(p.entered) | ||
M internal/usecase/daemon/resume.go +11 -1
| @@ -597,6 +597,12 @@ func (d *Daemon) resumeParked(h ports.Hello, tr ports.Transport, sz domain.Size) | |||
| 597 | 597 | sess, resumed, ok, err := d.resumeParkedLocked(h, tr, sz) | |
| 598 | 598 | d.mu.Unlock() | |
| 599 | 599 | ac.sendMu.Unlock() | |
| 600 | + | if err == nil && ok && sess != nil && resumed != nil { | |
| 601 | + | // Resume publication normally schedules reconciliation asynchronously; | |
| 602 | + | // complete the claiming terminal's geometry before the resumed | |
| 603 | + | // attachment is allowed to produce its first frame. | |
| 604 | + | d.recalculateSessionGeometry(sess, resumed) | |
| 605 | + | } | |
| 600 | 606 | if err != nil && resumed != nil { | |
| 601 | 607 | d.abortResumeClaim(resumed) | |
| 602 | 608 | } | |
| @@ -640,7 +646,11 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain | |||
| 640 | 646 | ac.rebaseOutput() | |
| 641 | 647 | ac.output.setWindow(h.MaxOutputInFlight) | |
| 642 | 648 | ac.replaceTransport(tr) | |
| 643 | - | ac.setSize(sz) | |
| 649 | + | geometry := h.Geometry() | |
| 650 | + | if geometry.Size != sz { | |
| 651 | + | geometry = domain.Geometry{Size: sz} | |
| 652 | + | } | |
| 653 | + | ac.setGeometry(geometry) | |
| 644 | 654 | ac.resumeToken = d.nextResumeTokenLocked() | |
| 645 | 655 | ac.parked = false | |
| 646 | 656 | // The resumed session's snapshot is the sole source for future PTY children. | |
| @@ -597,6 +597,12 @@ func (d *Daemon) resumeParked(h ports.Hello, tr ports.Transport, sz domain.Size) | |||||
| 597 | sess, resumed, ok, err := d.resumeParkedLocked(h, tr, sz) | 597 | sess, resumed, ok, err := d.resumeParkedLocked(h, tr, sz) | ||
| 598 | d.mu.Unlock() | 598 | d.mu.Unlock() | ||
| 599 | ac.sendMu.Unlock() | 599 | ac.sendMu.Unlock() | ||
| 600 | + | if err == nil && ok && sess != nil && resumed != nil { | |||
| 601 | + | // Resume publication normally schedules reconciliation asynchronously; | |||
| 602 | + | // complete the claiming terminal's geometry before the resumed | |||
| 603 | + | // attachment is allowed to produce its first frame. | |||
| 604 | + | d.recalculateSessionGeometry(sess, resumed) | |||
| 605 | + | } | |||
| 600 | if err != nil && resumed != nil { | 606 | if err != nil && resumed != nil { | ||
| 601 | d.abortResumeClaim(resumed) | 607 | d.abortResumeClaim(resumed) | ||
| 602 | } | 608 | } | ||
| @@ -640,7 +646,11 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain | |||||
| 640 | ac.rebaseOutput() | 646 | ac.rebaseOutput() | ||
| 641 | ac.output.setWindow(h.MaxOutputInFlight) | 647 | ac.output.setWindow(h.MaxOutputInFlight) | ||
| 642 | ac.replaceTransport(tr) | 648 | ac.replaceTransport(tr) | ||
| 643 | - | ac.setSize(sz) | 649 | + | geometry := h.Geometry() |
| 650 | + | if geometry.Size != sz { | |||
| 651 | + | geometry = domain.Geometry{Size: sz} | |||
| 652 | + | } | |||
| 653 | + | ac.setGeometry(geometry) | |||
| 644 | ac.resumeToken = d.nextResumeTokenLocked() | 654 | ac.resumeToken = d.nextResumeTokenLocked() | ||
| 645 | ac.parked = false | 655 | ac.parked = false | ||
| 646 | // The resumed session's snapshot is the sole source for future PTY children. | 656 | // The resumed session's snapshot is the sole source for future PTY children. | ||
M internal/usecase/daemon/resume_test.go +2 -2
| @@ -1574,8 +1574,8 @@ func TestResumeParkedReplacesFuturePTYEnvironment(t *testing.T) { | |||
| 1574 | 1574 | var envs [][]string | |
| 1575 | 1575 | factory := portsmocks.NewMockPTYFactory(t) | |
| 1576 | 1576 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 1577 | - | func(_ context.Context, command string, _ []string, env []string, _ string, size domain.Size) (ports.PTY, error) { | |
| 1578 | - | if size != (domain.Size{Cols: 80, Rows: 22}) { | |
| 1577 | + | func(_ context.Context, command string, _ []string, env []string, _ string, geometry domain.Geometry) (ports.PTY, error) { | |
| 1578 | + | if geometry.Size != (domain.Size{Cols: 80, Rows: 22}) { | |
| 1579 | 1579 | return newQuietPTY(), nil | |
| 1580 | 1580 | } | |
| 1581 | 1581 | commands = append(commands, command) | |
| @@ -1574,8 +1574,8 @@ func TestResumeParkedReplacesFuturePTYEnvironment(t *testing.T) { | |||||
| 1574 | var envs [][]string | 1574 | var envs [][]string | ||
| 1575 | factory := portsmocks.NewMockPTYFactory(t) | 1575 | factory := portsmocks.NewMockPTYFactory(t) | ||
| 1576 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 1576 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | ||
| 1577 | - | func(_ context.Context, command string, _ []string, env []string, _ string, size domain.Size) (ports.PTY, error) { | 1577 | + | func(_ context.Context, command string, _ []string, env []string, _ string, geometry domain.Geometry) (ports.PTY, error) { |
| 1578 | - | if size != (domain.Size{Cols: 80, Rows: 22}) { | 1578 | + | if geometry.Size != (domain.Size{Cols: 80, Rows: 22}) { |
| 1579 | return newQuietPTY(), nil | 1579 | return newQuietPTY(), nil | ||
| 1580 | } | 1580 | } | ||
| 1581 | commands = append(commands, command) | 1581 | commands = append(commands, command) | ||
M internal/usecase/daemon/session.go +33 -15
| @@ -182,19 +182,19 @@ func (d *Daemon) touchMRU(entry *session) { | |||
| 182 | 182 | entry.mu.Unlock() | |
| 183 | 183 | } | |
| 184 | 184 | ||
| 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...) | |
| 185 | + | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, geometry domain.Geometry, env []string, restoredTabNames ...[]string) (*session, error) { | |
| 186 | + | return d.createSessionLockedWithMode(name, ephemeral, cwd, geometry, env, restoredTabNames...) | |
| 187 | 187 | } | |
| 188 | 188 | ||
| 189 | 189 | // resumeInactiveSessionLocked resumes exactly expectedInactive. It rechecks | |
| 190 | 190 | // lifecycle and resumability after catalogue I/O, which intentionally runs | |
| 191 | 191 | // without d.mu. | |
| 192 | - | func (d *Daemon) resumeInactiveSessionLocked(name string, cwd string, sz domain.Size, env []string, expectedInactive inactiveSession, restoredTabNames ...[]string) (*session, error) { | |
| 193 | - | return d.createSessionLockedWithModeAndInactiveFence(name, false, cwd, sz, env, &expectedInactive, nil, restoredTabNames...) | |
| 192 | + | func (d *Daemon) resumeInactiveSessionLocked(name string, cwd string, geometry domain.Geometry, env []string, expectedInactive inactiveSession, restoredTabNames ...[]string) (*session, error) { | |
| 193 | + | return d.createSessionLockedWithModeAndInactiveFence(name, false, cwd, geometry, env, &expectedInactive, nil, restoredTabNames...) | |
| 194 | 194 | } | |
| 195 | 195 | ||
| 196 | - | func (d *Daemon) createSessionLockedWithMode(name string, ephemeral bool, cwd string, sz domain.Size, env []string, restoredTabNames ...[]string) (*session, error) { | |
| 197 | - | return d.createSessionLockedWithModeAndInactiveFence(name, ephemeral, cwd, sz, env, nil, nil, restoredTabNames...) | |
| 196 | + | func (d *Daemon) createSessionLockedWithMode(name string, ephemeral bool, cwd string, geometry domain.Geometry, env []string, restoredTabNames ...[]string) (*session, error) { | |
| 197 | + | return d.createSessionLockedWithModeAndInactiveFence(name, ephemeral, cwd, geometry, env, nil, nil, restoredTabNames...) | |
| 198 | 198 | } | |
| 199 | 199 | ||
| 200 | 200 | type inactiveResumeValidator func(inactiveSession, domain.CatalogueRecord, bool) bool | |
| @@ -219,8 +219,10 @@ func inactiveResumeFenceAllows(current inactiveSession, exists bool, expected *i | |||
| 219 | 219 | return validate == nil || validate(current, authoritative, authoritativeExists) | |
| 220 | 220 | } | |
| 221 | 221 | ||
| 222 | - | func (d *Daemon) createSessionLockedWithModeAndInactiveFence(name string, ephemeral bool, cwd string, sz domain.Size, env []string, expectedInactive *inactiveSession, validateInactive inactiveResumeValidator, restoredTabNames ...[]string) (*session, error) { | |
| 222 | + | func (d *Daemon) createSessionLockedWithModeAndInactiveFence(name string, ephemeral bool, cwd string, geometry domain.Geometry, env []string, expectedInactive *inactiveSession, validateInactive inactiveResumeValidator, restoredTabNames ...[]string) (*session, error) { | |
| 223 | 223 | env = copyEnvironment(env) | |
| 224 | + | geometry = geometry.NormalizePixels() | |
| 225 | + | sz := geometry.Size | |
| 224 | 226 | if _, reserved := d.creating[name]; reserved { | |
| 225 | 227 | return nil, errSessionNameInUse | |
| 226 | 228 | } | |
| @@ -285,6 +287,7 @@ func (d *Daemon) createSessionLockedWithModeAndInactiveFence(name string, epheme | |||
| 285 | 287 | return nil, domain.UserErr(domain.NoticeSessionSpawn, "couldn't create session", errors.New("daemon: PTY factory is unavailable")) | |
| 286 | 288 | } | |
| 287 | 289 | tbSize := contentSize(sz) | |
| 290 | + | initialPaneGeometry := scalePaneGeometry(geometry, tbSize) | |
| 288 | 291 | var names []string | |
| 289 | 292 | var tabRecords []domain.CatalogueTabRecord | |
| 290 | 293 | if resuming && len(stopped.tabRecords) != 0 { | |
| @@ -308,7 +311,7 @@ func (d *Daemon) createSessionLockedWithModeAndInactiveFence(name string, epheme | |||
| 308 | 311 | } | |
| 309 | 312 | launch := d.shellLaunch(env) | |
| 310 | 313 | lifetime := d.newPaneProcessLifetime(d.serveCtx) | |
| 311 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, tbSize) | |
| 314 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, initialPaneGeometry) | |
| 312 | 315 | if err != nil { | |
| 313 | 316 | lifetime.abort() | |
| 314 | 317 | if pty != nil { | |
| @@ -318,7 +321,12 @@ func (d *Daemon) createSessionLockedWithModeAndInactiveFence(name string, epheme | |||
| 318 | 321 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "session") | |
| 319 | 322 | return nil, domain.UserErr(domain.NoticeSessionSpawn, "couldn't create session: shell failed to start", err) | |
| 320 | 323 | } | |
| 324 | + | appliedPaneGeometry := initialPaneGeometry | |
| 321 | 325 | tb := newTabWithStableIDAndTitle(tabStableID, paneStableID, pty, tbSize, launch.title) | |
| 326 | + | if pane := tb.focusedPane(); pane != nil { | |
| 327 | + | pane.geometry = appliedPaneGeometry | |
| 328 | + | setScreenGeometry(pane.screen, appliedPaneGeometry) | |
| 329 | + | } | |
| 322 | 330 | if !lifetime.publish(tb.focusedPane()) { | |
| 323 | 331 | lifetime.abort() | |
| 324 | 332 | _ = pty.Close() | |
| @@ -453,7 +461,7 @@ func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name | |||
| 453 | 461 | if err := domain.ValidateSessionName(name); err != nil { | |
| 454 | 462 | return err | |
| 455 | 463 | } | |
| 456 | - | sz := ac.sizeSnapshot() | |
| 464 | + | geometry := ac.geometrySnapshot() | |
| 457 | 465 | d.mu.Lock() | |
| 458 | 466 | if d.closing { | |
| 459 | 467 | d.mu.Unlock() | |
| @@ -473,7 +481,7 @@ func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name | |||
| 473 | 481 | return errors.New("client detached") | |
| 474 | 482 | } | |
| 475 | 483 | ||
| 476 | - | newSess, err := d.createSessionLockedWithMode(name, false, cwd, sz, env) | |
| 484 | + | newSess, err := d.createSessionLockedWithMode(name, false, cwd, geometry, env) | |
| 477 | 485 | d.mu.Unlock() | |
| 478 | 486 | if err != nil { | |
| 479 | 487 | return err | |
| @@ -537,7 +545,7 @@ func (d *Daemon) createSessionAndSwitchForAttachment(token attachmentConnectionT | |||
| 537 | 545 | cwd, env := source.cwd, copyEnvironment(source.env) | |
| 538 | 546 | source.mu.Unlock() | |
| 539 | 547 | var createErr error | |
| 540 | - | created, createErr = d.createSessionLockedWithMode(name, false, cwd, token.ac.sizeSnapshot(), env) | |
| 548 | + | created, createErr = d.createSessionLockedWithMode(name, false, cwd, token.ac.geometrySnapshot(), env) | |
| 541 | 549 | return created, createErr | |
| 542 | 550 | }, | |
| 543 | 551 | }) | |
| @@ -556,7 +564,7 @@ func (d *Daemon) createSessionAndSwitchForAttachment(token attachmentConnectionT | |||
| 556 | 564 | } | |
| 557 | 565 | ||
| 558 | 566 | func (d *Daemon) createEphemeralSessionAndSwitch(from *session, ac *attachedClient) error { | |
| 559 | - | sz := ac.sizeSnapshot() | |
| 567 | + | geometry := ac.geometrySnapshot() | |
| 560 | 568 | d.mu.Lock() | |
| 561 | 569 | if d.closing { | |
| 562 | 570 | d.mu.Unlock() | |
| @@ -571,7 +579,7 @@ func (d *Daemon) createEphemeralSessionAndSwitch(from *session, ac *attachedClie | |||
| 571 | 579 | return errors.New("client detached") | |
| 572 | 580 | } | |
| 573 | 581 | name := d.allocEphemeralNameLocked() | |
| 574 | - | newSess, err := d.createSessionLockedWithMode(name, true, cwd, sz, env) | |
| 582 | + | newSess, err := d.createSessionLockedWithMode(name, true, cwd, geometry, env) | |
| 575 | 583 | d.mu.Unlock() | |
| 576 | 584 | if err != nil { | |
| 577 | 585 | return err | |
| @@ -613,7 +621,7 @@ func (d *Daemon) createEphemeralSessionAndSwitchForAttachment(token attachmentCo | |||
| 613 | 621 | source.mu.Unlock() | |
| 614 | 622 | name := d.allocEphemeralNameLocked() | |
| 615 | 623 | var createErr error | |
| 616 | - | created, createErr = d.createSessionLockedWithMode(name, true, cwd, token.ac.sizeSnapshot(), env) | |
| 624 | + | created, createErr = d.createSessionLockedWithMode(name, true, cwd, token.ac.geometrySnapshot(), env) | |
| 617 | 625 | return created, createErr | |
| 618 | 626 | }, | |
| 619 | 627 | }) | |
| @@ -642,13 +650,19 @@ func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, _ dom | |||
| 642 | 650 | attachments := sess.snapshotAttachmentsLocked() | |
| 643 | 651 | sess.mu.Unlock() | |
| 644 | 652 | tbSize := contentSize(sess.fullViewportSize()) | |
| 653 | + | claimGeometry := domain.Geometry{Size: tbSize} | |
| 654 | + | if source, ok := sess.geometrySourceSnapshot(); ok { | |
| 655 | + | claimGeometry = scalePaneGeometry(source.geometry, tbSize) | |
| 656 | + | } else if ac != nil { | |
| 657 | + | claimGeometry = scalePaneGeometry(ac.geometrySnapshot(), tbSize) | |
| 658 | + | } | |
| 645 | 659 | tabStableID, paneStableID, err := d.newTabPaneStableIDs() | |
| 646 | 660 | if err != nil { | |
| 647 | 661 | return err | |
| 648 | 662 | } | |
| 649 | 663 | launch := d.shellLaunch(env) | |
| 650 | 664 | lifetime := d.newPaneProcessLifetime(sess.ctx) | |
| 651 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, tbSize) | |
| 665 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, claimGeometry) | |
| 652 | 666 | if err != nil { | |
| 653 | 667 | lifetime.abort() | |
| 654 | 668 | if pty != nil { | |
| @@ -658,6 +672,10 @@ func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, _ dom | |||
| 658 | 672 | return domain.UserErr(domain.NoticeTabSpawn, "couldn't open tab: shell failed to start", err) | |
| 659 | 673 | } | |
| 660 | 674 | tb := newTabWithStableIDAndTitle(tabStableID, paneStableID, pty, tbSize, launch.title) | |
| 675 | + | if pane := tb.focusedPane(); pane != nil { | |
| 676 | + | pane.geometry = claimGeometry | |
| 677 | + | setScreenGeometry(pane.screen, claimGeometry) | |
| 678 | + | } | |
| 661 | 679 | themeClient := ac | |
| 662 | 680 | if themeClient == nil && len(attachments) != 0 { | |
| 663 | 681 | themeClient = attachments[0] | |
| @@ -182,19 +182,19 @@ func (d *Daemon) touchMRU(entry *session) { | |||||
| 182 | entry.mu.Unlock() | 182 | entry.mu.Unlock() | ||
| 183 | } | 183 | } | ||
| 184 | 184 | ||||
| 185 | - | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz domain.Size, env []string, restoredTabNames ...[]string) (*session, error) { | 185 | + | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, geometry domain.Geometry, env []string, restoredTabNames ...[]string) (*session, error) { |
| 186 | - | return d.createSessionLockedWithMode(name, ephemeral, cwd, sz, env, restoredTabNames...) | 186 | + | return d.createSessionLockedWithMode(name, ephemeral, cwd, geometry, env, restoredTabNames...) |
| 187 | } | 187 | } | ||
| 188 | 188 | ||||
| 189 | // resumeInactiveSessionLocked resumes exactly expectedInactive. It rechecks | 189 | // resumeInactiveSessionLocked resumes exactly expectedInactive. It rechecks | ||
| 190 | // lifecycle and resumability after catalogue I/O, which intentionally runs | 190 | // lifecycle and resumability after catalogue I/O, which intentionally runs | ||
| 191 | // without d.mu. | 191 | // without d.mu. | ||
| 192 | - | func (d *Daemon) resumeInactiveSessionLocked(name string, cwd string, sz domain.Size, env []string, expectedInactive inactiveSession, restoredTabNames ...[]string) (*session, error) { | 192 | + | func (d *Daemon) resumeInactiveSessionLocked(name string, cwd string, geometry domain.Geometry, env []string, expectedInactive inactiveSession, restoredTabNames ...[]string) (*session, error) { |
| 193 | - | return d.createSessionLockedWithModeAndInactiveFence(name, false, cwd, sz, env, &expectedInactive, nil, restoredTabNames...) | 193 | + | return d.createSessionLockedWithModeAndInactiveFence(name, false, cwd, geometry, env, &expectedInactive, nil, restoredTabNames...) |
| 194 | } | 194 | } | ||
| 195 | 195 | ||||
| 196 | - | func (d *Daemon) createSessionLockedWithMode(name string, ephemeral bool, cwd string, sz domain.Size, env []string, restoredTabNames ...[]string) (*session, error) { | 196 | + | func (d *Daemon) createSessionLockedWithMode(name string, ephemeral bool, cwd string, geometry domain.Geometry, env []string, restoredTabNames ...[]string) (*session, error) { |
| 197 | - | return d.createSessionLockedWithModeAndInactiveFence(name, ephemeral, cwd, sz, env, nil, nil, restoredTabNames...) | 197 | + | return d.createSessionLockedWithModeAndInactiveFence(name, ephemeral, cwd, geometry, env, nil, nil, restoredTabNames...) |
| 198 | } | 198 | } | ||
| 199 | 199 | ||||
| 200 | type inactiveResumeValidator func(inactiveSession, domain.CatalogueRecord, bool) bool | 200 | type inactiveResumeValidator func(inactiveSession, domain.CatalogueRecord, bool) bool | ||
| @@ -219,8 +219,10 @@ func inactiveResumeFenceAllows(current inactiveSession, exists bool, expected *i | |||||
| 219 | return validate == nil || validate(current, authoritative, authoritativeExists) | 219 | return validate == nil || validate(current, authoritative, authoritativeExists) | ||
| 220 | } | 220 | } | ||
| 221 | 221 | ||||
| 222 | - | func (d *Daemon) createSessionLockedWithModeAndInactiveFence(name string, ephemeral bool, cwd string, sz domain.Size, env []string, expectedInactive *inactiveSession, validateInactive inactiveResumeValidator, restoredTabNames ...[]string) (*session, error) { | 222 | + | func (d *Daemon) createSessionLockedWithModeAndInactiveFence(name string, ephemeral bool, cwd string, geometry domain.Geometry, env []string, expectedInactive *inactiveSession, validateInactive inactiveResumeValidator, restoredTabNames ...[]string) (*session, error) { |
| 223 | env = copyEnvironment(env) | 223 | env = copyEnvironment(env) | ||
| 224 | + | geometry = geometry.NormalizePixels() | |||
| 225 | + | sz := geometry.Size | |||
| 224 | if _, reserved := d.creating[name]; reserved { | 226 | if _, reserved := d.creating[name]; reserved { | ||
| 225 | return nil, errSessionNameInUse | 227 | return nil, errSessionNameInUse | ||
| 226 | } | 228 | } | ||
| @@ -285,6 +287,7 @@ func (d *Daemon) createSessionLockedWithModeAndInactiveFence(name string, epheme | |||||
| 285 | return nil, domain.UserErr(domain.NoticeSessionSpawn, "couldn't create session", errors.New("daemon: PTY factory is unavailable")) | 287 | return nil, domain.UserErr(domain.NoticeSessionSpawn, "couldn't create session", errors.New("daemon: PTY factory is unavailable")) | ||
| 286 | } | 288 | } | ||
| 287 | tbSize := contentSize(sz) | 289 | tbSize := contentSize(sz) | ||
| 290 | + | initialPaneGeometry := scalePaneGeometry(geometry, tbSize) | |||
| 288 | var names []string | 291 | var names []string | ||
| 289 | var tabRecords []domain.CatalogueTabRecord | 292 | var tabRecords []domain.CatalogueTabRecord | ||
| 290 | if resuming && len(stopped.tabRecords) != 0 { | 293 | if resuming && len(stopped.tabRecords) != 0 { | ||
| @@ -308,7 +311,7 @@ func (d *Daemon) createSessionLockedWithModeAndInactiveFence(name string, epheme | |||||
| 308 | } | 311 | } | ||
| 309 | launch := d.shellLaunch(env) | 312 | launch := d.shellLaunch(env) | ||
| 310 | lifetime := d.newPaneProcessLifetime(d.serveCtx) | 313 | lifetime := d.newPaneProcessLifetime(d.serveCtx) | ||
| 311 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, tbSize) | 314 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, initialPaneGeometry) |
| 312 | if err != nil { | 315 | if err != nil { | ||
| 313 | lifetime.abort() | 316 | lifetime.abort() | ||
| 314 | if pty != nil { | 317 | if pty != nil { | ||
| @@ -318,7 +321,12 @@ func (d *Daemon) createSessionLockedWithModeAndInactiveFence(name string, epheme | |||||
| 318 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "session") | 321 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "session") | ||
| 319 | return nil, domain.UserErr(domain.NoticeSessionSpawn, "couldn't create session: shell failed to start", err) | 322 | return nil, domain.UserErr(domain.NoticeSessionSpawn, "couldn't create session: shell failed to start", err) | ||
| 320 | } | 323 | } | ||
| 324 | + | appliedPaneGeometry := initialPaneGeometry | |||
| 321 | tb := newTabWithStableIDAndTitle(tabStableID, paneStableID, pty, tbSize, launch.title) | 325 | tb := newTabWithStableIDAndTitle(tabStableID, paneStableID, pty, tbSize, launch.title) | ||
| 326 | + | if pane := tb.focusedPane(); pane != nil { | |||
| 327 | + | pane.geometry = appliedPaneGeometry | |||
| 328 | + | setScreenGeometry(pane.screen, appliedPaneGeometry) | |||
| 329 | + | } | |||
| 322 | if !lifetime.publish(tb.focusedPane()) { | 330 | if !lifetime.publish(tb.focusedPane()) { | ||
| 323 | lifetime.abort() | 331 | lifetime.abort() | ||
| 324 | _ = pty.Close() | 332 | _ = pty.Close() | ||
| @@ -453,7 +461,7 @@ func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name | |||||
| 453 | if err := domain.ValidateSessionName(name); err != nil { | 461 | if err := domain.ValidateSessionName(name); err != nil { | ||
| 454 | return err | 462 | return err | ||
| 455 | } | 463 | } | ||
| 456 | - | sz := ac.sizeSnapshot() | 464 | + | geometry := ac.geometrySnapshot() |
| 457 | d.mu.Lock() | 465 | d.mu.Lock() | ||
| 458 | if d.closing { | 466 | if d.closing { | ||
| 459 | d.mu.Unlock() | 467 | d.mu.Unlock() | ||
| @@ -473,7 +481,7 @@ func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name | |||||
| 473 | return errors.New("client detached") | 481 | return errors.New("client detached") | ||
| 474 | } | 482 | } | ||
| 475 | 483 | ||||
| 476 | - | newSess, err := d.createSessionLockedWithMode(name, false, cwd, sz, env) | 484 | + | newSess, err := d.createSessionLockedWithMode(name, false, cwd, geometry, env) |
| 477 | d.mu.Unlock() | 485 | d.mu.Unlock() | ||
| 478 | if err != nil { | 486 | if err != nil { | ||
| 479 | return err | 487 | return err | ||
| @@ -537,7 +545,7 @@ func (d *Daemon) createSessionAndSwitchForAttachment(token attachmentConnectionT | |||||
| 537 | cwd, env := source.cwd, copyEnvironment(source.env) | 545 | cwd, env := source.cwd, copyEnvironment(source.env) | ||
| 538 | source.mu.Unlock() | 546 | source.mu.Unlock() | ||
| 539 | var createErr error | 547 | var createErr error | ||
| 540 | - | created, createErr = d.createSessionLockedWithMode(name, false, cwd, token.ac.sizeSnapshot(), env) | 548 | + | created, createErr = d.createSessionLockedWithMode(name, false, cwd, token.ac.geometrySnapshot(), env) |
| 541 | return created, createErr | 549 | return created, createErr | ||
| 542 | }, | 550 | }, | ||
| 543 | }) | 551 | }) | ||
| @@ -556,7 +564,7 @@ func (d *Daemon) createSessionAndSwitchForAttachment(token attachmentConnectionT | |||||
| 556 | } | 564 | } | ||
| 557 | 565 | ||||
| 558 | func (d *Daemon) createEphemeralSessionAndSwitch(from *session, ac *attachedClient) error { | 566 | func (d *Daemon) createEphemeralSessionAndSwitch(from *session, ac *attachedClient) error { | ||
| 559 | - | sz := ac.sizeSnapshot() | 567 | + | geometry := ac.geometrySnapshot() |
| 560 | d.mu.Lock() | 568 | d.mu.Lock() | ||
| 561 | if d.closing { | 569 | if d.closing { | ||
| 562 | d.mu.Unlock() | 570 | d.mu.Unlock() | ||
| @@ -571,7 +579,7 @@ func (d *Daemon) createEphemeralSessionAndSwitch(from *session, ac *attachedClie | |||||
| 571 | return errors.New("client detached") | 579 | return errors.New("client detached") | ||
| 572 | } | 580 | } | ||
| 573 | name := d.allocEphemeralNameLocked() | 581 | name := d.allocEphemeralNameLocked() | ||
| 574 | - | newSess, err := d.createSessionLockedWithMode(name, true, cwd, sz, env) | 582 | + | newSess, err := d.createSessionLockedWithMode(name, true, cwd, geometry, env) |
| 575 | d.mu.Unlock() | 583 | d.mu.Unlock() | ||
| 576 | if err != nil { | 584 | if err != nil { | ||
| 577 | return err | 585 | return err | ||
| @@ -613,7 +621,7 @@ func (d *Daemon) createEphemeralSessionAndSwitchForAttachment(token attachmentCo | |||||
| 613 | source.mu.Unlock() | 621 | source.mu.Unlock() | ||
| 614 | name := d.allocEphemeralNameLocked() | 622 | name := d.allocEphemeralNameLocked() | ||
| 615 | var createErr error | 623 | var createErr error | ||
| 616 | - | created, createErr = d.createSessionLockedWithMode(name, true, cwd, token.ac.sizeSnapshot(), env) | 624 | + | created, createErr = d.createSessionLockedWithMode(name, true, cwd, token.ac.geometrySnapshot(), env) |
| 617 | return created, createErr | 625 | return created, createErr | ||
| 618 | }, | 626 | }, | ||
| 619 | }) | 627 | }) | ||
| @@ -642,13 +650,19 @@ func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, _ dom | |||||
| 642 | attachments := sess.snapshotAttachmentsLocked() | 650 | attachments := sess.snapshotAttachmentsLocked() | ||
| 643 | sess.mu.Unlock() | 651 | sess.mu.Unlock() | ||
| 644 | tbSize := contentSize(sess.fullViewportSize()) | 652 | tbSize := contentSize(sess.fullViewportSize()) | ||
| 653 | + | claimGeometry := domain.Geometry{Size: tbSize} | |||
| 654 | + | if source, ok := sess.geometrySourceSnapshot(); ok { | |||
| 655 | + | claimGeometry = scalePaneGeometry(source.geometry, tbSize) | |||
| 656 | + | } else if ac != nil { | |||
| 657 | + | claimGeometry = scalePaneGeometry(ac.geometrySnapshot(), tbSize) | |||
| 658 | + | } | |||
| 645 | tabStableID, paneStableID, err := d.newTabPaneStableIDs() | 659 | tabStableID, paneStableID, err := d.newTabPaneStableIDs() | ||
| 646 | if err != nil { | 660 | if err != nil { | ||
| 647 | return err | 661 | return err | ||
| 648 | } | 662 | } | ||
| 649 | launch := d.shellLaunch(env) | 663 | launch := d.shellLaunch(env) | ||
| 650 | lifetime := d.newPaneProcessLifetime(sess.ctx) | 664 | lifetime := d.newPaneProcessLifetime(sess.ctx) | ||
| 651 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, tbSize) | 665 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(env, name, tabStableID, paneStableID), cwd, claimGeometry) |
| 652 | if err != nil { | 666 | if err != nil { | ||
| 653 | lifetime.abort() | 667 | lifetime.abort() | ||
| 654 | if pty != nil { | 668 | if pty != nil { | ||
| @@ -658,6 +672,10 @@ func (d *Daemon) createTabForAttachment(sess *session, ac *attachedClient, _ dom | |||||
| 658 | return domain.UserErr(domain.NoticeTabSpawn, "couldn't open tab: shell failed to start", err) | 672 | return domain.UserErr(domain.NoticeTabSpawn, "couldn't open tab: shell failed to start", err) | ||
| 659 | } | 673 | } | ||
| 660 | tb := newTabWithStableIDAndTitle(tabStableID, paneStableID, pty, tbSize, launch.title) | 674 | tb := newTabWithStableIDAndTitle(tabStableID, paneStableID, pty, tbSize, launch.title) | ||
| 675 | + | if pane := tb.focusedPane(); pane != nil { | |||
| 676 | + | pane.geometry = claimGeometry | |||
| 677 | + | setScreenGeometry(pane.screen, claimGeometry) | |||
| 678 | + | } | |||
| 661 | themeClient := ac | 679 | themeClient := ac | ||
| 662 | if themeClient == nil && len(attachments) != 0 { | 680 | if themeClient == nil && len(attachments) != 0 { | ||
| 663 | themeClient = attachments[0] | 681 | themeClient = attachments[0] | ||
M internal/usecase/daemon/session_core.go +3 -2
| @@ -29,8 +29,9 @@ type sessionCore struct { | |||
| 29 | 29 | // controls shared PTY/layout geometry. The pointer is published atomically | |
| 30 | 30 | // so resize transactions can reject stale claims without taking session.mu. | |
| 31 | 31 | // geometryMu serializes claim publication with the final shared-layout commit. | |
| 32 | - | geometryMu sync.Mutex | |
| 33 | - | geometryOwner atomic.Pointer[attachedClient] | |
| 32 | + | geometryMu sync.Mutex | |
| 33 | + | geometryOwner atomic.Pointer[attachedClient] | |
| 34 | + | appliedGeometry domain.Geometry // guarded by geometryMu | |
| 34 | 35 | // geometryClaimSeq is mutated while mu and geometryMu are held; its value | |
| 35 | 36 | // is copied into attachedClient.geometryClaim. | |
| 36 | 37 | geometryClaimSeq uint64 | |
| @@ -29,8 +29,9 @@ type sessionCore struct { | |||||
| 29 | // controls shared PTY/layout geometry. The pointer is published atomically | 29 | // controls shared PTY/layout geometry. The pointer is published atomically | ||
| 30 | // so resize transactions can reject stale claims without taking session.mu. | 30 | // so resize transactions can reject stale claims without taking session.mu. | ||
| 31 | // geometryMu serializes claim publication with the final shared-layout commit. | 31 | // geometryMu serializes claim publication with the final shared-layout commit. | ||
| 32 | - | geometryMu sync.Mutex | 32 | + | geometryMu sync.Mutex |
| 33 | - | geometryOwner atomic.Pointer[attachedClient] | 33 | + | geometryOwner atomic.Pointer[attachedClient] |
| 34 | + | appliedGeometry domain.Geometry // guarded by geometryMu | |||
| 34 | // geometryClaimSeq is mutated while mu and geometryMu are held; its value | 35 | // geometryClaimSeq is mutated while mu and geometryMu are held; its value | ||
| 35 | // is copied into attachedClient.geometryClaim. | 36 | // is copied into attachedClient.geometryClaim. | ||
| 36 | geometryClaimSeq uint64 | 37 | geometryClaimSeq uint64 | ||
M internal/usecase/daemon/session_geometry.go +48 -8
| @@ -8,7 +8,7 @@ import "github.com/bnema/vev/internal/domain" | |||
| 8 | 8 | // directly instead of publishing them through the attachment registry. | |
| 9 | 9 | type sessionGeometrySource struct { | |
| 10 | 10 | attachment *attachedClient | |
| 11 | - | size domain.Size | |
| 11 | + | geometry domain.Geometry | |
| 12 | 12 | claim uint64 | |
| 13 | 13 | } | |
| 14 | 14 | ||
| @@ -25,18 +25,18 @@ func (s *session) geometrySourceSnapshot() (sessionGeometrySource, bool) { | |||
| 25 | 25 | s.mu.Lock() | |
| 26 | 26 | s.refreshGeometryOwnerLocked() | |
| 27 | 27 | owner := s.geometryOwner.Load() | |
| 28 | - | var size domain.Size | |
| 28 | + | var geometry domain.Geometry | |
| 29 | 29 | var claim uint64 | |
| 30 | 30 | if owner != nil { | |
| 31 | - | size = owner.geometryClaimSizeSnapshot() | |
| 31 | + | geometry = owner.geometryClaimSnapshot() | |
| 32 | 32 | claim = owner.geometryClaim.Load() | |
| 33 | 33 | } | |
| 34 | 34 | s.mu.Unlock() | |
| 35 | 35 | ||
| 36 | - | if owner == nil || !size.Valid() { | |
| 36 | + | if owner == nil || !geometry.Valid() { | |
| 37 | 37 | return sessionGeometrySource{}, false | |
| 38 | 38 | } | |
| 39 | - | return sessionGeometrySource{attachment: owner, size: size, claim: claim}, true | |
| 39 | + | return sessionGeometrySource{attachment: owner, geometry: geometry, claim: claim}, true | |
| 40 | 40 | } | |
| 41 | 41 | ||
| 42 | 42 | // geometryClaimTokenCurrent is deliberately lock-free. applySessionLayout invokes | |
| @@ -51,7 +51,36 @@ func (s *session) geometryClaimTokenCurrent(attachment *attachedClient, claim ui | |||
| 51 | 51 | ||
| 52 | 52 | func (s *session) geometryClaimCurrent(source sessionGeometrySource) bool { | |
| 53 | 53 | return s.geometryClaimTokenCurrent(source.attachment, source.claim) && | |
| 54 | - | (source.attachment == nil || source.attachment.geometryClaimSizeSnapshot() == source.size) | |
| 54 | + | (source.attachment == nil || source.attachment.geometryClaimSnapshot() == source.geometry) | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // paneGeometry maps the controlling terminal's optional pixel dimensions to a | |
| 58 | + | // pane's cell rectangle. Integer division deliberately truncates: the kernel | |
| 59 | + | // winsize and CSI reports cannot represent fractional pixels. | |
| 60 | + | // scalePaneGeometry maps a full terminal claim to one pane rectangle. Pixel | |
| 61 | + | // dimensions are deliberately scaled with integer truncation, matching the | |
| 62 | + | // mapping used by paneGeometry, but it is also usable while a session is still | |
| 63 | + | // private and has not published its attachment owner. | |
| 64 | + | func scalePaneGeometry(full domain.Geometry, size domain.Size) domain.Geometry { | |
| 65 | + | geometry := domain.Geometry{Size: size} | |
| 66 | + | if !full.Valid() || !size.Valid() || !full.PixelsKnown() { | |
| 67 | + | return geometry | |
| 68 | + | } | |
| 69 | + | geometry.PixelWidth = int(int64(full.PixelWidth) * int64(size.Cols) / int64(full.Cols)) | |
| 70 | + | geometry.PixelHeight = int(int64(full.PixelHeight) * int64(size.Rows) / int64(full.Rows)) | |
| 71 | + | return geometry.NormalizePixels() | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | func (s *session) paneGeometry(size domain.Size) domain.Geometry { | |
| 75 | + | geometry := domain.Geometry{Size: size} | |
| 76 | + | if s == nil || !size.Valid() { | |
| 77 | + | return geometry | |
| 78 | + | } | |
| 79 | + | owner := s.geometryOwner.Load() | |
| 80 | + | if owner == nil { | |
| 81 | + | return geometry | |
| 82 | + | } | |
| 83 | + | return scalePaneGeometry(owner.geometryClaimSnapshot(), size) | |
| 55 | 84 | } | |
| 56 | 85 | ||
| 57 | 86 | // sessionGeometryViewport is called with a non-nil session by the daemon | |
| @@ -61,7 +90,7 @@ func sessionGeometryViewport(sess *session) (domain.Size, sessionGeometrySource, | |||
| 61 | 90 | if !ok { | |
| 62 | 91 | return domain.Size{}, sessionGeometrySource{}, false | |
| 63 | 92 | } | |
| 64 | - | content := contentSize(source.size) | |
| 93 | + | content := contentSize(source.geometry.Size) | |
| 65 | 94 | return domain.Size{Cols: content.Cols, Rows: content.Rows + tabChromeRows}, source, true | |
| 66 | 95 | } | |
| 67 | 96 | ||
| @@ -84,10 +113,21 @@ func (d *Daemon) recalculateSessionGeometry(sess *session, source *attachedClien | |||
| 84 | 113 | return false | |
| 85 | 114 | } | |
| 86 | 115 | current := func() bool { return sess.geometryClaimCurrent(owner) } | |
| 87 | - | if contentSize(sess.fullViewportSize()) == contentSize(want) { | |
| 116 | + | sess.geometryMu.Lock() | |
| 117 | + | appliedGeometry := sess.appliedGeometry | |
| 118 | + | sess.geometryMu.Unlock() | |
| 119 | + | pixelGeometryChanged := (appliedGeometry.PixelsKnown() || owner.geometry.PixelsKnown()) && appliedGeometry != owner.geometry | |
| 120 | + | if !pixelGeometryChanged && contentSize(sess.fullViewportSize()) == contentSize(want) { | |
| 88 | 121 | return false | |
| 89 | 122 | } | |
| 90 | 123 | failed, ok := d.applySessionLayout(sess, want, current, nil) | |
| 124 | + | if ok { | |
| 125 | + | sess.geometryMu.Lock() | |
| 126 | + | if current() { | |
| 127 | + | sess.appliedGeometry = owner.geometry | |
| 128 | + | } | |
| 129 | + | sess.geometryMu.Unlock() | |
| 130 | + | } | |
| 91 | 131 | if ok && len(failed) != 0 { | |
| 92 | 132 | seen := make(map[*tab]struct{}, len(failed)) | |
| 93 | 133 | for _, member := range failed { | |
| @@ -8,7 +8,7 @@ import "github.com/bnema/vev/internal/domain" | |||||
| 8 | // directly instead of publishing them through the attachment registry. | 8 | // directly instead of publishing them through the attachment registry. | ||
| 9 | type sessionGeometrySource struct { | 9 | type sessionGeometrySource struct { | ||
| 10 | attachment *attachedClient | 10 | attachment *attachedClient | ||
| 11 | - | size domain.Size | 11 | + | geometry domain.Geometry |
| 12 | claim uint64 | 12 | claim uint64 | ||
| 13 | } | 13 | } | ||
| 14 | 14 | ||||
| @@ -25,18 +25,18 @@ func (s *session) geometrySourceSnapshot() (sessionGeometrySource, bool) { | |||||
| 25 | s.mu.Lock() | 25 | s.mu.Lock() | ||
| 26 | s.refreshGeometryOwnerLocked() | 26 | s.refreshGeometryOwnerLocked() | ||
| 27 | owner := s.geometryOwner.Load() | 27 | owner := s.geometryOwner.Load() | ||
| 28 | - | var size domain.Size | 28 | + | var geometry domain.Geometry |
| 29 | var claim uint64 | 29 | var claim uint64 | ||
| 30 | if owner != nil { | 30 | if owner != nil { | ||
| 31 | - | size = owner.geometryClaimSizeSnapshot() | 31 | + | geometry = owner.geometryClaimSnapshot() |
| 32 | claim = owner.geometryClaim.Load() | 32 | claim = owner.geometryClaim.Load() | ||
| 33 | } | 33 | } | ||
| 34 | s.mu.Unlock() | 34 | s.mu.Unlock() | ||
| 35 | 35 | ||||
| 36 | - | if owner == nil || !size.Valid() { | 36 | + | if owner == nil || !geometry.Valid() { |
| 37 | return sessionGeometrySource{}, false | 37 | return sessionGeometrySource{}, false | ||
| 38 | } | 38 | } | ||
| 39 | - | return sessionGeometrySource{attachment: owner, size: size, claim: claim}, true | 39 | + | return sessionGeometrySource{attachment: owner, geometry: geometry, claim: claim}, true |
| 40 | } | 40 | } | ||
| 41 | 41 | ||||
| 42 | // geometryClaimTokenCurrent is deliberately lock-free. applySessionLayout invokes | 42 | // geometryClaimTokenCurrent is deliberately lock-free. applySessionLayout invokes | ||
| @@ -51,7 +51,36 @@ func (s *session) geometryClaimTokenCurrent(attachment *attachedClient, claim ui | |||||
| 51 | 51 | ||||
| 52 | func (s *session) geometryClaimCurrent(source sessionGeometrySource) bool { | 52 | func (s *session) geometryClaimCurrent(source sessionGeometrySource) bool { | ||
| 53 | return s.geometryClaimTokenCurrent(source.attachment, source.claim) && | 53 | return s.geometryClaimTokenCurrent(source.attachment, source.claim) && | ||
| 54 | - | (source.attachment == nil || source.attachment.geometryClaimSizeSnapshot() == source.size) | 54 | + | (source.attachment == nil || source.attachment.geometryClaimSnapshot() == source.geometry) |
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // paneGeometry maps the controlling terminal's optional pixel dimensions to a | |||
| 58 | + | // pane's cell rectangle. Integer division deliberately truncates: the kernel | |||
| 59 | + | // winsize and CSI reports cannot represent fractional pixels. | |||
| 60 | + | // scalePaneGeometry maps a full terminal claim to one pane rectangle. Pixel | |||
| 61 | + | // dimensions are deliberately scaled with integer truncation, matching the | |||
| 62 | + | // mapping used by paneGeometry, but it is also usable while a session is still | |||
| 63 | + | // private and has not published its attachment owner. | |||
| 64 | + | func scalePaneGeometry(full domain.Geometry, size domain.Size) domain.Geometry { | |||
| 65 | + | geometry := domain.Geometry{Size: size} | |||
| 66 | + | if !full.Valid() || !size.Valid() || !full.PixelsKnown() { | |||
| 67 | + | return geometry | |||
| 68 | + | } | |||
| 69 | + | geometry.PixelWidth = int(int64(full.PixelWidth) * int64(size.Cols) / int64(full.Cols)) | |||
| 70 | + | geometry.PixelHeight = int(int64(full.PixelHeight) * int64(size.Rows) / int64(full.Rows)) | |||
| 71 | + | return geometry.NormalizePixels() | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | func (s *session) paneGeometry(size domain.Size) domain.Geometry { | |||
| 75 | + | geometry := domain.Geometry{Size: size} | |||
| 76 | + | if s == nil || !size.Valid() { | |||
| 77 | + | return geometry | |||
| 78 | + | } | |||
| 79 | + | owner := s.geometryOwner.Load() | |||
| 80 | + | if owner == nil { | |||
| 81 | + | return geometry | |||
| 82 | + | } | |||
| 83 | + | return scalePaneGeometry(owner.geometryClaimSnapshot(), size) | |||
| 55 | } | 84 | } | ||
| 56 | 85 | ||||
| 57 | // sessionGeometryViewport is called with a non-nil session by the daemon | 86 | // sessionGeometryViewport is called with a non-nil session by the daemon | ||
| @@ -61,7 +90,7 @@ func sessionGeometryViewport(sess *session) (domain.Size, sessionGeometrySource, | |||||
| 61 | if !ok { | 90 | if !ok { | ||
| 62 | return domain.Size{}, sessionGeometrySource{}, false | 91 | return domain.Size{}, sessionGeometrySource{}, false | ||
| 63 | } | 92 | } | ||
| 64 | - | content := contentSize(source.size) | 93 | + | content := contentSize(source.geometry.Size) |
| 65 | return domain.Size{Cols: content.Cols, Rows: content.Rows + tabChromeRows}, source, true | 94 | return domain.Size{Cols: content.Cols, Rows: content.Rows + tabChromeRows}, source, true | ||
| 66 | } | 95 | } | ||
| 67 | 96 | ||||
| @@ -84,10 +113,21 @@ func (d *Daemon) recalculateSessionGeometry(sess *session, source *attachedClien | |||||
| 84 | return false | 113 | return false | ||
| 85 | } | 114 | } | ||
| 86 | current := func() bool { return sess.geometryClaimCurrent(owner) } | 115 | current := func() bool { return sess.geometryClaimCurrent(owner) } | ||
| 87 | - | if contentSize(sess.fullViewportSize()) == contentSize(want) { | 116 | + | sess.geometryMu.Lock() |
| 117 | + | appliedGeometry := sess.appliedGeometry | |||
| 118 | + | sess.geometryMu.Unlock() | |||
| 119 | + | pixelGeometryChanged := (appliedGeometry.PixelsKnown() || owner.geometry.PixelsKnown()) && appliedGeometry != owner.geometry | |||
| 120 | + | if !pixelGeometryChanged && contentSize(sess.fullViewportSize()) == contentSize(want) { | |||
| 88 | return false | 121 | return false | ||
| 89 | } | 122 | } | ||
| 90 | failed, ok := d.applySessionLayout(sess, want, current, nil) | 123 | failed, ok := d.applySessionLayout(sess, want, current, nil) | ||
| 124 | + | if ok { | |||
| 125 | + | sess.geometryMu.Lock() | |||
| 126 | + | if current() { | |||
| 127 | + | sess.appliedGeometry = owner.geometry | |||
| 128 | + | } | |||
| 129 | + | sess.geometryMu.Unlock() | |||
| 130 | + | } | |||
| 91 | if ok && len(failed) != 0 { | 131 | if ok && len(failed) != 0 { | ||
| 92 | seen := make(map[*tab]struct{}, len(failed)) | 132 | seen := make(map[*tab]struct{}, len(failed)) | ||
| 93 | for _, member := range failed { | 133 | for _, member := range failed { | ||
M internal/usecase/daemon/session_test.go +16 -16
| @@ -25,8 +25,8 @@ import ( | |||
| 25 | 25 | ||
| 26 | 26 | // --- test doubles ----------------------------------------------------------- | |
| 27 | 27 | func expectFloatingPrewarmOpen(factory *portsmocks.MockPTYFactory, normalSize domain.Size, floating ports.PTY) { | |
| 28 | - | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 29 | - | return got != normalSize && got.Valid() | |
| 28 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { | |
| 29 | + | return got.Size != normalSize && got.Size.Valid() | |
| 30 | 30 | })).Return(floating, nil).Maybe() | |
| 31 | 31 | } | |
| 32 | 32 | ||
| @@ -213,11 +213,11 @@ func TestCreateTabClosesPTYIfSessionKilledDuringOpen(t *testing.T) { | |||
| 213 | 213 | ||
| 214 | 214 | f := portsmocks.NewMockPTYFactory(t) | |
| 215 | 215 | normalSize := domain.Size{Cols: 80, Rows: 22} | |
| 216 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).Return(p1, nil).Once() | |
| 216 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).Return(p1, nil).Once() | |
| 217 | 217 | floating := newQuietPTY() | |
| 218 | 218 | expectFloatingPrewarmOpen(f, normalSize, floating) | |
| 219 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 220 | - | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 219 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).RunAndReturn( | |
| 220 | + | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 221 | 221 | openCtx <- ctx | |
| 222 | 222 | select { | |
| 223 | 223 | case <-ctx.Done(): | |
| @@ -494,13 +494,13 @@ func TestHelloRacingShutdownIsRejected(t *testing.T) { | |||
| 494 | 494 | var opensAfterShutdown atomic.Int32 | |
| 495 | 495 | d := newTestDaemon(t, f, stubClock{}) | |
| 496 | 496 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 497 | - | func(_ context.Context, _ string, _ []string, _ []string, _ string, size domain.Size) (ports.PTY, error) { | |
| 497 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, geometry domain.Geometry) (ports.PTY, error) { | |
| 498 | 498 | select { | |
| 499 | 499 | case <-d.done: | |
| 500 | 500 | opensAfterShutdown.Add(1) | |
| 501 | 501 | default: | |
| 502 | 502 | } | |
| 503 | - | if size == normalSize { | |
| 503 | + | if geometry.Size == normalSize { | |
| 504 | 504 | return p, nil | |
| 505 | 505 | } | |
| 506 | 506 | select { | |
| @@ -1255,7 +1255,7 @@ func TestNewSessionAssignsStableIDsAndChildEnv(t *testing.T) { | |||
| 1255 | 1255 | var gotEnv []string | |
| 1256 | 1256 | f := portsmocks.NewMockPTYFactory(t) | |
| 1257 | 1257 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 1258 | - | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1258 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 1259 | 1259 | gotEnv = append([]string(nil), env...) | |
| 1260 | 1260 | return p, nil | |
| 1261 | 1261 | }, | |
| @@ -1430,8 +1430,8 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1430 | 1430 | var opens [][]string | |
| 1431 | 1431 | f := portsmocks.NewMockPTYFactory(t) | |
| 1432 | 1432 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 1433 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1434 | - | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1433 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).RunAndReturn( | |
| 1434 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 1435 | 1435 | opens = append(opens, append([]string(nil), env...)) | |
| 1436 | 1436 | if len(opens) == 1 { | |
| 1437 | 1437 | return p1, nil | |
| @@ -1473,8 +1473,8 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1473 | 1473 | var opens [][]string | |
| 1474 | 1474 | f := portsmocks.NewMockPTYFactory(t) | |
| 1475 | 1475 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 1476 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1477 | - | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1476 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).RunAndReturn( | |
| 1477 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 1478 | 1478 | opens = append(opens, append([]string(nil), env...)) | |
| 1479 | 1479 | if len(opens) == 1 { | |
| 1480 | 1480 | return p1, nil | |
| @@ -1518,8 +1518,8 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 1518 | 1518 | var opens [][]string | |
| 1519 | 1519 | f := portsmocks.NewMockPTYFactory(t) | |
| 1520 | 1520 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 1521 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1522 | - | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1521 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).RunAndReturn( | |
| 1522 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 1523 | 1523 | opens = append(opens, append([]string(nil), env...)) | |
| 1524 | 1524 | if len(opens) == 1 { | |
| 1525 | 1525 | return p1, nil | |
| @@ -1586,8 +1586,8 @@ func TestAttachEnvironmentReplacesFuturePTYInputs(t *testing.T) { | |||
| 1586 | 1586 | var envs [][]string | |
| 1587 | 1587 | f := portsmocks.NewMockPTYFactory(t) | |
| 1588 | 1588 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 1589 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1590 | - | func(_ context.Context, command string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1589 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).RunAndReturn( | |
| 1590 | + | func(_ context.Context, command string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 1591 | 1591 | commands = append(commands, command) | |
| 1592 | 1592 | envs = append(envs, append([]string(nil), env...)) | |
| 1593 | 1593 | if len(envs) == 1 { | |
| @@ -25,8 +25,8 @@ import ( | |||||
| 25 | 25 | ||||
| 26 | // --- test doubles ----------------------------------------------------------- | 26 | // --- test doubles ----------------------------------------------------------- | ||
| 27 | func expectFloatingPrewarmOpen(factory *portsmocks.MockPTYFactory, normalSize domain.Size, floating ports.PTY) { | 27 | func expectFloatingPrewarmOpen(factory *portsmocks.MockPTYFactory, normalSize domain.Size, floating ports.PTY) { | ||
| 28 | - | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | 28 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { |
| 29 | - | return got != normalSize && got.Valid() | 29 | + | return got.Size != normalSize && got.Size.Valid() |
| 30 | })).Return(floating, nil).Maybe() | 30 | })).Return(floating, nil).Maybe() | ||
| 31 | } | 31 | } | ||
| 32 | 32 | ||||
| @@ -213,11 +213,11 @@ func TestCreateTabClosesPTYIfSessionKilledDuringOpen(t *testing.T) { | |||||
| 213 | 213 | ||||
| 214 | f := portsmocks.NewMockPTYFactory(t) | 214 | f := portsmocks.NewMockPTYFactory(t) | ||
| 215 | normalSize := domain.Size{Cols: 80, Rows: 22} | 215 | normalSize := domain.Size{Cols: 80, Rows: 22} | ||
| 216 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).Return(p1, nil).Once() | 216 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).Return(p1, nil).Once() |
| 217 | floating := newQuietPTY() | 217 | floating := newQuietPTY() | ||
| 218 | expectFloatingPrewarmOpen(f, normalSize, floating) | 218 | expectFloatingPrewarmOpen(f, normalSize, floating) | ||
| 219 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | 219 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).RunAndReturn( |
| 220 | - | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | 220 | + | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 221 | openCtx <- ctx | 221 | openCtx <- ctx | ||
| 222 | select { | 222 | select { | ||
| 223 | case <-ctx.Done(): | 223 | case <-ctx.Done(): | ||
| @@ -494,13 +494,13 @@ func TestHelloRacingShutdownIsRejected(t *testing.T) { | |||||
| 494 | var opensAfterShutdown atomic.Int32 | 494 | var opensAfterShutdown atomic.Int32 | ||
| 495 | d := newTestDaemon(t, f, stubClock{}) | 495 | d := newTestDaemon(t, f, stubClock{}) | ||
| 496 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 496 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | ||
| 497 | - | func(_ context.Context, _ string, _ []string, _ []string, _ string, size domain.Size) (ports.PTY, error) { | 497 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, geometry domain.Geometry) (ports.PTY, error) { |
| 498 | select { | 498 | select { | ||
| 499 | case <-d.done: | 499 | case <-d.done: | ||
| 500 | opensAfterShutdown.Add(1) | 500 | opensAfterShutdown.Add(1) | ||
| 501 | default: | 501 | default: | ||
| 502 | } | 502 | } | ||
| 503 | - | if size == normalSize { | 503 | + | if geometry.Size == normalSize { |
| 504 | return p, nil | 504 | return p, nil | ||
| 505 | } | 505 | } | ||
| 506 | select { | 506 | select { | ||
| @@ -1255,7 +1255,7 @@ func TestNewSessionAssignsStableIDsAndChildEnv(t *testing.T) { | |||||
| 1255 | var gotEnv []string | 1255 | var gotEnv []string | ||
| 1256 | f := portsmocks.NewMockPTYFactory(t) | 1256 | f := portsmocks.NewMockPTYFactory(t) | ||
| 1257 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 1257 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | ||
| 1258 | - | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 1258 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 1259 | gotEnv = append([]string(nil), env...) | 1259 | gotEnv = append([]string(nil), env...) | ||
| 1260 | return p, nil | 1260 | return p, nil | ||
| 1261 | }, | 1261 | }, | ||
| @@ -1430,8 +1430,8 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1430 | var opens [][]string | 1430 | var opens [][]string | ||
| 1431 | f := portsmocks.NewMockPTYFactory(t) | 1431 | f := portsmocks.NewMockPTYFactory(t) | ||
| 1432 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | 1432 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | ||
| 1433 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | 1433 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).RunAndReturn( |
| 1434 | - | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 1434 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 1435 | opens = append(opens, append([]string(nil), env...)) | 1435 | opens = append(opens, append([]string(nil), env...)) | ||
| 1436 | if len(opens) == 1 { | 1436 | if len(opens) == 1 { | ||
| 1437 | return p1, nil | 1437 | return p1, nil | ||
| @@ -1473,8 +1473,8 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1473 | var opens [][]string | 1473 | var opens [][]string | ||
| 1474 | f := portsmocks.NewMockPTYFactory(t) | 1474 | f := portsmocks.NewMockPTYFactory(t) | ||
| 1475 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | 1475 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | ||
| 1476 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | 1476 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).RunAndReturn( |
| 1477 | - | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 1477 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 1478 | opens = append(opens, append([]string(nil), env...)) | 1478 | opens = append(opens, append([]string(nil), env...)) | ||
| 1479 | if len(opens) == 1 { | 1479 | if len(opens) == 1 { | ||
| 1480 | return p1, nil | 1480 | return p1, nil | ||
| @@ -1518,8 +1518,8 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 1518 | var opens [][]string | 1518 | var opens [][]string | ||
| 1519 | f := portsmocks.NewMockPTYFactory(t) | 1519 | f := portsmocks.NewMockPTYFactory(t) | ||
| 1520 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | 1520 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | ||
| 1521 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | 1521 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).RunAndReturn( |
| 1522 | - | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 1522 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 1523 | opens = append(opens, append([]string(nil), env...)) | 1523 | opens = append(opens, append([]string(nil), env...)) | ||
| 1524 | if len(opens) == 1 { | 1524 | if len(opens) == 1 { | ||
| 1525 | return p1, nil | 1525 | return p1, nil | ||
| @@ -1586,8 +1586,8 @@ func TestAttachEnvironmentReplacesFuturePTYInputs(t *testing.T) { | |||||
| 1586 | var envs [][]string | 1586 | var envs [][]string | ||
| 1587 | f := portsmocks.NewMockPTYFactory(t) | 1587 | f := portsmocks.NewMockPTYFactory(t) | ||
| 1588 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | 1588 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | ||
| 1589 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | 1589 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Geometry) bool { return got.Size == normalSize })).RunAndReturn( |
| 1590 | - | func(_ context.Context, command string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 1590 | + | func(_ context.Context, command string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 1591 | commands = append(commands, command) | 1591 | commands = append(commands, command) | ||
| 1592 | envs = append(envs, append([]string(nil), env...)) | 1592 | envs = append(envs, append([]string(nil), env...)) | ||
| 1593 | if len(envs) == 1 { | 1593 | if len(envs) == 1 { | ||
M internal/usecase/daemon/snapshot_restore.go +3 -2
| @@ -261,7 +261,8 @@ func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStable | |||
| 261 | 261 | } | |
| 262 | 262 | launch := d.shellLaunch(restoreEnv) | |
| 263 | 263 | lifetime := d.newPaneProcessLifetime(ctx, tabCtx) | |
| 264 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(restoreEnv, sessionName, tabStableID, paneStableID), paneSnap.Cwd, restorePTYSize(contentRect, tabSize)) | |
| 264 | + | ptyGeometry := domain.Geometry{Size: restorePTYSize(contentRect, tabSize)} | |
| 265 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(restoreEnv, sessionName, tabStableID, paneStableID), paneSnap.Cwd, ptyGeometry) | |
| 265 | 266 | if err != nil { | |
| 266 | 267 | lifetime.abort() | |
| 267 | 268 | if pty != nil { | |
| @@ -269,7 +270,7 @@ func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStable | |||
| 269 | 270 | } | |
| 270 | 271 | return nil, err | |
| 271 | 272 | } | |
| 272 | - | p := newPaneWithStableIDAndTitle(paneSnap.ID, paneStableID, pty, restorePTYSize(contentRect, tabSize), launch.title) | |
| 273 | + | p := newPaneWithStableIDAndTitle(paneSnap.ID, paneStableID, pty, ptyGeometry.Size, launch.title) | |
| 273 | 274 | if !lifetime.publish(p) { | |
| 274 | 275 | _ = pty.Close() | |
| 275 | 276 | return nil, lifetime.ctx.Err() | |
| @@ -261,7 +261,8 @@ func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStable | |||||
| 261 | } | 261 | } | ||
| 262 | launch := d.shellLaunch(restoreEnv) | 262 | launch := d.shellLaunch(restoreEnv) | ||
| 263 | lifetime := d.newPaneProcessLifetime(ctx, tabCtx) | 263 | lifetime := d.newPaneProcessLifetime(ctx, tabCtx) | ||
| 264 | - | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(restoreEnv, sessionName, tabStableID, paneStableID), paneSnap.Cwd, restorePTYSize(contentRect, tabSize)) | 264 | + | ptyGeometry := domain.Geometry{Size: restorePTYSize(contentRect, tabSize)} |
| 265 | + | pty, err := d.ptys.Open(lifetime.ctx, launch.command, launch.args, childEnvFrom(restoreEnv, sessionName, tabStableID, paneStableID), paneSnap.Cwd, ptyGeometry) | |||
| 265 | if err != nil { | 266 | if err != nil { | ||
| 266 | lifetime.abort() | 267 | lifetime.abort() | ||
| 267 | if pty != nil { | 268 | if pty != nil { | ||
| @@ -269,7 +270,7 @@ func (d *Daemon) restoreSnapshotPane(ctx context.Context, sessionName, tabStable | |||||
| 269 | } | 270 | } | ||
| 270 | return nil, err | 271 | return nil, err | ||
| 271 | } | 272 | } | ||
| 272 | - | p := newPaneWithStableIDAndTitle(paneSnap.ID, paneStableID, pty, restorePTYSize(contentRect, tabSize), launch.title) | 273 | + | p := newPaneWithStableIDAndTitle(paneSnap.ID, paneStableID, pty, ptyGeometry.Size, launch.title) |
| 273 | if !lifetime.publish(p) { | 274 | if !lifetime.publish(p) { | ||
| 274 | _ = pty.Close() | 275 | _ = pty.Close() | ||
| 275 | return nil, lifetime.ctx.Err() | 276 | return nil, lifetime.ctx.Err() | ||
M internal/usecase/daemon/snapshot_restore_test.go +1 -1
| @@ -347,7 +347,7 @@ func TestSnapshotRestoreUsesDaemonTerminalCapabilities(t *testing.T) { | |||
| 347 | 347 | var gotEnv []string | |
| 348 | 348 | factory := portsmocks.NewMockPTYFactory(t) | |
| 349 | 349 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). | |
| 350 | - | RunAndReturn(func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 350 | + | RunAndReturn(func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { | |
| 351 | 351 | gotEnv = append([]string(nil), env...) | |
| 352 | 352 | return pty, nil | |
| 353 | 353 | }).Once() | |
| @@ -347,7 +347,7 @@ func TestSnapshotRestoreUsesDaemonTerminalCapabilities(t *testing.T) { | |||||
| 347 | var gotEnv []string | 347 | var gotEnv []string | ||
| 348 | factory := portsmocks.NewMockPTYFactory(t) | 348 | factory := portsmocks.NewMockPTYFactory(t) | ||
| 349 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). | 349 | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). | ||
| 350 | - | RunAndReturn(func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 350 | + | RunAndReturn(func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Geometry) (ports.PTY, error) { |
| 351 | gotEnv = append([]string(nil), env...) | 351 | gotEnv = append([]string(nil), env...) | ||
| 352 | return pty, nil | 352 | return pty, nil | ||
| 353 | }).Once() | 353 | }).Once() | ||
M internal/usecase/daemon/transactional_resize.go +35 -6
| @@ -3,6 +3,7 @@ package daemon | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | ||
| 6 | + | vt "github.com/bnema/vev-vt" | |
| 6 | 7 | "github.com/bnema/vev/internal/domain" | |
| 7 | 8 | "github.com/bnema/vev/internal/ports" | |
| 8 | 9 | "github.com/bnema/vev/internal/usecase/layout" | |
| @@ -25,6 +26,7 @@ type resizeMember struct { | |||
| 25 | 26 | pane *pane | |
| 26 | 27 | owner paneEffectLease | |
| 27 | 28 | rect domain.Rect | |
| 29 | + | geometry domain.Geometry | |
| 28 | 30 | floating floatingGeometry | |
| 29 | 31 | isFloating bool | |
| 30 | 32 | // floatingGeneration identifies the exact popup slot accepted by a failed | |
| @@ -37,6 +39,20 @@ type resizeMember struct { | |||
| 37 | 39 | screenResized bool | |
| 38 | 40 | } | |
| 39 | 41 | ||
| 42 | + | func resizePTYGeometry(pty ports.PTY, geometry domain.Geometry) error { | |
| 43 | + | return pty.Resize(geometry.NormalizePixels()) | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func setScreenGeometry(screen *vt.Screen, geometry domain.Geometry) { | |
| 47 | + | if screen == nil { | |
| 48 | + | return | |
| 49 | + | } | |
| 50 | + | screen.SetGeometry(vt.Geometry{ | |
| 51 | + | Cols: geometry.Cols, Rows: geometry.Rows, | |
| 52 | + | PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight, | |
| 53 | + | }) | |
| 54 | + | } | |
| 55 | + | ||
| 40 | 56 | type preparedTabLayout struct { | |
| 41 | 57 | tab *tab | |
| 42 | 58 | generation uint64 | |
| @@ -71,7 +87,11 @@ func prepareTabLayoutForSizeLocked(sess *session, tb *tab, size domain.Size) pre | |||
| 71 | 87 | p.mu.Lock() | |
| 72 | 88 | owner := p.effectLeaseLocked() | |
| 73 | 89 | p.mu.Unlock() | |
| 74 | - | plan.members = append(plan.members, resizeMember{session: sess, tab: tb, pane: p, owner: owner, rect: placement.Content}) | |
| 90 | + | paneSize := rectSize(placement.Content) | |
| 91 | + | plan.members = append(plan.members, resizeMember{ | |
| 92 | + | session: sess, tab: tb, pane: p, owner: owner, rect: placement.Content, | |
| 93 | + | geometry: sess.paneGeometry(paneSize), | |
| 94 | + | }) | |
| 75 | 95 | } | |
| 76 | 96 | } | |
| 77 | 97 | return plan | |
| @@ -82,6 +102,9 @@ func prepareTabLayoutForSizeLocked(sess *session, tb *tab, size domain.Size) pre | |||
| 82 | 102 | func (d *Daemon) applyPreparedTabMembers(plan *preparedTabLayout) { | |
| 83 | 103 | for i := range plan.members { | |
| 84 | 104 | member := &plan.members[i] | |
| 105 | + | if !member.geometry.Valid() { | |
| 106 | + | member.geometry = domain.Geometry{Size: rectSize(member.rect)} | |
| 107 | + | } | |
| 85 | 108 | p := member.pane | |
| 86 | 109 | p.resizeMu.Lock() | |
| 87 | 110 | p.mu.Lock() | |
| @@ -98,14 +121,15 @@ func (d *Daemon) applyPreparedTabMembers(plan *preparedTabLayout) { | |||
| 98 | 121 | // Reapply even if the committed rectangle already matches: the PTY may | |
| 99 | 122 | // still be at that rejected attempt's external size. An accepted failed | |
| 100 | 123 | // attempt records resizeRetry for the same reason after its gate closes. | |
| 101 | - | needsApply := p.resizeApplying || p.resizeRetry || old.Width != member.rect.Width || old.Height != member.rect.Height | |
| 124 | + | pixelGeometryChanged := (p.geometry.PixelsKnown() || member.geometry.PixelsKnown()) && p.geometry != member.geometry | |
| 125 | + | needsApply := p.resizeApplying || p.resizeRetry || old.Width != member.rect.Width || old.Height != member.rect.Height || pixelGeometryChanged | |
| 102 | 126 | if needsApply && pty != nil { | |
| 103 | 127 | p.resizeApplying = true | |
| 104 | 128 | } | |
| 105 | 129 | p.mu.Unlock() | |
| 106 | 130 | if !needsApply || pty == nil { | |
| 107 | 131 | member.ok = true | |
| 108 | - | } else if err := pty.Resize(rectSize(member.rect)); err != nil { | |
| 132 | + | } else if err := resizePTYGeometry(pty, member.geometry); err != nil { | |
| 109 | 133 | d.log.Warn("pty resize failed", "err", err) | |
| 110 | 134 | p.mu.Lock() | |
| 111 | 135 | current := resizeMemberOwnerCurrent(member) | |
| @@ -184,7 +208,8 @@ func commitPreparedTabLayoutLocked(plan *preparedTabLayout) bool { | |||
| 184 | 208 | member.pane.rect = member.rect | |
| 185 | 209 | if member.ok { | |
| 186 | 210 | member.pane.resizeRetry = false | |
| 187 | - | member.pane.screen.Resize(member.rect.Width, member.rect.Height) | |
| 211 | + | member.pane.geometry = member.geometry | |
| 212 | + | setScreenGeometry(member.pane.screen, member.geometry) | |
| 188 | 213 | } | |
| 189 | 214 | member.pane.mu.Unlock() | |
| 190 | 215 | } | |
| @@ -460,8 +485,11 @@ func (d *Daemon) applyVisibleFloatingLayoutForMember(sess *session, tb *tab, cur | |||
| 460 | 485 | p.mu.Lock() | |
| 461 | 486 | owner := p.effectLeaseLocked() | |
| 462 | 487 | p.mu.Unlock() | |
| 488 | + | ptyRect := geometry.ptyRect() | |
| 463 | 489 | plan := preparedTabLayout{members: []resizeMember{{ | |
| 464 | - | session: sess, tab: tb, pane: p, owner: owner, rect: geometry.ptyRect(), floating: geometry, isFloating: true, floatingGeneration: generation, | |
| 490 | + | session: sess, tab: tb, pane: p, owner: owner, rect: ptyRect, | |
| 491 | + | geometry: sess.paneGeometry(rectSize(ptyRect)), floating: geometry, | |
| 492 | + | isFloating: true, floatingGeneration: generation, | |
| 465 | 493 | }}} | |
| 466 | 494 | d.applyPreparedTabMembers(&plan) | |
| 467 | 495 | ||
| @@ -477,9 +505,10 @@ func (d *Daemon) applyVisibleFloatingLayoutForMember(sess *session, tb *tab, cur | |||
| 477 | 505 | currentSlot = resizeMemberOwnerCurrent(&plan.members[0]) | |
| 478 | 506 | if currentSlot { | |
| 479 | 507 | p.rect = plan.members[0].rect | |
| 508 | + | p.geometry = plan.members[0].geometry | |
| 480 | 509 | p.popupGeometry = geometry | |
| 481 | 510 | p.resizeRetry = false | |
| 482 | - | p.screen.Resize(plan.members[0].rect.Width, plan.members[0].rect.Height) | |
| 511 | + | setScreenGeometry(p.screen, plan.members[0].geometry) | |
| 483 | 512 | } | |
| 484 | 513 | p.mu.Unlock() | |
| 485 | 514 | } | |
| @@ -3,6 +3,7 @@ package daemon | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | 5 | ||||
| 6 | + | vt "github.com/bnema/vev-vt" | |||
| 6 | "github.com/bnema/vev/internal/domain" | 7 | "github.com/bnema/vev/internal/domain" | ||
| 7 | "github.com/bnema/vev/internal/ports" | 8 | "github.com/bnema/vev/internal/ports" | ||
| 8 | "github.com/bnema/vev/internal/usecase/layout" | 9 | "github.com/bnema/vev/internal/usecase/layout" | ||
| @@ -25,6 +26,7 @@ type resizeMember struct { | |||||
| 25 | pane *pane | 26 | pane *pane | ||
| 26 | owner paneEffectLease | 27 | owner paneEffectLease | ||
| 27 | rect domain.Rect | 28 | rect domain.Rect | ||
| 29 | + | geometry domain.Geometry | |||
| 28 | floating floatingGeometry | 30 | floating floatingGeometry | ||
| 29 | isFloating bool | 31 | isFloating bool | ||
| 30 | // floatingGeneration identifies the exact popup slot accepted by a failed | 32 | // floatingGeneration identifies the exact popup slot accepted by a failed | ||
| @@ -37,6 +39,20 @@ type resizeMember struct { | |||||
| 37 | screenResized bool | 39 | screenResized bool | ||
| 38 | } | 40 | } | ||
| 39 | 41 | ||||
| 42 | + | func resizePTYGeometry(pty ports.PTY, geometry domain.Geometry) error { | |||
| 43 | + | return pty.Resize(geometry.NormalizePixels()) | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func setScreenGeometry(screen *vt.Screen, geometry domain.Geometry) { | |||
| 47 | + | if screen == nil { | |||
| 48 | + | return | |||
| 49 | + | } | |||
| 50 | + | screen.SetGeometry(vt.Geometry{ | |||
| 51 | + | Cols: geometry.Cols, Rows: geometry.Rows, | |||
| 52 | + | PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight, | |||
| 53 | + | }) | |||
| 54 | + | } | |||
| 55 | + | ||||
| 40 | type preparedTabLayout struct { | 56 | type preparedTabLayout struct { | ||
| 41 | tab *tab | 57 | tab *tab | ||
| 42 | generation uint64 | 58 | generation uint64 | ||
| @@ -71,7 +87,11 @@ func prepareTabLayoutForSizeLocked(sess *session, tb *tab, size domain.Size) pre | |||||
| 71 | p.mu.Lock() | 87 | p.mu.Lock() | ||
| 72 | owner := p.effectLeaseLocked() | 88 | owner := p.effectLeaseLocked() | ||
| 73 | p.mu.Unlock() | 89 | p.mu.Unlock() | ||
| 74 | - | plan.members = append(plan.members, resizeMember{session: sess, tab: tb, pane: p, owner: owner, rect: placement.Content}) | 90 | + | paneSize := rectSize(placement.Content) |
| 91 | + | plan.members = append(plan.members, resizeMember{ | |||
| 92 | + | session: sess, tab: tb, pane: p, owner: owner, rect: placement.Content, | |||
| 93 | + | geometry: sess.paneGeometry(paneSize), | |||
| 94 | + | }) | |||
| 75 | } | 95 | } | ||
| 76 | } | 96 | } | ||
| 77 | return plan | 97 | return plan | ||
| @@ -82,6 +102,9 @@ func prepareTabLayoutForSizeLocked(sess *session, tb *tab, size domain.Size) pre | |||||
| 82 | func (d *Daemon) applyPreparedTabMembers(plan *preparedTabLayout) { | 102 | func (d *Daemon) applyPreparedTabMembers(plan *preparedTabLayout) { | ||
| 83 | for i := range plan.members { | 103 | for i := range plan.members { | ||
| 84 | member := &plan.members[i] | 104 | member := &plan.members[i] | ||
| 105 | + | if !member.geometry.Valid() { | |||
| 106 | + | member.geometry = domain.Geometry{Size: rectSize(member.rect)} | |||
| 107 | + | } | |||
| 85 | p := member.pane | 108 | p := member.pane | ||
| 86 | p.resizeMu.Lock() | 109 | p.resizeMu.Lock() | ||
| 87 | p.mu.Lock() | 110 | p.mu.Lock() | ||
| @@ -98,14 +121,15 @@ func (d *Daemon) applyPreparedTabMembers(plan *preparedTabLayout) { | |||||
| 98 | // Reapply even if the committed rectangle already matches: the PTY may | 121 | // Reapply even if the committed rectangle already matches: the PTY may | ||
| 99 | // still be at that rejected attempt's external size. An accepted failed | 122 | // still be at that rejected attempt's external size. An accepted failed | ||
| 100 | // attempt records resizeRetry for the same reason after its gate closes. | 123 | // attempt records resizeRetry for the same reason after its gate closes. | ||
| 101 | - | needsApply := p.resizeApplying || p.resizeRetry || old.Width != member.rect.Width || old.Height != member.rect.Height | 124 | + | pixelGeometryChanged := (p.geometry.PixelsKnown() || member.geometry.PixelsKnown()) && p.geometry != member.geometry |
| 125 | + | needsApply := p.resizeApplying || p.resizeRetry || old.Width != member.rect.Width || old.Height != member.rect.Height || pixelGeometryChanged | |||
| 102 | if needsApply && pty != nil { | 126 | if needsApply && pty != nil { | ||
| 103 | p.resizeApplying = true | 127 | p.resizeApplying = true | ||
| 104 | } | 128 | } | ||
| 105 | p.mu.Unlock() | 129 | p.mu.Unlock() | ||
| 106 | if !needsApply || pty == nil { | 130 | if !needsApply || pty == nil { | ||
| 107 | member.ok = true | 131 | member.ok = true | ||
| 108 | - | } else if err := pty.Resize(rectSize(member.rect)); err != nil { | 132 | + | } else if err := resizePTYGeometry(pty, member.geometry); err != nil { |
| 109 | d.log.Warn("pty resize failed", "err", err) | 133 | d.log.Warn("pty resize failed", "err", err) | ||
| 110 | p.mu.Lock() | 134 | p.mu.Lock() | ||
| 111 | current := resizeMemberOwnerCurrent(member) | 135 | current := resizeMemberOwnerCurrent(member) | ||
| @@ -184,7 +208,8 @@ func commitPreparedTabLayoutLocked(plan *preparedTabLayout) bool { | |||||
| 184 | member.pane.rect = member.rect | 208 | member.pane.rect = member.rect | ||
| 185 | if member.ok { | 209 | if member.ok { | ||
| 186 | member.pane.resizeRetry = false | 210 | member.pane.resizeRetry = false | ||
| 187 | - | member.pane.screen.Resize(member.rect.Width, member.rect.Height) | 211 | + | member.pane.geometry = member.geometry |
| 212 | + | setScreenGeometry(member.pane.screen, member.geometry) | |||
| 188 | } | 213 | } | ||
| 189 | member.pane.mu.Unlock() | 214 | member.pane.mu.Unlock() | ||
| 190 | } | 215 | } | ||
| @@ -460,8 +485,11 @@ func (d *Daemon) applyVisibleFloatingLayoutForMember(sess *session, tb *tab, cur | |||||
| 460 | p.mu.Lock() | 485 | p.mu.Lock() | ||
| 461 | owner := p.effectLeaseLocked() | 486 | owner := p.effectLeaseLocked() | ||
| 462 | p.mu.Unlock() | 487 | p.mu.Unlock() | ||
| 488 | + | ptyRect := geometry.ptyRect() | |||
| 463 | plan := preparedTabLayout{members: []resizeMember{{ | 489 | plan := preparedTabLayout{members: []resizeMember{{ | ||
| 464 | - | session: sess, tab: tb, pane: p, owner: owner, rect: geometry.ptyRect(), floating: geometry, isFloating: true, floatingGeneration: generation, | 490 | + | session: sess, tab: tb, pane: p, owner: owner, rect: ptyRect, |
| 491 | + | geometry: sess.paneGeometry(rectSize(ptyRect)), floating: geometry, | |||
| 492 | + | isFloating: true, floatingGeneration: generation, | |||
| 465 | }}} | 493 | }}} | ||
| 466 | d.applyPreparedTabMembers(&plan) | 494 | d.applyPreparedTabMembers(&plan) | ||
| 467 | 495 | ||||
| @@ -477,9 +505,10 @@ func (d *Daemon) applyVisibleFloatingLayoutForMember(sess *session, tb *tab, cur | |||||
| 477 | currentSlot = resizeMemberOwnerCurrent(&plan.members[0]) | 505 | currentSlot = resizeMemberOwnerCurrent(&plan.members[0]) | ||
| 478 | if currentSlot { | 506 | if currentSlot { | ||
| 479 | p.rect = plan.members[0].rect | 507 | p.rect = plan.members[0].rect | ||
| 508 | + | p.geometry = plan.members[0].geometry | |||
| 480 | p.popupGeometry = geometry | 509 | p.popupGeometry = geometry | ||
| 481 | p.resizeRetry = false | 510 | p.resizeRetry = false | ||
| 482 | - | p.screen.Resize(plan.members[0].rect.Width, plan.members[0].rect.Height) | 511 | + | setScreenGeometry(p.screen, plan.members[0].geometry) |
| 483 | } | 512 | } | ||
| 484 | p.mu.Unlock() | 513 | p.mu.Unlock() | ||
| 485 | } | 514 | } | ||
M internal/usecase/daemon/transactional_resize_test.go +3 -3
| @@ -28,9 +28,9 @@ type transactionalResizePTY struct { | |||
| 28 | 28 | onWrite func([]byte) | |
| 29 | 29 | } | |
| 30 | 30 | ||
| 31 | - | func (p *transactionalResizePTY) Resize(size domain.Size) error { | |
| 31 | + | func (p *transactionalResizePTY) Resize(geometry domain.Geometry) error { | |
| 32 | 32 | p.mu.Lock() | |
| 33 | - | p.sizes = append(p.sizes, size) | |
| 33 | + | p.sizes = append(p.sizes, geometry.Size) | |
| 34 | 34 | n := len(p.sizes) | |
| 35 | 35 | hook := p.onResize | |
| 36 | 36 | var err error | |
| @@ -92,7 +92,7 @@ func newResizeReaderPTY(redraw []byte) *resizeReaderPTY { | |||
| 92 | 92 | return &resizeReaderPTY{redraw: redraw, reads: make(chan []byte), accepted: make(chan struct{}), closed: make(chan struct{})} | |
| 93 | 93 | } | |
| 94 | 94 | ||
| 95 | - | func (p *resizeReaderPTY) Resize(domain.Size) error { | |
| 95 | + | func (p *resizeReaderPTY) Resize(domain.Geometry) error { | |
| 96 | 96 | p.reads <- p.redraw | |
| 97 | 97 | <-p.accepted | |
| 98 | 98 | // A second, empty read cannot begin until ptyReader has processed the | |
| @@ -28,9 +28,9 @@ type transactionalResizePTY struct { | |||||
| 28 | onWrite func([]byte) | 28 | onWrite func([]byte) | ||
| 29 | } | 29 | } | ||
| 30 | 30 | ||||
| 31 | - | func (p *transactionalResizePTY) Resize(size domain.Size) error { | 31 | + | func (p *transactionalResizePTY) Resize(geometry domain.Geometry) error { |
| 32 | p.mu.Lock() | 32 | p.mu.Lock() | ||
| 33 | - | p.sizes = append(p.sizes, size) | 33 | + | p.sizes = append(p.sizes, geometry.Size) |
| 34 | n := len(p.sizes) | 34 | n := len(p.sizes) | ||
| 35 | hook := p.onResize | 35 | hook := p.onResize | ||
| 36 | var err error | 36 | var err error | ||
| @@ -92,7 +92,7 @@ func newResizeReaderPTY(redraw []byte) *resizeReaderPTY { | |||||
| 92 | return &resizeReaderPTY{redraw: redraw, reads: make(chan []byte), accepted: make(chan struct{}), closed: make(chan struct{})} | 92 | return &resizeReaderPTY{redraw: redraw, reads: make(chan []byte), accepted: make(chan struct{}), closed: make(chan struct{})} | ||
| 93 | } | 93 | } | ||
| 94 | 94 | ||||
| 95 | - | func (p *resizeReaderPTY) Resize(domain.Size) error { | 95 | + | func (p *resizeReaderPTY) Resize(domain.Geometry) error { |
| 96 | p.reads <- p.redraw | 96 | p.reads <- p.redraw | ||
| 97 | <-p.accepted | 97 | <-p.accepted | ||
| 98 | // A second, empty read cannot begin until ptyReader has processed the | 98 | // A second, empty read cannot begin until ptyReader has processed the | ||
M pkg/rawterm/rawterm.go +20 -6
| @@ -17,8 +17,9 @@ type State struct { | |||
| 17 | 17 | termios syscall.Termios | |
| 18 | 18 | } | |
| 19 | 19 | ||
| 20 | - | // winsize mirrors the kernel's struct winsize for TIOCGWINSZ/TIOCSWINSZ. | |
| 21 | - | type winsize struct { | |
| 20 | + | // Winsize mirrors the kernel's struct winsize for TIOCGWINSZ/TIOCSWINSZ. | |
| 21 | + | // Zero pixel dimensions mean the controlling terminal did not report them. | |
| 22 | + | type Winsize struct { | |
| 22 | 23 | Row uint16 | |
| 23 | 24 | Col uint16 | |
| 24 | 25 | Xpixel uint16 | |
| @@ -79,16 +80,29 @@ func Restore(fd int, s *State) error { | |||
| 79 | 80 | ||
| 80 | 81 | // GetSize returns the terminal's current column and row count. | |
| 81 | 82 | func GetSize(fd int) (cols, rows int, err error) { | |
| 82 | - | var ws winsize | |
| 83 | - | if err := ioctl(fd, syscall.TIOCGWINSZ, unsafe.Pointer(&ws)); err != nil { | |
| 84 | - | return 0, 0, fmt.Errorf("rawterm: get winsize: %w", err) | |
| 83 | + | ws, err := GetWinsize(fd) | |
| 84 | + | if err != nil { | |
| 85 | + | return 0, 0, err | |
| 85 | 86 | } | |
| 86 | 87 | return int(ws.Col), int(ws.Row), nil | |
| 87 | 88 | } | |
| 88 | 89 | ||
| 90 | + | // GetWinsize returns the terminal's cell and optional pixel dimensions. | |
| 91 | + | func GetWinsize(fd int) (Winsize, error) { | |
| 92 | + | var ws Winsize | |
| 93 | + | if err := ioctl(fd, syscall.TIOCGWINSZ, unsafe.Pointer(&ws)); err != nil { | |
| 94 | + | return Winsize{}, fmt.Errorf("rawterm: get winsize: %w", err) | |
| 95 | + | } | |
| 96 | + | return ws, nil | |
| 97 | + | } | |
| 98 | + | ||
| 89 | 99 | // SetWinsize sets the terminal's column and row count. | |
| 90 | 100 | func SetWinsize(fd int, cols, rows uint16) error { | |
| 91 | - | ws := winsize{Row: rows, Col: cols} | |
| 101 | + | return SetWinsizeFull(fd, Winsize{Row: rows, Col: cols}) | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // SetWinsizeFull sets the terminal's cell and optional pixel dimensions. | |
| 105 | + | func SetWinsizeFull(fd int, ws Winsize) error { | |
| 92 | 106 | if err := ioctl(fd, syscall.TIOCSWINSZ, unsafe.Pointer(&ws)); err != nil { | |
| 93 | 107 | return fmt.Errorf("rawterm: set winsize: %w", err) | |
| 94 | 108 | } | |
| @@ -17,8 +17,9 @@ type State struct { | |||||
| 17 | termios syscall.Termios | 17 | termios syscall.Termios | ||
| 18 | } | 18 | } | ||
| 19 | 19 | ||||
| 20 | - | // winsize mirrors the kernel's struct winsize for TIOCGWINSZ/TIOCSWINSZ. | 20 | + | // Winsize mirrors the kernel's struct winsize for TIOCGWINSZ/TIOCSWINSZ. |
| 21 | - | type winsize struct { | 21 | + | // Zero pixel dimensions mean the controlling terminal did not report them. |
| 22 | + | type Winsize struct { | |||
| 22 | Row uint16 | 23 | Row uint16 | ||
| 23 | Col uint16 | 24 | Col uint16 | ||
| 24 | Xpixel uint16 | 25 | Xpixel uint16 | ||
| @@ -79,16 +80,29 @@ func Restore(fd int, s *State) error { | |||||
| 79 | 80 | ||||
| 80 | // GetSize returns the terminal's current column and row count. | 81 | // GetSize returns the terminal's current column and row count. | ||
| 81 | func GetSize(fd int) (cols, rows int, err error) { | 82 | func GetSize(fd int) (cols, rows int, err error) { | ||
| 82 | - | var ws winsize | 83 | + | ws, err := GetWinsize(fd) |
| 83 | - | if err := ioctl(fd, syscall.TIOCGWINSZ, unsafe.Pointer(&ws)); err != nil { | 84 | + | if err != nil { |
| 84 | - | return 0, 0, fmt.Errorf("rawterm: get winsize: %w", err) | 85 | + | return 0, 0, err |
| 85 | } | 86 | } | ||
| 86 | return int(ws.Col), int(ws.Row), nil | 87 | return int(ws.Col), int(ws.Row), nil | ||
| 87 | } | 88 | } | ||
| 88 | 89 | ||||
| 90 | + | // GetWinsize returns the terminal's cell and optional pixel dimensions. | |||
| 91 | + | func GetWinsize(fd int) (Winsize, error) { | |||
| 92 | + | var ws Winsize | |||
| 93 | + | if err := ioctl(fd, syscall.TIOCGWINSZ, unsafe.Pointer(&ws)); err != nil { | |||
| 94 | + | return Winsize{}, fmt.Errorf("rawterm: get winsize: %w", err) | |||
| 95 | + | } | |||
| 96 | + | return ws, nil | |||
| 97 | + | } | |||
| 98 | + | ||||
| 89 | // SetWinsize sets the terminal's column and row count. | 99 | // SetWinsize sets the terminal's column and row count. | ||
| 90 | func SetWinsize(fd int, cols, rows uint16) error { | 100 | func SetWinsize(fd int, cols, rows uint16) error { | ||
| 91 | - | ws := winsize{Row: rows, Col: cols} | 101 | + | return SetWinsizeFull(fd, Winsize{Row: rows, Col: cols}) |
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | // SetWinsizeFull sets the terminal's cell and optional pixel dimensions. | |||
| 105 | + | func SetWinsizeFull(fd int, ws Winsize) error { | |||
| 92 | if err := ioctl(fd, syscall.TIOCSWINSZ, unsafe.Pointer(&ws)); err != nil { | 106 | if err := ioctl(fd, syscall.TIOCSWINSZ, unsafe.Pointer(&ws)); err != nil { | ||
| 93 | return fmt.Errorf("rawterm: set winsize: %w", err) | 107 | return fmt.Errorf("rawterm: set winsize: %w", err) | ||
| 94 | } | 108 | } | ||
M pkg/rawterm/rawterm_test.go +18 -0
| @@ -83,6 +83,24 @@ func TestSetWinsizeGetSizeRoundtrip(t *testing.T) { | |||
| 83 | 83 | } | |
| 84 | 84 | } | |
| 85 | 85 | ||
| 86 | + | func TestWinsizeFullRoundtrip(t *testing.T) { | |
| 87 | + | master, slave := openPTYPair(t) | |
| 88 | + | defer func() { _ = master.Close() }() | |
| 89 | + | defer func() { _ = slave.Close() }() | |
| 90 | + | ||
| 91 | + | want := Winsize{Col: 80, Row: 24, Xpixel: 640, Ypixel: 384} | |
| 92 | + | if err := SetWinsizeFull(int(slave.Fd()), want); err != nil { | |
| 93 | + | t.Fatalf("SetWinsizeFull: %v", err) | |
| 94 | + | } | |
| 95 | + | got, err := GetWinsize(int(slave.Fd())) | |
| 96 | + | if err != nil { | |
| 97 | + | t.Fatalf("GetWinsize: %v", err) | |
| 98 | + | } | |
| 99 | + | if got != want { | |
| 100 | + | t.Fatalf("GetWinsize = %+v, want %+v", got, want) | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | ||
| 86 | 104 | func TestMakeRawRestore(t *testing.T) { | |
| 87 | 105 | master, slave := openPTYPair(t) | |
| 88 | 106 | defer func() { _ = master.Close() }() | |
| @@ -83,6 +83,24 @@ func TestSetWinsizeGetSizeRoundtrip(t *testing.T) { | |||||
| 83 | } | 83 | } | ||
| 84 | } | 84 | } | ||
| 85 | 85 | ||||
| 86 | + | func TestWinsizeFullRoundtrip(t *testing.T) { | |||
| 87 | + | master, slave := openPTYPair(t) | |||
| 88 | + | defer func() { _ = master.Close() }() | |||
| 89 | + | defer func() { _ = slave.Close() }() | |||
| 90 | + | ||||
| 91 | + | want := Winsize{Col: 80, Row: 24, Xpixel: 640, Ypixel: 384} | |||
| 92 | + | if err := SetWinsizeFull(int(slave.Fd()), want); err != nil { | |||
| 93 | + | t.Fatalf("SetWinsizeFull: %v", err) | |||
| 94 | + | } | |||
| 95 | + | got, err := GetWinsize(int(slave.Fd())) | |||
| 96 | + | if err != nil { | |||
| 97 | + | t.Fatalf("GetWinsize: %v", err) | |||
| 98 | + | } | |||
| 99 | + | if got != want { | |||
| 100 | + | t.Fatalf("GetWinsize = %+v, want %+v", got, want) | |||
| 101 | + | } | |||
| 102 | + | } | |||
| 103 | + | ||||
| 86 | func TestMakeRawRestore(t *testing.T) { | 104 | func TestMakeRawRestore(t *testing.T) { | ||
| 87 | master, slave := openPTYPair(t) | 105 | master, slave := openPTYPair(t) | ||
| 88 | defer func() { _ = master.Close() }() | 106 | defer func() { _ = master.Close() }() | ||