1package daemon
2
3import (
4 "slices"
5
6 "github.com/bnema/vev/internal/domain"
7 "github.com/bnema/vev/internal/domain/layout"
8)
9
10// mutateTargetLayout preserves the publication semantics used by resize and
11// equalize mutations, which always publish a successful candidate.
12func (d *Daemon) mutateTargetLayout(target daemonActionTarget, requirePane bool, mutate func(*layout.Tree, domain.Rect) error) error {
13 _, err := d.mutateTargetLayoutChanged(target, requirePane, func(candidate *layout.Tree, area domain.Rect) (bool, error) {
14 if err := mutate(candidate, area); err != nil {
15 return false, err
16 }
17 return true, nil
18 })
19 return err
20}
21
22// mutateTargetLayoutChanged validates an explicit target and publishes the
23// candidate only when mutate reports a change. State locks are released before
24// applying pane geometry so PTY.Resize never runs under daemon state locks.
25func (d *Daemon) mutateTargetLayoutChanged(target daemonActionTarget, requirePane bool, mutate func(*layout.Tree, domain.Rect) (bool, error)) (bool, error) {
26 if target.session == nil || target.tab == nil || (requirePane && target.pane == nil) {
27 return false, layout.ErrNotFound
28 }
29
30 d.mu.Lock()
31 if d.sessions[target.session.id] != target.session {
32 d.mu.Unlock()
33 return false, layout.ErrNotFound
34 }
35 target.session.mu.Lock()
36 if !slices.Contains(target.session.tabs, target.tab) {
37 target.session.mu.Unlock()
38 d.mu.Unlock()
39 return false, layout.ErrNotFound
40 }
41 target.tab.mu.Lock()
42 if target.tab.tree == nil || (requirePane && target.tab.panes[target.pane.id] != target.pane) {
43 target.tab.mu.Unlock()
44 target.session.mu.Unlock()
45 d.mu.Unlock()
46 return false, layout.ErrNotFound
47 }
48
49 candidate := target.tab.tree.Clone()
50 area := domain.Rect{Width: target.tab.size.Cols, Height: target.tab.size.Rows}
51 changed, err := mutate(candidate, area)
52 if err != nil {
53 target.tab.mu.Unlock()
54 target.session.mu.Unlock()
55 d.mu.Unlock()
56 return false, err
57 }
58 if !changed {
59 target.tab.mu.Unlock()
60 target.session.mu.Unlock()
61 d.mu.Unlock()
62 return false, nil
63 }
64
65 target.tab.tree = candidate
66 target.tab.bumpLayoutGenerationLocked()
67 target.tab.mu.Unlock()
68 target.session.mu.Unlock()
69 d.mu.Unlock()
70
71 if !target.session.geometry.applyTabLayout(d, target.session, target.tab) {
72 target.tab.mu.Lock()
73 applied := target.tab.tree == candidate
74 target.tab.mu.Unlock()
75 if !applied {
76 return false, layout.ErrNotFound
77 }
78 }
79 return true, nil
80}