1// Command lock shows an ext-session-lock-v1 session lock with a password
2// field, drawn by NeferGUI.
3//
4// THIS IS A DEMO, NOT A SCREEN LOCKER. It performs no authentication: pressing
5// Enter unlocks the session whatever was typed, so anyone at the keyboard can
6// open it. Do not use it, or code copied from it, to protect a real session. A
7// real locker must verify the secret (PAM, for example) before it calls
8// Lock.Unlock. Run it on a real session only if you accept that.
9//
10// The typed text goes into a neferclient.SecretBuffer, never into the view or
11// the renderer: the field shows Node.Masked with the count from
12// SecretBuffer.Len(). The program never converts the secret to a string and
13// never prints it; the buffer is wiped on unlock and on exit.
14//
15// The program prints "locked", "focused" (the keyboard reached a lock surface)
16// and "unlocked" on standard output as progress markers; the headless test
17// waits for them. It never prints anything typed.
18//
19// All logic runs on the owner goroutine, inside or right after Conn.Dispatch.
20// The lock needs one surface per output; each gets its own Renderer.
21package main
22
23import (
24 "context"
25 "errors"
26 "fmt"
27 "os"
28 "os/signal"
29 "syscall"
30 "time"
31
32 "github.com/bnema/neferclient"
33 "github.com/bnema/nefergui"
34)
35
36const keysymReturn, keysymKPEnter = 0xff0d, 0xff8d // xkb keysyms
37
38// guiMods is the set of modifiers NeferGUI knows. neferclient and NeferGUI use
39// the same bit order for them, so the plain bits can be copied.
40const guiMods = neferclient.ModShift | neferclient.ModCtrl | neferclient.ModAlt | neferclient.ModSuper
41
42// model is what the view reads. The secret stays in its buffer; the view only
43// asks for its length.
44type model struct{ secret *neferclient.SecretBuffer }
45
46func view(f *nefergui.Frame, m *model) {
47 root := f.Root().Column(nefergui.Inline("padding:40px;gap:12px;color:#ffffff;background:#101820"))
48 root.Heading("Session locked (demo: no authentication)")
49 root.Text("Type anything, then press Enter to unlock.")
50 field := root.Row(nefergui.Inline("width:320px;height:36px;padding:6px;background:#203040;border:1px solid #6080a0"))
51 field.Masked(m.secret.Len()) // bullets only: the view never sees the text
52}
53
54// screen is the lock surface of one output and its renderer.
55type screen struct {
56 output uint32 // wl_output registry name (Output.Global)
57 conn *neferclient.Conn
58 model *model
59 surf *neferclient.Surface
60 r *nefergui.Renderer
61 out nefergui.Output
62 damage []neferclient.Rect
63
64 configured, canPresent, haveAcquire bool
65 live map[uint64]int // buffer → release eventfd still watched
66}
67
68type app struct {
69 neferclient.NopHandler
70
71 conn *neferclient.Conn
72 lock *neferclient.Lock
73 seat *neferclient.Seat
74 secret *neferclient.SecretBuffer
75 model model
76 screens map[neferclient.SurfaceID]*screen
77
78 cursor neferclient.CursorShape
79 hover neferclient.SurfaceID // surface under the pointer, 0 outside
80 locked, unlocked bool
81 err error
82}
83
84func main() {
85 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
86 defer stop()
87 if err := run(ctx); err != nil {
88 fmt.Fprintln(os.Stderr, "lock:", err)
89 os.Exit(1)
90 }
91}
92
93func run(ctx context.Context) (err error) {
94 conn, err := neferclient.Connect(ctx, "") // WAYLAND_DISPLAY
95 if err != nil {
96 return fmt.Errorf("connect: %w", err)
97 }
98 a := &app{conn: conn, screens: map[neferclient.SurfaceID]*screen{}, cursor: neferclient.CursorDefault}
99 a.secret = neferclient.NewSecretBuffer(256)
100 a.model.secret = a.secret
101 // Shutdown order: wipe the secret, close the connection (this drops its
102 // descriptor watches), then free the renderers and their GPU resources,
103 // including the release eventfds. The same order as the layer example.
104 defer func() {
105 a.secret.Wipe()
106 err = errors.Join(err, conn.Close())
107 for _, s := range a.screens {
108 err = errors.Join(err, s.close())
109 }
110 }()
111
112 // The compositor may announce its first output just after Connect.
113 for len(conn.Outputs()) == 0 {
114 select {
115 case <-conn.Wake():
116 if err = conn.Dispatch(a); err != nil {
117 return fmt.Errorf("dispatch: %w", err)
118 }
119 case <-ctx.Done():
120 return nil // interrupt or SIGTERM before the lock: nothing to undo
121 }
122 }
123 if a.lock, err = conn.Lock(); err != nil {
124 return fmt.Errorf("lock: %w", err)
125 }
126 for _, o := range conn.Outputs() {
127 if err = a.addScreen(o.Global); err != nil {
128 return err
129 }
130 }
131 // Typed text goes into the secret buffer, not into Handler.Key.
132 a.seat = conn.Seat()
133 a.seat.SetSecret(a.secret)
134 defer a.seat.SetSecret(nil)
135
136 var tick <-chan time.Time
137 for !a.unlocked {
138 select {
139 case <-ctx.Done():
140 // Interrupt or SIGTERM: a clean exit. The session stays locked,
141 // because only Lock.Unlock ends a lock; the compositor decides
142 // what happens to a lock whose client is gone.
143 return nil
144 case <-conn.Wake():
145 if err = conn.Dispatch(a); err != nil {
146 return fmt.Errorf("dispatch: %w", err)
147 }
148 case <-tick:
149 }
150 if a.err != nil {
151 return a.err
152 }
153 if a.unlocked {
154 break
155 }
156 pending := false
157 for _, s := range a.screens {
158 shown, err := s.draw()
159 if err != nil {
160 return err
161 }
162 // The cursor the hovered element asked for → Seat.SetCursor, only
163 // from the screen under the pointer.
164 if shown && s.surf.ID() == a.hover {
165 a.cursor = cursorShape(s.out.Cursor)
166 if err = a.seat.SetCursor(a.cursor); err != nil {
167 return fmt.Errorf("cursor: %w", err)
168 }
169 }
170 pending = pending || (s.r != nil && s.r.Pending())
171 }
172 tick = nil
173 if pending {
174 tick = time.After(2 * time.Millisecond)
175 }
176 }
177 return nil
178}
179
180func (a *app) addScreen(output uint32) error {
181 surf, err := a.lock.NewSurface(output)
182 if err != nil {
183 return fmt.Errorf("lock surface: %w", err)
184 }
185 a.screens[surf.ID()] = &screen{output: output, conn: a.conn, model: &a.model, surf: surf, live: map[uint64]int{}}
186 return nil
187}
188
189func (a *app) fail(err error) {
190 if a.err == nil {
191 a.err = err
192 }
193}
194
195func (a *app) Error(err error) { a.fail(err) }
196
197// OutputAdded: a new output must be covered too while the session is locked.
198func (a *app) OutputAdded(out *neferclient.Output) {
199 if a.lock == nil || a.unlocked {
200 return
201 }
202 if err := a.addScreen(out.Global); err != nil {
203 a.fail(err)
204 }
205}
206
207// OutputRemoved: stop watching the screen's release fds, free its renderer and
208// surface, and forget it.
209func (a *app) OutputRemoved(global uint32) {
210 for id, s := range a.screens {
211 if s.output != global {
212 continue
213 }
214 if a.hover == id {
215 a.hover = 0
216 }
217 delete(a.screens, id)
218 if err := s.release(); err != nil {
219 a.fail(fmt.Errorf("output %d removed: %w", global, err))
220 }
221 }
222}
223
224func (a *app) Locked() {
225 a.locked = true
226 fmt.Println("locked")
227}
228
229func (a *app) LockFinished() {
230 a.fail(errors.Join(errors.New("the compositor refused or ended the lock"), a.lock.Close()))
231}
232
233// FeedbackDone: dmabuf feedback → nefergui.RendererConfig.
234//
235// Only the first complete feedback is used. A later one (the compositor
236// changing its preferred device or formats) is ignored: this demo assumes a
237// single GPU, and rebuilding the renderers is application policy that would
238// obscure the glue. A real application would create a new Renderer here and
239// re-import its buffers.
240func (a *app) FeedbackDone(id neferclient.SurfaceID) {
241 if s := a.screens[id]; s != nil {
242 if err := s.setup(); err != nil {
243 a.fail(err)
244 }
245 }
246}
247
248func (a *app) Configure(id neferclient.SurfaceID, _, _ int32) {
249 if s := a.screens[id]; s != nil {
250 if !s.configured { // later configures must not lift the frame-callback gate
251 s.canPresent = true
252 }
253 s.configured = true
254 s.resize()
255 if err := s.setup(); err != nil {
256 a.fail(err)
257 }
258 }
259}
260
261func (a *app) Scale(id neferclient.SurfaceID, _ float64) {
262 if s := a.screens[id]; s != nil {
263 s.resize()
264 }
265}
266
267func (a *app) Frame(id neferclient.SurfaceID) {
268 if s := a.screens[id]; s != nil {
269 s.canPresent = true
270 }
271}
272
273// FDReady: the release eventfd of a buffer is readable (watched in present;
274// the id packs the surface and the buffer): hand the buffer back.
275func (a *app) FDReady(id uint64) {
276 s := a.screens[neferclient.SurfaceID(id>>32)]
277 if s == nil || s.r == nil {
278 return
279 }
280 if err := s.r.Released(id & 0xffffffff); err != nil {
281 a.fail(fmt.Errorf("release buffer: %w", err))
282 }
283}
284
285// Pointer: neferclient events → nefergui.Input, field by field.
286func (a *app) Pointer(ev *neferclient.PointerEvent) {
287 // Track the screen under the pointer first: the cursor shape is applied
288 // only from it.
289 switch ev.Kind {
290 case neferclient.PointerEnter:
291 a.hover = ev.Surface
292 // The shape only applies after an enter: send the last one again.
293 if err := a.seat.SetCursor(a.cursor); err != nil {
294 a.fail(fmt.Errorf("cursor: %w", err))
295 }
296 case neferclient.PointerLeave:
297 if a.hover == ev.Surface {
298 a.hover = 0
299 }
300 }
301 s := a.screens[ev.Surface]
302 if s == nil || s.r == nil {
303 return
304 }
305 in := nefergui.Input{X: ev.X, Y: ev.Y, Kind: nefergui.InputPointerMotion}
306 switch ev.Kind {
307 case neferclient.PointerLeave:
308 in.Kind = nefergui.InputPointerLeave
309 case neferclient.PointerButton:
310 in.Kind, in.Button = nefergui.InputPointerRelease, ev.Button
311 if ev.Pressed {
312 in.Kind = nefergui.InputPointerPress
313 }
314 case neferclient.PointerAxis:
315 in.Kind, in.DX, in.DY = nefergui.InputPointerAxis, ev.DX, ev.DY
316 }
317 s.r.Input(&in)
318}
319
320// Key: Enter unlocks (DEMO: no authentication). Keys that produced text were
321// already stored in the SecretBuffer and carry no keysym or text: they are not
322// forwarded, so the renderer never sees the secret.
323func (a *app) Key(ev *neferclient.KeyEvent) {
324 if ev.Secret {
325 return
326 }
327 if ev.Pressed && !ev.Repeat && (ev.Keysym == keysymReturn || ev.Keysym == keysymKPEnter) {
328 a.unlock()
329 return
330 }
331 if s := a.screens[ev.Surface]; s != nil && s.r != nil {
332 s.r.Input(&nefergui.Input{
333 Kind: nefergui.InputKey, Keysym: ev.Keysym, Pressed: ev.Pressed, Repeat: ev.Repeat,
334 Modifiers: nefergui.Modifiers(ev.Modifiers & guiMods),
335 })
336 }
337}
338
339func (a *app) KeyboardFocus(id neferclient.SurfaceID, focused bool) {
340 if focused {
341 fmt.Println("focused")
342 }
343 if s := a.screens[id]; s != nil && s.r != nil {
344 kind := nefergui.InputFocusOut
345 if focused {
346 kind = nefergui.InputFocusIn
347 }
348 s.r.Input(&nefergui.Input{Kind: kind})
349 }
350}
351
352// SecretChanged: the buffer changed; only its length is shown, on every screen.
353func (a *app) SecretChanged(int) {
354 for _, s := range a.screens {
355 if s.r != nil {
356 s.r.Invalidate()
357 }
358 }
359}
360
361// unlock ends the session lock. DEMO ONLY: nothing is verified here.
362func (a *app) unlock() {
363 if !a.locked || a.unlocked {
364 return
365 }
366 a.secret.Wipe()
367 if err := a.lock.Unlock(); err != nil {
368 a.fail(fmt.Errorf("unlock: %w", err))
369 return
370 }
371 a.unlocked = true
372 fmt.Println("unlocked")
373}
374
375// setup creates the renderer once the surface is configured and its dmabuf
376// feedback is complete.
377func (s *screen) setup() error {
378 fb := s.surf.Feedback()
379 if s.r != nil || !s.configured || fb == nil {
380 return nil
381 }
382 cfg := nefergui.RendererConfig{MainDevice: fb.MainDevice, Formats: make([]nefergui.Format, len(fb.Formats))}
383 for i, f := range fb.Formats {
384 cfg.Formats[i] = nefergui.Format{FourCC: f.FourCC, Modifier: f.Modifier}
385 }
386 r, err := nefergui.NewRenderer(cfg)
387 if err != nil {
388 return fmt.Errorf("renderer: %w", err)
389 }
390 s.r = r
391 s.resize()
392 return nil
393}
394
395func (s *screen) resize() {
396 if s.r != nil {
397 w, h, scale := s.surf.Size()
398 s.r.Resize(int(w), int(h), scale)
399 }
400}
401
402// draw renders when something changed and presents the frame.
403func (s *screen) draw() (bool, error) {
404 if s.r == nil || !s.canPresent {
405 return false, nil
406 }
407 ok, err := s.r.Render(&s.out, s.model, view)
408 if err != nil {
409 return false, fmt.Errorf("render: %w", err)
410 }
411 if !ok {
412 return false, nil
413 }
414 return true, s.present()
415}
416
417func cursorShape(c nefergui.Cursor) neferclient.CursorShape {
418 switch c {
419 case nefergui.CursorPointer:
420 return neferclient.CursorPointer
421 case nefergui.CursorText:
422 return neferclient.CursorText
423 case nefergui.CursorNotAllowed:
424 return neferclient.CursorNotAllowed
425 }
426 return neferclient.CursorDefault
427}
428
429// watchID packs a surface and a buffer into the Conn.WatchFD id.
430func (s *screen) watchID(buffer uint64) uint64 { return uint64(s.surf.ID())<<32 | buffer }
431
432// present: Renderer.Render output → ImportBuffer, ImportTimeline, Present.
433func (s *screen) present() error {
434 out := &s.out
435 for _, rt := range out.Retired { // buffers of an older size
436 if err := s.conn.UnwatchFD(rt.ReleaseFD); err != nil {
437 return fmt.Errorf("unwatch retired buffer %d: %w", rt.Buffer, err)
438 }
439 delete(s.live, rt.Buffer)
440 if err := s.surf.DestroyBuffer(rt.Buffer); err != nil {
441 return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err)
442 }
443 // The release timeline of a buffer is imported under the buffer's id.
444 if err := s.surf.DestroyTimeline(rt.Buffer); err != nil {
445 return fmt.Errorf("destroy retired timeline %d: %w", rt.Buffer, err)
446 }
447 }
448 if out.NewBuffer {
449 buf := neferclient.Buffer{
450 Width: out.Width, Height: out.Height, FourCC: out.FourCC, Modifier: out.Modifier,
451 PlaneCount: out.PlaneCount,
452 }
453 for i, p := range out.Planes {
454 buf.Planes[i] = neferclient.Plane{FD: p.FD, Offset: p.Offset, Stride: p.Stride}
455 }
456 if err := s.surf.ImportBuffer(out.Buffer, &buf); err != nil {
457 return fmt.Errorf("import buffer: %w", err)
458 }
459 // The release eventfd is stable per buffer: watch it once; FDReady
460 // calls Renderer.Released.
461 if err := s.conn.WatchFD(out.ReleaseFD, s.watchID(out.Buffer)); err != nil {
462 return fmt.Errorf("watch release fd: %w", err)
463 }
464 s.live[out.Buffer] = out.ReleaseFD
465 }
466 if out.NewTimelines {
467 if !s.haveAcquire { // the acquire timeline is shared by every buffer
468 if err := s.surf.ImportTimeline(out.Acquire.ID, out.Acquire.FD); err != nil {
469 return fmt.Errorf("import acquire timeline: %w", err)
470 }
471 s.haveAcquire = true
472 }
473 if err := s.surf.ImportTimeline(out.Release.ID, out.Release.FD); err != nil {
474 return fmt.Errorf("import release timeline: %w", err)
475 }
476 }
477 s.damage = s.damage[:0]
478 for _, d := range out.Damage {
479 s.damage = append(s.damage, neferclient.Rect{X: d.X, Y: d.Y, Width: d.Width, Height: d.Height})
480 }
481 err := s.surf.Present(&neferclient.Present{
482 Buffer: out.Buffer,
483 AcquireTimeline: out.Acquire.ID,
484 ReleaseTimeline: out.Release.ID,
485 AcquirePoint: out.AcquirePoint,
486 ReleasePoint: out.ReleasePoint,
487 Damage: s.damage,
488 Opaque: true, // XRGB8888: RendererConfig.Transparent is false
489 })
490 if err != nil {
491 return fmt.Errorf("present: %w", err)
492 }
493 s.canPresent = false // until Handler.Frame
494 return nil
495}
496
497// release stops watching the live release eventfds, then frees the renderer and
498// the surface (an output went away). The release eventfds belong to the
499// renderer, so they are unwatched before it closes.
500func (s *screen) release() error {
501 var errs []error
502 for buffer, fd := range s.live {
503 if err := s.conn.UnwatchFD(fd); err != nil {
504 errs = append(errs, fmt.Errorf("unwatch buffer %d: %w", buffer, err))
505 }
506 }
507 clear(s.live)
508 return errors.Join(append(errs, s.close())...)
509}
510
511// close frees the renderer and the surface. The session lock itself is only
512// ended by Lock.Unlock.
513func (s *screen) close() error {
514 var err error
515 if s.r != nil {
516 err = s.r.Close()
517 }
518 return errors.Join(err, s.surf.Close())
519}