Session lock
nefergui.RunLock implements the client side of ext-session-lock-v1. It acquires the lock, covers every output with opaque black and hosts the caller's view on one output. Authentication is the caller's; NeferGUI never unlocks on its own.
Behavior
- One Wayland connection, seat and reader serve every lock surface. One lock surface is created per output present at acquisition; outputs added later get a black surface, removed outputs lose theirs.
- Rendering uses the normal Vulkan, DMA-BUF and explicit-sync path only: no shared-memory buffer, no readback, no implicit-sync fallback. A surface allocates and attaches a buffer only after its configure was acknowledged and its dmabuf feedback round completed (
Window.FeedbackDone).
- Exactly one output hosts the view: the one whose surface has keyboard focus, initially the first. Every other output shows plain black (an empty frame).
OnLocked fires when the compositor sends locked. finished (refused or ended) makes RunLock return ErrLockFinished; the lock is then destroyed with a plain destroy, never an unlock.
- Verification is the caller's and asynchronous:
OnSubmit never blocks and never unlocks. Unlock happens only when the caller sends on LockConfig.Unlock (after its own verification); a request made before locked is held until locked, closing the channel is not a request. Then: unlock_and_destroy, a display roundtrip, and the connection closes. The caller shows progress with LockConfig.Status (LockIdle, LockBusy, LockFailed), surfaced as LockState.Status. Cancellation or any error closes the connection without unlocking, which keeps the session locked.
Configuration
Display is the Wayland socket name; empty uses the environment.
Wake requests a redraw for each value received. Its forwarder is scoped to RunLock and stops when it returns.
OnOutputError(output, err) reports, with the connector name, an output added after acquisition that could not get a lock surface or render path. Such hotplug failures are skipped (the compositor keeps the output black); failures on outputs present at acquisition abort RunLock.
NewSecretBuffer panics unless the capacity is between 1 and 4096 bytes. RunLock wipes Secret when it returns, and the buffer is wiped after OnSubmit even if it panics.
- Outputs are promoted in ascending global order, so the first host is deterministic.
- Keyboard focus-out drops held keys and ignores key presses until focus returns; typed bytes stay in
Secret, and input continues when any lock surface regains focus (that surface becomes the host).
Typed keys go through the keyboard secret path (EventSecret/TickSecret) into a caller-owned SecretBuffer. They never become a Go string, and never enter the model, the frame tree, render text, IME, pre-edit or the clipboard (lock surfaces bind neither). The view receives LockState.Mask, the number of typed code points, and draws bullets.
Enter calls OnSubmit (after locked only; the buffer is wiped right after it returns, so it must copy or consume the slice and must not block), Escape and Ctrl+U clear, Backspace removes the last code point. Before locked, Enter only clears. Scratch buffers are zeroed after every key.
Example
secret := nefergui.NewSecretBuffer(256)
defer secret.Wipe()
err := nefergui.RunLock(ctx, nefergui.LockConfig{
Secret: secret,
Styles: "lock.css",
View: func(f *nefergui.Frame, s nefergui.LockState) {
box := f.Root(nefergui.Class("screen")).Column(nefergui.Class("box"))
box.Text(s.Bullets(), nefergui.Key("mask"))
},
Unlock: unlock, // chan struct{}: send after verification succeeded
Status: status, // chan nefergui.LockStatus: LockBusy / LockFailed / LockIdle
OnSubmit: func(pw []byte) { // copy or consume pw now; it is wiped on return
go verify(bytes.Clone(pw), unlock, status)
},
})
Verification
Wire tests cover the shared connection (reader pause and queue saturation, keyboard and pointer generations, feedback completion). NEFERGUI_HARNESS=1 with a NeferWL binary in NEFERGUI_NEFERWL runs the headless lock tests (TestHarnessConnectionLock, TestHarnessRunLock, TestHarnessRunLockRefused).
source
1# Session lock
2
3`nefergui.RunLock` implements the client side of `ext-session-lock-v1`. It acquires the lock, covers every output with opaque black and hosts the caller's view on one output. Authentication is the caller's; NeferGUI never unlocks on its own.
4
5## Behavior
6
7- One Wayland connection, seat and reader serve every lock surface. One lock surface is created per output present at acquisition; outputs added later get a black surface, removed outputs lose theirs.
8- Rendering uses the normal Vulkan, DMA-BUF and explicit-sync path only: no shared-memory buffer, no readback, no implicit-sync fallback. A surface allocates and attaches a buffer only after its configure was acknowledged and its dmabuf feedback round completed (`Window.FeedbackDone`).
9- Exactly one output hosts the view: the one whose surface has keyboard focus, initially the first. Every other output shows plain black (an empty frame).
10- `OnLocked` fires when the compositor sends `locked`. `finished` (refused or ended) makes `RunLock` return `ErrLockFinished`; the lock is then destroyed with a plain `destroy`, never an unlock.
11- Verification is the caller's and asynchronous: `OnSubmit` never blocks and never unlocks. Unlock happens only when the caller sends on `LockConfig.Unlock` (after its own verification); a request made before `locked` is held until `locked`, closing the channel is not a request. Then: `unlock_and_destroy`, a display roundtrip, and the connection closes. The caller shows progress with `LockConfig.Status` (`LockIdle`, `LockBusy`, `LockFailed`), surfaced as `LockState.Status`. Cancellation or any error closes the connection without unlocking, which keeps the session locked.
12
13## Configuration
14
15- `Display` is the Wayland socket name; empty uses the environment.
16- `Wake` requests a redraw for each value received. Its forwarder is scoped to `RunLock` and stops when it returns.
17- `OnOutputError(output, err)` reports, with the connector name, an output added after acquisition that could not get a lock surface or render path. Such hotplug failures are skipped (the compositor keeps the output black); failures on outputs present at acquisition abort `RunLock`.
18- `NewSecretBuffer` panics unless the capacity is between 1 and 4096 bytes. `RunLock` wipes `Secret` when it returns, and the buffer is wiped after `OnSubmit` even if it panics.
19- Outputs are promoted in ascending global order, so the first host is deterministic.
20- Keyboard focus-out drops held keys and ignores key presses until focus returns; typed bytes stay in `Secret`, and input continues when any lock surface regains focus (that surface becomes the host).
21
22## Secret input
23
24Typed keys go through the keyboard secret path (`EventSecret`/`TickSecret`) into a caller-owned `SecretBuffer`. They never become a Go string, and never enter the model, the frame tree, render text, IME, pre-edit or the clipboard (lock surfaces bind neither). The view receives `LockState.Mask`, the number of typed code points, and draws bullets.
25
26Enter calls `OnSubmit` (after `locked` only; the buffer is wiped right after it returns, so it must copy or consume the slice and must not block), Escape and Ctrl+U clear, Backspace removes the last code point. Before `locked`, Enter only clears. Scratch buffers are zeroed after every key.
27
28## Example
29
30```go
31secret := nefergui.NewSecretBuffer(256)
32defer secret.Wipe()
33err := nefergui.RunLock(ctx, nefergui.LockConfig{
34 Secret: secret,
35 Styles: "lock.css",
36 View: func(f *nefergui.Frame, s nefergui.LockState) {
37 box := f.Root(nefergui.Class("screen")).Column(nefergui.Class("box"))
38 box.Text(s.Bullets(), nefergui.Key("mask"))
39 },
40 Unlock: unlock, // chan struct{}: send after verification succeeded
41 Status: status, // chan nefergui.LockStatus: LockBusy / LockFailed / LockIdle
42 OnSubmit: func(pw []byte) { // copy or consume pw now; it is wiped on return
43 go verify(bytes.Clone(pw), unlock, status)
44 },
45})
46```
47
48## Verification
49
50Wire tests cover the shared connection (reader pause and queue saturation, keyboard and pointer generations, feedback completion). `NEFERGUI_HARNESS=1` with a NeferWL binary in `NEFERGUI_NEFERWL` runs the headless lock tests (`TestHarnessConnectionLock`, `TestHarnessRunLock`, `TestHarnessRunLockRefused`).