1package daemon
2
3import (
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/bnema/vev/internal/protocol"
14 "github.com/stretchr/testify/require"
15)
16
17func TestSessionPaneGeometryUsesLatestAttachmentClaimAndTruncates(t *testing.T) {
18 sess := &session{}
19 first := &attachedClient{}
20 first.setGeometry(domain.Geometry{
21 Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480,
22 })
23 second := &attachedClient{}
24 second.setGeometry(domain.Geometry{
25 Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 803, PixelHeight: 487,
26 })
27
28 require.True(t, sess.registerAttachment(first))
29 require.True(t, sess.registerAttachment(second))
30 require.Equal(t, domain.Geometry{
31 Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 391, PixelHeight: 466,
32 }, sess.geometry.paneGeometry(domain.Size{Cols: 39, Rows: 23}))
33
34 require.True(t, sess.unregisterAttachment(second))
35 require.Equal(t, domain.Geometry{
36 Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 390, PixelHeight: 460,
37 }, sess.geometry.paneGeometry(domain.Size{Cols: 39, Rows: 23}))
38}
39
40func TestPreparedLayoutPropagatesGeometryToPTYAndScreenQueries(t *testing.T) {
41 geometry := domain.Geometry{
42 Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 391, PixelHeight: 466,
43 }
44 pty := &recordingGeometryPTY{}
45 pane := &pane{pty: pty, screen: vt.NewScreen(1, 1), rect: domain.Rect{Width: 1, Height: 1}}
46 plan := preparedTabLayout{members: []resizeMember{{pane: pane, rect: domain.Rect{Width: 39, Height: 23}, geometry: geometry}}}
47 daemon := &Daemon{log: slog.New(slog.NewTextHandler(io.Discard, nil))}
48
49 geometryModule := &sharedPTYGeometry{}
50 geometryModule.applyPreparedTabMembers(daemon, &plan)
51 require.True(t, commitPreparedTabLayoutLocked(&plan))
52 require.Equal(t, geometry, pty.geometry)
53 require.Equal(t, vt.Geometry{
54 Cols: 39, Rows: 23, PixelWidth: 391, PixelHeight: 466,
55 }, pane.screen.Geometry())
56
57 var responses string
58 pane.screen.OnResponse = func(response []byte) { responses += string(response) }
59 pane.screen.Write([]byte("\x1b[14t\x1b[16t"))
60 require.Equal(t, "\x1b[4;466;391t\x1b[6;20;10t", responses)
61}
62
63type recordingGeometryPTY struct {
64 geometry domain.Geometry
65}
66
67func (p *recordingGeometryPTY) Read([]byte) (int, error) { return 0, io.EOF }
68func (p *recordingGeometryPTY) Write(b []byte) (int, error) { return len(b), nil }
69func (p *recordingGeometryPTY) Close() error { return nil }
70func (p *recordingGeometryPTY) Resize(geometry domain.Geometry) error {
71 p.geometry = geometry
72 return nil
73}
74func (p *recordingGeometryPTY) Pid() int { return 1 }
75func (p *recordingGeometryPTY) ForegroundPgid() (int, error) { return 1, nil }
76
77func TestSizeOnlyGeometryClaimsPreservePixelsAndFallback(t *testing.T) {
78 sess := &session{}
79 first := &attachedClient{}
80 first.setGeometry(domain.Geometry{
81 Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480,
82 })
83 second := &attachedClient{}
84 second.setGeometry(domain.Geometry{
85 Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 900, PixelHeight: 540,
86 })
87 require.True(t, sess.registerAttachment(first))
88 require.True(t, sess.registerAttachment(second))
89
90 _, claimed := sess.geometry.claimSize(sess, second, domain.Size{Cols: 80, Rows: 24})
91 require.True(t, claimed)
92 require.Equal(t, domain.Geometry{
93 Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 438, PixelHeight: 517,
94 }, sess.geometry.paneGeometry(domain.Size{Cols: 39, Rows: 23}))
95
96 require.True(t, sess.unregisterAttachment(second))
97 require.Equal(t, domain.Geometry{
98 Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 390, PixelHeight: 460,
99 }, sess.geometry.paneGeometry(domain.Size{Cols: 39, Rows: 23}))
100
101 partial := &attachedClient{}
102 partial.setGeometry(domain.Geometry{
103 Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 901,
104 })
105 require.True(t, sess.registerAttachment(partial))
106 require.Equal(t, domain.Geometry{Size: domain.Size{Cols: 39, Rows: 23}}, sess.geometry.paneGeometry(domain.Size{Cols: 39, Rows: 23}), "partial pixels must remain cell-only")
107 require.True(t, sess.unregisterAttachment(partial))
108 require.Equal(t, domain.Geometry{
109 Size: domain.Size{Cols: 39, Rows: 23}, PixelWidth: 390, PixelHeight: 460,
110 }, sess.geometry.paneGeometry(domain.Size{Cols: 39, Rows: 23}), "fallback must restore the remaining complete pair")
111}
112
113func TestSizeOnlyLayoutAndFloatingRetainClaimingPixelGeometry(t *testing.T) {
114 full := domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480}
115 ac := &attachedClient{}
116 ac.setGeometry(full)
117 sess := &session{sessionCore: sessionCore{id: "geometry"}}
118 require.True(t, sess.registerAttachment(ac))
119
120 layoutPTY := &recordingGeometryPTY{}
121 tb := newTab(layoutPTY, domain.Size{Cols: 80, Rows: 22})
122 sess.tabs = []*tab{tb}
123 d := newTestDaemon(t, nil, stubClock{})
124 require.True(t, sess.geometry.applyTabLayout(d, sess, tb))
125 require.Equal(t, domain.Geometry{Size: domain.Size{Cols: 80, Rows: 22}, PixelWidth: 800, PixelHeight: 440}, layoutPTY.geometry)
126
127 floatingPTY := &recordingGeometryPTY{}
128 cfg := domain.FloatingConfig{Width: 50, Height: 50}
129 d.ApplyConfig(domain.Config{Floating: cfg})
130 floatingGeometry := calculateContentFloatingGeometry(tb.size, cfg)
131 floating := newPane("floating", floatingPTY, rectSize(floatingGeometry.Inner))
132 floating.popupGeometry = floatingGeometry
133 tb.floating = floatingSlot{state: floatingVisible, pane: floating, generation: 1}
134 failed, ok := sess.geometry.applyVisibleFloatingLayout(d, sess, tb, nil)
135 require.True(t, ok)
136 require.Empty(t, failed)
137 ptyRect := floatingGeometry.ptyRect()
138 require.Equal(t, domain.Geometry{
139 Size: domain.Size{Cols: ptyRect.Width, Rows: ptyRect.Height},
140 PixelWidth: 800 * ptyRect.Width / 80, PixelHeight: 480 * ptyRect.Height / 24,
141 }, floatingPTY.geometry)
142}
143
144type recordingPTYFactory struct {
145 pty ports.PTY
146 geometry domain.Geometry
147}
148
149func (f *recordingPTYFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, geometry domain.Geometry) (ports.PTY, error) {
150 f.geometry = geometry
151 return f.pty, nil
152}
153
154type blockingRecordingGeometryPTY struct {
155 mu sync.Mutex
156 geometry domain.Geometry
157 done chan struct{}
158 once sync.Once
159}
160
161func (p *blockingRecordingGeometryPTY) Read([]byte) (int, error) {
162 <-p.done
163 return 0, io.EOF
164}
165func (p *blockingRecordingGeometryPTY) Write(b []byte) (int, error) { return len(b), nil }
166func (p *blockingRecordingGeometryPTY) Close() error {
167 p.once.Do(func() { close(p.done) })
168 return nil
169}
170func (p *blockingRecordingGeometryPTY) Resize(geometry domain.Geometry) error {
171 p.mu.Lock()
172 p.geometry = geometry
173 p.mu.Unlock()
174 return nil
175}
176func (p *blockingRecordingGeometryPTY) geometrySnapshot() domain.Geometry {
177 p.mu.Lock()
178 defer p.mu.Unlock()
179 return p.geometry
180}
181func (*blockingRecordingGeometryPTY) Pid() int { return 1 }
182func (*blockingRecordingGeometryPTY) ForegroundPgid() (int, error) { return 1, nil }
183
184func TestInitialSessionPTYReceivesClaimingPixelGeometry(t *testing.T) {
185 pty := &recordingGeometryPTY{}
186 factory := &recordingPTYFactory{pty: pty}
187 d := newTestDaemon(t, factory, stubClock{})
188 geometry := domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480}
189
190 d.mu.Lock()
191 sess, err := d.createSessionLockedWithMode("pixels", true, "/tmp", geometry, nil)
192 d.mu.Unlock()
193 require.NoError(t, err)
194 want := domain.Geometry{Size: domain.Size{Cols: 80, Rows: 22}, PixelWidth: 800, PixelHeight: 440}
195 require.Equal(t, want, factory.geometry)
196 require.Equal(t, want, sess.tabs[0].focusedPane().geometry)
197 require.Equal(t, vt.Geometry{Cols: 80, Rows: 22, PixelWidth: 800, PixelHeight: 440}, sess.tabs[0].focusedPane().screen.Geometry())
198}
199
200func TestResumeReappliesClaimingPixelGeometryBeforeReturn(t *testing.T) {
201 pty := &blockingRecordingGeometryPTY{done: make(chan struct{})}
202 t.Cleanup(func() { _ = pty.Close() })
203 d := newTestDaemon(t, &recordingPTYFactory{pty: pty}, stubClock{})
204 firstHello := helloResumeCapable(protocol.IntentNew, "pixels", 0)
205 firstHello.PixelWidth, firstHello.PixelHeight = 800, 480
206 firstTransport := &closeTrackingTransport{}
207 sess, ac, err := d.route(firstHello, firstTransport)
208 require.NoError(t, err)
209 d.clientGone(sess, ac, firstTransport, false)
210
211 resumeHello := helloResumeCapable(protocol.IntentResume, "pixels", ac.resumeToken)
212 resumeHello.PixelWidth, resumeHello.PixelHeight = 1000, 600
213 resumed, same, ok, err := d.resumeParked(resumeHello, &closeTrackingTransport{}, resumeHello.Size)
214 require.NoError(t, err)
215 require.True(t, ok)
216 require.Same(t, sess, resumed)
217 require.Same(t, ac, same)
218 require.Equal(t, domain.Geometry{Size: domain.Size{Cols: 80, Rows: 22}, PixelWidth: 1000, PixelHeight: 550}, pty.geometrySnapshot())
219}