geometry_propagation_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close