v0.4.0 2de4e1d

session-lock.md RawHistoryFolder

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).

Secret input

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`).

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close