1package daemon
2
3import (
4 "context"
5
6 "github.com/bnema/vev/internal/domain"
7 "github.com/bnema/vev/internal/domain/layout"
8)
9
10// moveTabTopology is the private whole-Tab topology Implementation consumed by
11// the shared moveTransaction Module.
12type moveTabTopology struct {
13 req moveTabRequest
14 admission moveTabAdmission
15 fencedPanes []*pane
16}
17
18type moveTabAdmission struct {
19 tab *tab
20 sourceIndex int
21 layoutGeneration uint64
22 floatingState floatingState
23 floatingPane *pane
24 floatingGeneration uint64
25 panes []*pane
26 destinationActive *tab
27 destinationSize domain.Size
28 finalSource bool
29}
30
31type moveTabCommit struct {
32 req moveTabRequest
33 source *session
34 destination *session
35 admission moveTabAdmission
36}
37
38func (d *Daemon) moveTab(req moveTabRequest) error {
39 return d.executeMove(&moveTabTopology{req: req})
40}
41
42func (p *moveTabTopology) transactionRequest() moveTransactionRequest {
43 req := p.req
44 return moveTransactionRequest{
45 follow: req.Follow,
46 beforeFollow: req.BeforeFollow,
47 afterFollow: req.AfterFollow,
48 operation: "tab",
49 attachment: req.Attachment,
50 attachmentCapability: req.AttachmentCapability,
51 source: req.Source,
52 destination: req.Destination,
53 logAttrs: []any{
54 "source_session_id", req.Source.ID,
55 "source_tab_id", req.SourceTabID,
56 "destination_session_id", req.Destination.ID,
57 },
58 }
59}
60
61func (p *moveTabTopology) validRequest() bool {
62 return p != nil && p.req.SourceTabID != "" && p.req.Source.ID != p.req.Destination.ID
63}
64
65// admitLocked runs with daemon and ordered Session locks held. It snapshots
66// whole-Tab policy without publishing topology or Pane ownership.
67func (p *moveTabTopology) admitLocked(_ *Daemon, source, destination *session) error {
68 if source == destination {
69 return errMovePaneInvalid
70 }
71 moved := findMoveTabLocked(source, p.req.SourceTabID)
72 if moved == nil || len(destination.tabs) == 0 {
73 return errMoveStaleTarget
74 }
75 destinationActive := destination.tabs[0]
76 destinationActive.mu.Lock()
77 destinationSize := destinationActive.size
78 destinationActive.mu.Unlock()
79
80 moved.mu.Lock()
81 defer moved.mu.Unlock()
82 if !moved.floatingTransferableLocked() {
83 return errMoveFloatingWarming
84 }
85 if moved.tree == nil || moved.tree.Root == nil {
86 return errMovePaneInvalid
87 }
88 if _, ok := layout.Solve(moved.tree.Root, domain.Rect{Width: destinationSize.Cols, Height: destinationSize.Rows}); !ok {
89 return errMoveTooSmall
90 }
91 panes := make([]*pane, 0, len(moved.panes)+1)
92 for _, pane := range moved.panes {
93 panes = append(panes, pane)
94 }
95 if moved.floating.pane != nil && (moved.floating.state == floatingHidden || moved.floating.state == floatingVisible) {
96 panes = append(panes, moved.floating.pane)
97 }
98 p.admission = moveTabAdmission{
99 tab: moved, sourceIndex: indexMoveTabLocked(source, moved),
100 layoutGeneration: moved.layoutGeneration, floatingState: moved.floating.state,
101 floatingPane: moved.floating.pane, floatingGeneration: moved.floating.generation,
102 panes: panes, destinationActive: destinationActive, destinationSize: destinationSize,
103 finalSource: len(source.tabs) == 1,
104 }
105 return nil
106}
107
108func (p *moveTabTopology) afterAdmission(d *Daemon) {
109 if d.afterMoveTabSourceSnapshot != nil {
110 d.afterMoveTabSourceSnapshot()
111 }
112}
113
114func (p *moveTabTopology) willRetireSource() bool {
115 return p != nil && p.admission.finalSource
116}
117
118func (p *moveTabTopology) resizeFences(source, destination *session) *moveResizeFences {
119 fences := newMoveTabResizeFences(source, destination, p.admission.tab)
120 p.fencedPanes = fences.panes
121 return fences
122}
123
124func (p *moveTabTopology) publishLocked(d *Daemon, source, destination *session) (moveTopologyPublication, error) {
125 commit := moveTabCommit{req: p.req, source: source, destination: destination, admission: p.admission}
126 return commit.publishTopologyLocked(d, p.fencedPanes)
127}
128
129// publishTopologyLocked performs every whole-Tab validation before changing
130// membership, Tab context, or Pane owners. The shared transaction owns common
131// Session retirement and persistence capture after this method succeeds.
132func (c *moveTabCommit) publishTopologyLocked(d *Daemon, fencedPanes []*pane) (moveTopologyPublication, error) {
133 moved := c.admission.tab
134 if indexMoveTabLocked(c.source, moved) != c.admission.sourceIndex || findMoveTabLocked(c.destination, c.req.SourceTabID) != nil ||
135 (len(c.source.tabs) == 1) != c.admission.finalSource {
136 return moveTopologyPublication{}, errMoveStaleTarget
137 }
138 if len(c.destination.tabs) == 0 || c.destination.tabs[0] != c.admission.destinationActive {
139 return moveTopologyPublication{}, errMoveStaleTarget
140 }
141 c.admission.destinationActive.mu.Lock()
142 destinationSizeCurrent := c.admission.destinationActive.size == c.admission.destinationSize
143 c.admission.destinationActive.mu.Unlock()
144 if !destinationSizeCurrent {
145 return moveTopologyPublication{}, errMoveStaleTarget
146 }
147
148 moved.mu.Lock()
149 defer moved.mu.Unlock()
150 if moved.layoutGeneration != c.admission.layoutGeneration || !moved.floatingTransferableLocked() ||
151 moved.floating.state != c.admission.floatingState || moved.floating.pane != c.admission.floatingPane ||
152 moved.floating.generation != c.admission.floatingGeneration || !sameMoveTabPanesLocked(moved, c.admission.panes, fencedPanes) {
153 return moveTopologyPublication{}, errMoveStaleTarget
154 }
155 for _, pane := range fencedPanes {
156 pane.mu.Lock()
157 defer pane.mu.Unlock()
158 owner := pane.ownerSnapshot()
159 if owner == nil || owner.session != c.source || owner.tab != moved {
160 return moveTopologyPublication{}, errMoveStaleTarget
161 }
162 }
163
164 // Everything above is fallible. Membership, context, and Pane owner writes
165 // below form one in-memory publication and cannot become a Move rejection.
166 idx := c.admission.sourceIndex
167 if len(c.source.tabs) > 1 {
168 c.source.prepareAttachmentViewsForRemovedTabLocked(moved, idx)
169 }
170 c.source.tabs = append(c.source.tabs[:idx], c.source.tabs[idx+1:]...)
171 c.destination.tabs = append(c.destination.tabs, moved)
172 moved.size = c.admission.destinationSize
173 moved.bumpLayoutGenerationLocked()
174 oldTabCancel := moved.cancel
175 parent := c.destination.ctx
176 if parent == nil {
177 parent = d.serveCtx
178 }
179 moved.ctx, moved.cancel = context.WithCancel(parent)
180
181 type ownerChange struct {
182 pane *pane
183 oldOwner paneEffectLease
184 newOwner paneEffectLease
185 }
186 ownerChanges := make([]ownerChange, 0, len(fencedPanes))
187 for _, pane := range fencedPanes {
188 floatingGeneration := uint64(0)
189 if pane == moved.floating.pane {
190 floatingGeneration = moved.floating.generation
191 }
192 oldOwner, newOwner := pane.publishOwnerLocked(c.destination, moved, floatingGeneration)
193 ownerChanges = append(ownerChanges, ownerChange{pane: pane, oldOwner: oldOwner, newOwner: newOwner})
194 }
195 if d.beforeMoveTabCommit != nil {
196 d.beforeMoveTabCommit()
197 }
198 var syncCleanup syncTimerCleanup
199 for _, change := range ownerChanges {
200 syncCleanup.append(d.migratePaneSyncOwnerLocked(change.pane, change.oldOwner, change.newOwner))
201 }
202
203 return moveTopologyPublication{
204 sourceTab: moved,
205 destinationTab: moved,
206 movedPane: firstMovePane(c.admission.panes),
207 movedPanes: c.admission.panes,
208 syncCleanup: syncCleanup,
209 oldTabCancel: oldTabCancel,
210 sourceTabRemoved: true,
211 sourceEmpty: len(c.source.tabs) == 0,
212 }, nil
213}
214
215func sameMoveTabPanesLocked(tb *tab, admitted, fenced []*pane) bool {
216 current := make(map[*pane]struct{}, len(tb.panes)+1)
217 for _, pane := range tb.panes {
218 current[pane] = struct{}{}
219 }
220 if tb.floating.pane != nil && (tb.floating.state == floatingHidden || tb.floating.state == floatingVisible) {
221 current[tb.floating.pane] = struct{}{}
222 }
223 if len(current) != len(admitted) || len(current) != len(fenced) {
224 return false
225 }
226 for _, pane := range admitted {
227 if _, ok := current[pane]; !ok {
228 return false
229 }
230 }
231 for _, pane := range fenced {
232 if _, ok := current[pane]; !ok {
233 return false
234 }
235 }
236 return true
237}