session_geometry.go RawHistoryFolder

1package daemon
2
3import (
4	"sync"
5	"sync/atomic"
6
7	"github.com/bnema/vev/internal/domain"
8)
9
10// sharedPTYClaim is one immutable attach, resume, or resize claim for shared
11// Session PTY geometry. A Session retains only the latest claim per registered
12// Attachment; historical claims from the same Attachment are never fallback
13// candidates.
14type sharedPTYClaim struct {
15	attachment *attachedClient
16	geometry   domain.Geometry
17	sequence   uint64
18}
19
20// sharedPTYGeometry is the concrete Session Module for shared PTY geometry.
21// It owns claim publication, latest-valid selection, and accepted geometry.
22// Attachment, render-coordinator, Tab, Pane, floating, and Move state retain
23// their independent owners and are consumed through their existing fences.
24type sharedPTYGeometry struct {
25	mu       sync.Mutex
26	latest   atomic.Pointer[sharedPTYClaim]
27	claims   map[*attachedClient]*sharedPTYClaim // session.mu -> mu
28	sequence uint64                              // session.mu -> mu
29	applied  domain.Geometry                     // mu
30}
31
32// claimOutranks reports whether claim a wins over b. A claim from an
33// attachment whose terminal reported losing focus never wins over one that may
34// be seen, so a background window cannot take geometry from the focused one.
35// Otherwise the newest claim wins, as when no terminal reports focus.
36func claimOutranks(a *sharedPTYClaim, aOrder uint64, b *sharedPTYClaim, bOrder uint64) bool {
37	if b == nil {
38		return true
39	}
40	aSeen, bSeen := a.attachment.terminalFocus().MaySee(), b.attachment.terminalFocus().MaySee()
41	if aSeen != bSeen {
42		return aSeen
43	}
44	return a.sequence > b.sequence || (a.sequence == b.sequence && aOrder > bOrder)
45}
46
47// latestValidClaimLocked selects the winning valid remaining Attachment claim
48// (see claimOutranks). The caller holds both session.mu and g.mu.
49func (g *sharedPTYGeometry) latestValidClaimLocked(core *sessionCore, exclude *attachedClient) *sharedPTYClaim {
50	if g == nil || core == nil {
51		return nil
52	}
53	var latest *sharedPTYClaim
54	var latestOrder uint64
55	for attachment := range core.attachments {
56		if attachment == nil || attachment == exclude || !attachment.geometrySnapshot().Valid() {
57			continue
58		}
59		claim := g.claims[attachment]
60		if claim == nil || !claim.geometry.Valid() {
61			continue
62		}
63		order := core.attachmentOrder[attachment]
64		if claimOutranks(claim, order, latest, latestOrder) {
65			latest = claim
66			latestOrder = order
67		}
68	}
69	return latest
70}
71
72// refreshLocked repairs an invalid publication without creating a newer
73// claim. The caller holds session.mu.
74func (g *sharedPTYGeometry) refreshLocked(core *sessionCore) {
75	if g == nil || core == nil {
76		return
77	}
78	g.mu.Lock()
79	defer g.mu.Unlock()
80	latest := g.latest.Load()
81	if latest != nil && latest.attachment != nil && g.claims[latest.attachment] == latest &&
82		latest.geometry.Valid() && latest.attachment.geometrySnapshot().Valid() {
83		if _, registered := core.attachments[latest.attachment]; registered {
84			return
85		}
86	}
87	g.latest.Store(g.latestValidClaimLocked(core, nil))
88}
89
90// claimLocked records a new immutable claim and publishes the winning claim,
91// which is the new one unless an unfocused attachment made it while another
92// may be seen. The caller holds session.mu; claim publication then follows the
93// session.mu -> geometry.mu order.
94func (g *sharedPTYGeometry) claimLocked(core *sessionCore, attachment *attachedClient, geometry domain.Geometry) *sharedPTYClaim {
95	if g == nil || core == nil || attachment == nil || !geometry.Valid() {
96		return nil
97	}
98	g.mu.Lock()
99	defer g.mu.Unlock()
100	if g.claims == nil {
101		g.claims = make(map[*attachedClient]*sharedPTYClaim)
102	}
103	g.sequence++
104	claim := &sharedPTYClaim{
105		attachment: attachment,
106		geometry:   geometry.NormalizePixels(),
107		sequence:   g.sequence,
108	}
109	g.claims[attachment] = claim
110	if !g.currentHidesLocked(core, claim) {
111		g.latest.Store(claim)
112	}
113	return claim
114}
115
116// currentHidesLocked reports whether the published claim must stay ahead of a
117// newer claim: the newer one comes from an unfocused window while the current
118// one is live and may be seen. The caller holds session.mu and g.mu.
119func (g *sharedPTYGeometry) currentHidesLocked(core *sessionCore, claim *sharedPTYClaim) bool {
120	current := g.latest.Load()
121	if current == nil || current.attachment == claim.attachment {
122		return false
123	}
124	if g.claims[current.attachment] != current {
125		return false // the current claim was superseded or released
126	}
127	if _, registered := core.attachments[current.attachment]; !registered {
128		return false
129	}
130	return !claim.attachment.terminalFocus().MaySee() && current.attachment.terminalFocus().MaySee()
131}
132
133// removeAttachmentLocked removes the exact Attachment claim and republishes
134// the most recent valid remaining claimant. The caller holds session.mu.
135func (g *sharedPTYGeometry) removeAttachmentLocked(core *sessionCore, attachment *attachedClient) {
136	if g == nil || core == nil || attachment == nil {
137		return
138	}
139	g.mu.Lock()
140	defer g.mu.Unlock()
141	delete(g.claims, attachment)
142	latest := g.latest.Load()
143	if latest == nil || latest.attachment == attachment {
144		g.latest.Store(g.latestValidClaimLocked(core, attachment))
145	}
146}
147
148func (g *sharedPTYGeometry) claimAttachment(sess *session, attachment *attachedClient) (*sharedPTYClaim, bool) {
149	if attachment == nil {
150		return nil, false
151	}
152	return g.claimGeometry(sess, attachment, attachment.geometrySnapshot())
153}
154
155func (g *sharedPTYGeometry) claimSize(sess *session, attachment *attachedClient, size domain.Size) (*sharedPTYClaim, bool) {
156	if attachment == nil {
157		return nil, false
158	}
159	// Layout and floating requests carry only shared cells. Preserve the
160	// claimant's complete terminal pixel pair for pane mapping and fallback.
161	geometry := attachment.geometrySnapshot()
162	geometry.Size = size
163	return g.claimGeometry(sess, attachment, geometry)
164}
165
166func (g *sharedPTYGeometry) claimGeometry(sess *session, attachment *attachedClient, geometry domain.Geometry) (*sharedPTYClaim, bool) {
167	if g == nil || sess == nil || attachment == nil || !geometry.Valid() {
168		return nil, false
169	}
170	sess.mu.Lock()
171	defer sess.mu.Unlock()
172	if _, registered := sess.attachments[attachment]; !registered {
173		return nil, false
174	}
175	claim := g.claimLocked(sess.core(), attachment, geometry)
176	return claim, claim != nil
177}
178
179// release removes a claim that did not reach coordinator admission. It removes
180// only the exact current claim and never restores an older claim from the same
181// Attachment.
182func (g *sharedPTYGeometry) release(sess *session, claim *sharedPTYClaim) {
183	if g == nil || sess == nil || claim == nil || claim.attachment == nil {
184		return
185	}
186	sess.mu.Lock()
187	defer sess.mu.Unlock()
188	g.mu.Lock()
189	defer g.mu.Unlock()
190	if g.latest.Load() != claim || g.claims[claim.attachment] != claim {
191		return
192	}
193	delete(g.claims, claim.attachment)
194	g.latest.Store(g.latestValidClaimLocked(sess.core(), claim.attachment))
195}
196
197// sourceSnapshot returns the latest live immutable claim. Refresh takes
198// session.mu only before Tab locks are involved; final transaction validation
199// uses current and remains lock-free.
200func (g *sharedPTYGeometry) sourceSnapshot(sess *session) (*sharedPTYClaim, bool) {
201	if g == nil || sess == nil {
202		return nil, false
203	}
204	sess.mu.Lock()
205	g.refreshLocked(sess.core())
206	claim := g.latest.Load()
207	sess.mu.Unlock()
208	if claim == nil || claim.attachment == nil || !claim.geometry.Valid() {
209		return nil, false
210	}
211	return claim, true
212}
213
214// current is deliberately one lock-free immutable-token comparison. Session
215// layout publication calls it while Tab locks and geometry.mu are held.
216func (g *sharedPTYGeometry) current(claim *sharedPTYClaim) bool {
217	return g != nil && claim != nil && g.latest.Load() == claim
218}
219
220func (g *sharedPTYGeometry) appliedSnapshot() domain.Geometry {
221	if g == nil {
222		return domain.Geometry{}
223	}
224	g.mu.Lock()
225	defer g.mu.Unlock()
226	return g.applied
227}
228
229func (g *sharedPTYGeometry) publishAppliedIfCurrent(claim *sharedPTYClaim) {
230	if g == nil || claim == nil {
231		return
232	}
233	g.mu.Lock()
234	if g.latest.Load() == claim {
235		g.applied = claim.geometry
236	}
237	g.mu.Unlock()
238}
239
240// scaleSharedPTYGeometry is the sole cell/pixel mapping policy. Integer
241// division deliberately truncates because kernel winsize and CSI reports
242// cannot represent fractional pixels. Session construction uses it before a
243// claim can be published; live paths call paneGeometry below.
244func scaleSharedPTYGeometry(full domain.Geometry, size domain.Size) domain.Geometry {
245	geometry := domain.Geometry{Size: size}
246	if !full.Valid() || !size.Valid() || !full.PixelsKnown() {
247		return geometry
248	}
249	geometry.PixelWidth = int(int64(full.PixelWidth) * int64(size.Cols) / int64(full.Cols))
250	geometry.PixelHeight = int(int64(full.PixelHeight) * int64(size.Rows) / int64(full.Rows))
251	return geometry.NormalizePixels()
252}
253
254func (g *sharedPTYGeometry) paneGeometry(size domain.Size) domain.Geometry {
255	if g == nil {
256		return domain.Geometry{Size: size}
257	}
258	claim := g.latest.Load()
259	if claim == nil {
260		return domain.Geometry{Size: size}
261	}
262	return scaleSharedPTYGeometry(claim.geometry, size)
263}
264
265// viewport returns false when no valid Attachment claim exists.
266func (g *sharedPTYGeometry) viewport(sess *session) (domain.Size, *sharedPTYClaim, bool) {
267	claim, ok := g.sourceSnapshot(sess)
268	if !ok {
269		return domain.Size{}, nil, false
270	}
271	content := contentSize(claim.geometry.Size)
272	return domain.Size{Cols: content.Cols, Rows: content.Rows + tabChromeRows}, claim, true
273}
274
275// reconcile applies the latest live Attachment claim to shared Session
276// PTY/layout geometry. An explicit source records a new claim first.
277func (g *sharedPTYGeometry) reconcile(d *Daemon, sess *session, source *attachedClient) bool {
278	if sess == nil {
279		return false
280	}
281	if source != nil {
282		if _, ok := g.claimAttachment(sess, source); !ok {
283			return false
284		}
285	}
286	want, claim, ok := g.viewport(sess)
287	if !ok {
288		return false
289	}
290	current := func() bool { return g.current(claim) }
291	applied := g.appliedSnapshot()
292	pixelGeometryChanged := (applied.PixelsKnown() || claim.geometry.PixelsKnown()) && applied != claim.geometry
293	if !pixelGeometryChanged && contentSize(sess.fullViewportSize()) == contentSize(want) {
294		return false
295	}
296	failed, ok := g.applySessionLayout(d, sess, want, current, nil)
297	if ok {
298		g.publishAppliedIfCurrent(claim)
299	}
300	if ok && len(failed) != 0 {
301		seen := make(map[*tab]struct{}, len(failed))
302		for _, member := range failed {
303			if member.tab == nil {
304				continue
305			}
306			if _, exists := seen[member.tab]; exists {
307				continue
308			}
309			seen[member.tab] = struct{}{}
310			g.scheduleAcceptedTabLayoutRetry(d, sess, member.tab)
311		}
312	}
313	return ok
314}
315
316func (g *sharedPTYGeometry) reconcileAndInvalidate(d *Daemon, sess *session, source *attachedClient, producer string) bool {
317	if !g.reconcile(d, sess, source) {
318		return false
319	}
320	d.invalidateRender(sess, nil, true, producer)
321	return true
322}
323
324func (g *sharedPTYGeometry) reconcileAndInvalidateAsync(d *Daemon, sess *session, producer string) {
325	if g == nil || d == nil {
326		return
327	}
328	d.sessWg.Go(func() {
329		g.reconcileAndInvalidate(d, sess, nil, producer)
330	})
331}

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