1package neferclient
2
3import (
4 "errors"
5 "fmt"
6
7 "github.com/bnema/go-wayland-bindings/client/extsessionlock"
8 "github.com/bnema/wlturbo/wl"
9)
10
11// Lock is an ext-session-lock-v1 session lock. It reports [Handler.Locked]
12// once every output is protected and [Handler.LockFinished] if the compositor
13// refuses or ends it. Closing the connection or destroying surfaces never
14// unlocks; only [Lock.Unlock] after Locked does. Owner goroutine only.
15type Lock struct {
16 c *Conn
17 gen uint32
18 obj *extsessionlock.ExtSessionLock
19 locked bool
20 finished bool
21 done bool
22}
23
24// Lock requests a session lock. At most one lock is live per connection.
25// The compositor answers through Handler.Locked or Handler.LockFinished.
26func (c *Conn) Lock() (*Lock, error) {
27 if c.closed {
28 return nil, ErrClosed
29 }
30 if c.curLock != nil {
31 return nil, errors.New("neferclient: session lock already acquired")
32 }
33 if err := c.ensureCore(); err != nil {
34 return nil, err
35 }
36 if err := c.ensureLockManager(); err != nil {
37 return nil, err
38 }
39 c.pause() // locked/finished may follow the request immediately
40 defer c.resume()
41 c.nextLock++
42 l := &Lock{c: c, gen: c.nextLock}
43 q, gen := c.q, l.gen
44 obj, err := c.g.lockMgr.Lock()
45 if err != nil {
46 return nil, err
47 }
48 obj.OnLocked(func() {
49 e := event{kind: evLocked, id: gen}
50 q.post(&e)
51 })
52 obj.OnFinished(func() {
53 e := event{kind: evLockFinished, id: gen}
54 q.post(&e)
55 })
56 l.obj = obj
57 c.curLock = l
58 return l, nil
59}
60
61// Locked reports whether the compositor confirmed the lock (applied by
62// Dispatch) and it has not finished.
63func (l *Lock) Locked() bool { return l.locked && !l.finished && !l.done }
64
65// NewSurface creates the lock surface covering the output with the given
66// registry name (see [Output.Global]). It becomes presentable after the first
67// Handler.Configure; a lock surface is always opaque and takes all input.
68func (l *Lock) NewSurface(output uint32) (*Surface, error) {
69 c := l.c
70 if c.closed {
71 return nil, ErrClosed
72 }
73 if l.done || l.finished {
74 return nil, errors.New("neferclient: lock ended")
75 }
76 e := c.entry(output)
77 if e == nil || !e.done {
78 return nil, fmt.Errorf("neferclient: lock output %d unavailable", output)
79 }
80 for _, other := range c.surfaces {
81 if other.lock == l && other.output == output {
82 return nil, fmt.Errorf("neferclient: output %d already has a lock surface", output)
83 }
84 }
85 c.pause()
86 defer c.resume()
87 s, err := c.newSurface(roleLock, 0, 0)
88 if err != nil {
89 return nil, err
90 }
91 ok := false
92 defer func() {
93 if !ok {
94 s.destroy()
95 }
96 }()
97 if s.lockSurf, err = l.obj.GetLockSurface(s.surf, e.proxy); err != nil {
98 return nil, err
99 }
100 s.lock, s.output = l, output
101 q, sid := c.q, uint32(s.id)
102 s.lockSurf.OnConfigure(func(serial, w, h uint32) {
103 ev := configureEvent(evLockConfigure, sid, serial, w, h)
104 q.post(&ev)
105 })
106 // ext-session-lock forbids the initial empty commit: the configure comes
107 // with get_lock_surface and the first commit carries a buffer.
108 if err = s.finishSurface(); err != nil {
109 return nil, err
110 }
111 ok = true
112 return s, nil
113}
114
115// Unlock sends unlock_and_destroy and roundtrips so the compositor processed
116// it before the caller closes anything. It is only valid after Handler.Locked.
117// The lock surfaces must be closed afterwards (or before: both are legal).
118func (l *Lock) Unlock() error {
119 if l.done {
120 return errors.New("neferclient: lock already destroyed")
121 }
122 if !l.Locked() {
123 return errors.New("neferclient: unlock before the locked event")
124 }
125 if err := l.obj.UnlockAndDestroy(); err != nil {
126 return err
127 }
128 l.done = true
129 l.c.curLock = nil
130 return l.c.Roundtrip()
131}
132
133// Close destroys a lock that never reached Locked (refused or abandoned). A
134// locked session is only ended by Unlock, so Close fails once Locked was
135// applied. Because the compositor may have sent locked before it processes the
136// destroy request (which would then be a protocol error), Close first
137// roundtrips and looks through the queued events: if a locked event for this
138// lock is already queued it is treated as applied (Handler.Locked is still
139// delivered by the next Dispatch) and Close fails with the same error.
140func (l *Lock) Close() error {
141 if l.done {
142 return nil
143 }
144 if !l.locked && !l.finished {
145 if err := l.c.Roundtrip(); err != nil {
146 return err
147 }
148 l.locked = l.locked || l.c.q.hasLocked(l.gen)
149 }
150 if l.locked && !l.finished {
151 return errors.New("neferclient: lock is locked: use Unlock")
152 }
153 l.done = true
154 l.c.curLock = nil
155 var err error
156 if l.obj != nil {
157 err = l.obj.Destroy()
158 }
159 return err
160}
161
162var _ wl.Proxy = (*frameCallback)(nil)