1package daemon
2
3import "github.com/bnema/vev/internal/domain"
4
5// sessionGeometrySource identifies the attachment whose latest attach,
6// resume, or resize claim controls shared PTY/layout geometry. claim is zero
7// only for legacy/headless test fixtures that construct attachment maps
8// directly instead of publishing them through the attachment registry.
9type sessionGeometrySource struct {
10 attachment *attachedClient
11 size domain.Size
12 claim uint64
13}
14
15// geometrySourceSnapshot returns the latest live attachment claim. The
16// attachment registry publishes geometryOwner under the session lock, and this
17// function reads the owner, claim, and claim size under that same lock. Later
18// revalidation at the layout publication fence is lock-free so a stale claim
19// can be rejected without taking session.mu.
20func (s *session) geometrySourceSnapshot() (sessionGeometrySource, bool) {
21 if s == nil {
22 return sessionGeometrySource{}, false
23 }
24
25 s.mu.Lock()
26 s.refreshGeometryOwnerLocked()
27 owner := s.geometryOwner.Load()
28 var size domain.Size
29 var claim uint64
30 if owner != nil {
31 size = owner.geometryClaimSizeSnapshot()
32 claim = owner.geometryClaim.Load()
33 }
34 s.mu.Unlock()
35
36 if owner == nil || !size.Valid() {
37 return sessionGeometrySource{}, false
38 }
39 return sessionGeometrySource{attachment: owner, size: size, claim: claim}, true
40}
41
42// geometryClaimTokenCurrent is deliberately lock-free. applySessionLayout invokes
43// its current callback while tab locks are held, so taking session.mu here
44// would reverse the session -> tab ordering used by layout mutation paths.
45func (s *session) geometryClaimTokenCurrent(attachment *attachedClient, claim uint64) bool {
46 if s == nil || attachment == nil || claim == 0 {
47 return true
48 }
49 return s.geometryOwner.Load() == attachment && attachment.geometryClaim.Load() == claim
50}
51
52func (s *session) geometryClaimCurrent(source sessionGeometrySource) bool {
53 return s.geometryClaimTokenCurrent(source.attachment, source.claim) &&
54 (source.attachment == nil || source.attachment.geometryClaimSizeSnapshot() == source.size)
55}
56
57// sessionGeometryViewport is called with a non-nil session by the daemon
58// geometry paths; it returns ok == false when no valid claim exists.
59func sessionGeometryViewport(sess *session) (domain.Size, sessionGeometrySource, bool) {
60 source, ok := sess.geometrySourceSnapshot()
61 if !ok {
62 return domain.Size{}, sessionGeometrySource{}, false
63 }
64 content := contentSize(source.size)
65 return domain.Size{Cols: content.Cols, Rows: content.Rows + tabChromeRows}, source, true
66}
67
68// recalculateSessionGeometry reconciles shared PTY/layout geometry with the
69// latest live attachment claim. An explicit source records a new claim before
70// the snapshot; attach and resume paths already claimed ownership when they
71// entered the attachment registry. If the owner detaches, unregistering the
72// attachment selects the most recently claimed remaining attachment.
73func (d *Daemon) recalculateSessionGeometry(sess *session, source *attachedClient) bool {
74 if sess == nil {
75 return false
76 }
77 if source != nil {
78 if _, ok := sess.claimGeometryOwner(source); !ok {
79 return false
80 }
81 }
82 want, owner, ok := sessionGeometryViewport(sess)
83 if !ok {
84 return false
85 }
86 current := func() bool { return sess.geometryClaimCurrent(owner) }
87 if contentSize(sess.fullViewportSize()) == contentSize(want) {
88 return false
89 }
90 failed, ok := d.applySessionLayout(sess, want, current, nil)
91 if ok && len(failed) != 0 {
92 seen := make(map[*tab]struct{}, len(failed))
93 for _, member := range failed {
94 if member.tab == nil {
95 continue
96 }
97 if _, exists := seen[member.tab]; exists {
98 continue
99 }
100 seen[member.tab] = struct{}{}
101 d.scheduleAcceptedTabLayoutRetry(sess, member.tab)
102 }
103 }
104 return ok
105}
106
107// recalculateSessionGeometryAndInvalidate publishes a shared render wake after
108// canonical geometry changes. A nil source uses the latest attach/resume/resize
109// claim already published by the attachment registry. The wake is asynchronous
110// so a slow peer cannot block a resize or detach.
111func (d *Daemon) recalculateSessionGeometryAndInvalidate(sess *session, source *attachedClient, producer string) bool {
112 if !d.recalculateSessionGeometry(sess, source) {
113 return false
114 }
115 d.invalidateRender(sess, nil, true, producer)
116 return true
117}
118
119func (d *Daemon) recalculateSessionGeometryAndInvalidateAsync(sess *session, producer string) {
120 if d == nil {
121 return
122 }
123 d.sessWg.Go(func() {
124 d.recalculateSessionGeometryAndInvalidate(sess, nil, producer)
125 })
126}