main 7c2aaeb

main.go RawHistoryFolder

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}

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 4

↑↓ moveenter openesc close