add per-pane display ownership

This commit is contained in:
2026-05-27 14:30:47 +01:00
parent 63cb8a4388
commit 6d15626e05
9 changed files with 333 additions and 36 deletions

View File

@@ -11,6 +11,7 @@ import (
"testing"
"time"
"github.com/hjbdev/patterm/internal/preset"
"github.com/hjbdev/patterm/internal/protocol"
)
@@ -160,6 +161,76 @@ func TestDaemonTCPTokenAuthAndUnixExemption(t *testing.T) {
}
}
func TestDaemonPaneDisplayOwnerSizing(t *testing.T) {
t.Setenv("XDG_DATA_HOME", t.TempDir())
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
projectDir := t.TempDir()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reg := newProjectRegistry(preset.Set{}, defaultSettings(), nil, 80, 24)
defer reg.Shutdown()
project, err := reg.Open(ctx, projectDir)
if err != nil {
t.Fatalf("open project: %v", err)
}
client1, daemon1 := protocol.NewLoopbackPair()
go handleDaemonConn(ctx, cancel, reg, daemon1, "")
sendFrame(t, client1, protocol.FrameAttach, protocol.Attach{
ProjectPath: projectDir,
TermSize: protocol.Size{Cols: 80, Rows: 24},
})
expectFrame(t, client1, protocol.FrameHello)
expectFrame(t, client1, protocol.FrameProjectList)
expectFrame(t, client1, protocol.FrameChrome)
data, _ := json.Marshal(map[string]any{
"argv": []string{"sh", "-c", "trap 'exit 0' TERM; while :; do sleep 1; done"},
"name": "owner-pane",
})
sendFrame(t, client1, protocol.FramePaletteCommand, protocol.PaletteCommand{
Kind: "spawn_command",
Data: data,
})
paneID := waitForLifecycleID(t, client1, protocol.LifecycleSpawned, 3*time.Second)
snap1 := waitForSnapshot(t, client1, paneID, 3*time.Second)
if !snap1.DisplayOwner || snap1.Size != (protocol.Size{Cols: 80, Rows: 24}) {
t.Fatalf("owner snapshot = owner:%v size:%+v, want owner true size 80x24", snap1.DisplayOwner, snap1.Size)
}
waitForEmulatorSize(t, project, paneID, 80, 24)
client2, daemon2 := protocol.NewLoopbackPair()
go handleDaemonConn(ctx, cancel, reg, daemon2, "")
sendFrame(t, client2, protocol.FrameAttach, protocol.Attach{
ProjectPath: projectDir,
TermSize: protocol.Size{Cols: 100, Rows: 30},
})
expectFrame(t, client2, protocol.FrameHello)
expectFrame(t, client2, protocol.FrameProjectList)
expectFrame(t, client2, protocol.FrameChrome)
snap2 := waitForSnapshot(t, client2, paneID, 3*time.Second)
if snap2.DisplayOwner || snap2.Size != (protocol.Size{Cols: 80, Rows: 24}) {
t.Fatalf("viewer snapshot = owner:%v size:%+v, want owner false size 80x24", snap2.DisplayOwner, snap2.Size)
}
sendFrame(t, client2, protocol.FrameResize, protocol.Resize{Size: protocol.Size{Cols: 100, Rows: 30}})
time.Sleep(100 * time.Millisecond)
waitForEmulatorSize(t, project, paneID, 80, 24)
sendFrame(t, client1, protocol.FrameDetach, protocol.Detach{})
_ = client1.Close()
time.Sleep(100 * time.Millisecond)
sendFrame(t, client2, protocol.FrameFocus, protocol.Focus{PaneID: paneID})
snap3 := waitForSnapshot(t, client2, paneID, 3*time.Second)
if !snap3.DisplayOwner || snap3.Size != (protocol.Size{Cols: 100, Rows: 30}) {
t.Fatalf("claimed snapshot = owner:%v size:%+v, want owner true size 100x30", snap3.DisplayOwner, snap3.Size)
}
waitForEmulatorSize(t, project, paneID, 100, 30)
sendFrame(t, client2, protocol.FrameDetach, protocol.Detach{})
_ = client2.Close()
}
func waitForSocket(t *testing.T, socket string, errCh <-chan error) {
t.Helper()
deadline := time.Now().Add(3 * time.Second)
@@ -297,6 +368,81 @@ func waitForLifecycle(t *testing.T, tr protocol.Transport, kind protocol.Lifecyc
t.Fatalf("lifecycle %s not received", kind)
}
func waitForLifecycleID(t *testing.T, tr protocol.Transport, kind protocol.LifecycleKind, timeout time.Duration) string {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
f, err, ok := recvFrameWithin(tr, time.Until(deadline))
if !ok {
break
}
if err != nil {
t.Fatalf("recv lifecycle: %v", err)
}
if f.Type != protocol.FrameLifecycle {
continue
}
msg, err := protocol.Decode[protocol.Lifecycle](f)
if err != nil {
t.Fatalf("decode lifecycle: %v", err)
}
if msg.Kind == kind {
return msg.ChildID
}
}
t.Fatalf("lifecycle %s not received", kind)
return ""
}
func waitForSnapshot(t *testing.T, tr protocol.Transport, paneID string, timeout time.Duration) protocol.PaneSnapshot {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
f, err, ok := recvFrameWithin(tr, time.Until(deadline))
if !ok {
break
}
if err != nil {
t.Fatalf("recv snapshot: %v", err)
}
if f.Type != protocol.FramePaneSnapshot {
continue
}
msg, err := protocol.Decode[protocol.PaneSnapshot](f)
if err != nil {
t.Fatalf("decode snapshot: %v", err)
}
if msg.PaneID == paneID {
return msg
}
}
t.Fatalf("snapshot for %s not received", paneID)
return protocol.PaneSnapshot{}
}
func waitForEmulatorSize(t *testing.T, project *Project, paneID string, cols, rows uint16) {
t.Helper()
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
if c := project.Session.FindChild(paneID); c != nil {
if em := c.Emulator(); em != nil {
gotCols, gotRows := em.Size()
if gotCols == cols && gotRows == rows {
return
}
}
}
time.Sleep(25 * time.Millisecond)
}
if c := project.Session.FindChild(paneID); c != nil {
if em := c.Emulator(); em != nil {
gotCols, gotRows := em.Size()
t.Fatalf("emulator size = %dx%d, want %dx%d", gotCols, gotRows, cols, rows)
}
}
t.Fatalf("pane %s missing emulator", paneID)
}
func recvFrameWithin(tr protocol.Transport, timeout time.Duration) (protocol.Frame, error, bool) {
type result struct {
f protocol.Frame