v0.4.0 b6be3a8

globals.go RawHistoryFolder

1package wayland
2
3import (
4	"os"
5	"slices"
6
7	"github.com/bnema/go-wayland-bindings/server/wayland"
8	"github.com/bnema/neferwl/internal/ports"
9	"github.com/bnema/purego-libwayland/server"
10)
11
12func registerGlobals(d *server.Display, o Options, s *Server) error {
13	for _, register := range []func() error{
14		func() error { return registerXDG(d, s) },
15		func() error { return registerLayer(d, s) },
16		func() error {
17			// Advertise only with the production owner gate and authoritative
18			// backend proof/barrier channels. Standalone adapters cannot claim
19			// protection from composed-frame submission alone.
20			if s.securityController == nil || s.channels.SecurityChanges == nil || s.channels.SecurityEvents == nil {
21				return nil
22			}
23			return registerSessionLock(d, s)
24		},
25		func() error { return registerXDGOutput(d, s) },
26		func() error { return registerOutputManagement(d, s) },
27		func() error { return registerWorkspaces(d, s) },
28		func() error { return registerForeignToplevel(d, s) },
29		func() error { return registerExtForeignToplevel(d, s) },
30		func() error { return registerDecoration(d, s) },
31		func() error { return registerScale(d, s) },
32		func() error { return registerCursorShape(d, s) },
33		func() error { return registerDMABuf(d, s, o.DMABuf) },
34		func() error { return registerSinglePixelBuffer(d, s) },
35		func() error { return registerPointerConstraints(d, s) },
36		func() error { return registerTearing(d, s) },
37		func() error { return registerColorManagement(d, s) },
38		func() error { return registerColorRepresentation(d, s) },
39		func() error { return registerPresentationConstraints(d, s) },
40		func() error { return registerPresentation(d, s) },
41		func() error { return registerSyncobj(d, s) },
42		func() error { return registerInhibit(d, s) },
43		func() error { return registerIdle(d, s) },
44		func() error { return registerCapture(d, s) },
45		func() error { return registerExclusion(d, s) },
46		func() error { return registerSecurityContext(d, s) },
47		func() error { return registerActivation(d, s) },
48		func() error { return registerForeign(d, s) },
49		func() error { return registerVirtualKeyboard(d, s) },
50		func() error { return registerTextInput(d, s) },
51		func() error { return registerInputMethod(d, s) },
52		func() error {
53			return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })
54		},
55		func() error {
56			return wayland.NewSubcompositorGlobal(d, 1, func(c server.Client, v, id uint32) {
57				wayland.NewSubcompositor(c, int32(v), id, subcompositor{s})
58			})
59		},
60		func() error {
61			return wayland.NewShmGlobal(d, 1, func(c server.Client, v, id uint32) {
62				r, e := wayland.NewShm(c, int32(v), id, shm{s})
63				if e == nil {
64					r.SendFormat(0)
65					r.SendFormat(1)
66				}
67			})
68		},
69		func() error {
70			return wayland.NewSeatGlobal(d, 8, func(c server.Client, v, id uint32) {
71				r, e := wayland.NewSeat(c, int32(v), id, seat{s})
72				if e == nil {
73					capabilities := uint32(wayland.SeatCapabilityPointer)
74					if s.seat.keymapFD >= 0 {
75						capabilities |= uint32(wayland.SeatCapabilityKeyboard)
76					}
77					r.SendCapabilities(capabilities)
78					if v >= 2 {
79						r.SendName("seat0")
80					}
81				}
82			})
83		},
84		func() error { return registerInputTimestamps(d, s) },
85		func() error { return registerClipboard(d, s) },
86		func() error { return registerFixes(d) },
87		func() error { return registerAlphaModifier(d, s) },
88	} {
89		if err := register(); err != nil {
90			return err
91		}
92	}
93	return nil
94}
95
96// registerFixes advertises wl_fixes: clients can destroy their registry,
97// and acknowledge removed globals (v2, libwayland 1.26+).
98func registerFixes(d *server.Display) error {
99	version := int32(1)
100	if server.AckGlobalRemove(nil, nil, 0) {
101		version = 2
102	}
103	return wayland.NewFixesGlobal(d, version, func(c server.Client, v, id uint32) {
104		_, _ = wayland.NewFixes(c, int32(v), id, fixes{})
105	})
106}
107
108type fixes struct{}
109
110func (fixes) Destroy(*wayland.Fixes) {}
111func (fixes) DestroyRegistry(_ *wayland.Fixes, r *wayland.Registry) {
112	if r != nil && r.Resource != nil {
113		r.Destroy()
114	}
115}
116func (fixes) AckGlobalRemove(f *wayland.Fixes, r *wayland.Registry, name uint32) {
117	if r != nil {
118		server.AckGlobalRemove(f.Resource, r.Resource, name)
119	}
120}
121
122type compositor struct{ server *Server }
123
124func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) {
125	state := &surface{server: c.server, bufferScale: 1, inputAll: true}
126	if w, err := wayland.NewSurface(r.Client(), r.Version(), id, state); err == nil {
127		c.server.nextSurface++
128		state.identity = c.server.nextSurface
129		state.wl = w
130		c.server.surfaces[w.Resource] = state
131		state.sendScale()
132		w.OnDestroy = func() { delete(c.server.surfaces, w.Resource); state.Destroy(w) }
133	}
134}
135func (c compositor) CreateRegion(r *wayland.Compositor, id uint32) {
136	state := &region{}
137	if w, err := wayland.NewRegion(r.Client(), r.Version(), id, state); err == nil {
138		c.server.regions[w.Resource] = state
139		w.OnDestroy = func() { delete(c.server.regions, w.Resource) }
140	}
141}
142func (compositor) Release(*wayland.Compositor) {}
143
144type subcompositor struct{ server *Server }
145
146func (subcompositor) Destroy(*wayland.Subcompositor) {}
147func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, parent *wayland.Surface) {
148	if w == nil || parent == nil {
149		return
150	}
151	state, up := c.server.surfaces[w.Resource], c.server.surfaces[parent.Resource]
152	if state == nil || up == nil {
153		return
154	}
155	if state.kind != roleNone && state.kind != roleSubsurface || state.sub.role != nil {
156		r.PostError(uint32(wayland.SubcompositorErrorBadSurface), "surface already has role")
157		return
158	}
159	// A surface cannot be its own ancestor.
160	for p := up; p != nil; p = p.sub.parent {
161		if p == state {
162			r.PostError(uint32(wayland.SubcompositorErrorBadParent), "parent is a descendant")
163			return
164		}
165	}
166	sub, err := wayland.NewSubsurface(r.Client(), 1, id, subsurface{state})
167	if err != nil {
168		return
169	}
170	state.kind = roleSubsurface
171	state.role = func(bool) {}
172	state.sub = subState{role: sub, parent: up, children: state.sub.children, layout: state.sub.layout, pendingLayout: state.sub.pendingLayout, synced: true}
173	up.sub.children = append(up.sub.children, state)
174	up.sub.pendingLayout = append(append([]childLayout(nil), up.sub.pendingLayout...), childLayout{child: state})
175	// The child follows its root window's output and scale.
176	state.sendScale()
177	up.emitInput()
178	sub.OnDestroy = func() {
179		if state.sub.role != sub {
180			return
181		}
182		state.sub.role = nil
183		state.role = nil
184		state.detach()
185	}
186}
187
188// Subsurfaces are drawn as their own commits arrive (desynchronized): the
189// synchronized mode's cached state is not implemented.
190type subsurface struct{ surface *surface }
191
192func (subsurface) Destroy(*wayland.Subsurface) {}
193func (s subsurface) SetPosition(_ *wayland.Subsurface, x, y int32) {
194	p := s.surface.sub.parent
195	if p == nil {
196		return
197	}
198	p.sub.pendingLayout = append([]childLayout(nil), p.sub.pendingLayout...)
199	for i := range p.sub.pendingLayout {
200		item := &p.sub.pendingLayout[i]
201		if item.child == s.surface {
202			item.x, item.y = int(x), int(y)
203			break
204		}
205	}
206}
207func (s subsurface) PlaceAbove(r *wayland.Subsurface, sibling *wayland.Surface) {
208	s.restack(r, sibling, true)
209}
210func (s subsurface) PlaceBelow(r *wayland.Subsurface, sibling *wayland.Surface) {
211	s.restack(r, sibling, false)
212}
213func (s subsurface) SetSync(*wayland.Subsurface) { s.surface.sub.synced = true }
214func (s subsurface) SetDesync(*wayland.Subsurface) {
215	was := s.surface.effectivelySynced()
216	s.surface.sub.synced = false
217	if was && !s.surface.effectivelySynced() {
218		s.surface.flushDesync()
219	}
220}
221
222// restack changes only the pending layout of the parent.
223func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, above bool) {
224	me := s.surface
225	p := me.sub.parent
226	if p == nil || sibling == nil {
227		return
228	}
229	other := me.server.surfaces[sibling.Resource]
230	if other == me || other == nil || other != p && other.sub.parent != p {
231		r.PostError(uint32(wayland.SubsurfaceErrorBadSurface), "not a sibling or the parent")
232		return
233	}
234	list := append([]childLayout(nil), p.sub.pendingLayout...)
235	var item childLayout
236	for _, entry := range list {
237		if entry.child == me {
238			item = entry
239			break
240		}
241	}
242	list = removeLayout(list, me)
243	at := len(list)
244	if other == p {
245		item.below = !above
246		if above {
247			for i, entry := range list {
248				if !entry.below {
249					at = i
250					break
251				}
252			}
253		}
254	} else {
255		for i, entry := range list {
256			if entry.child == other {
257				at, item.below = i, entry.below
258				if above {
259					at++
260				}
261				break
262			}
263		}
264	}
265	list = append(list, childLayout{})
266	copy(list[at+1:], list[at:])
267	list[at] = item
268	p.sub.pendingLayout = list
269}
270
271// region stores the exact union of added rectangles.
272type region struct{ rects []ports.Rect }
273
274func (*region) Destroy(*wayland.Region) {}
275func (g *region) Add(_ *wayland.Region, x, y, w, h int32) {
276	if w > 0 && h > 0 {
277		g.rects = append(g.rects, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)})
278	}
279}
280func (g *region) Subtract(_ *wayland.Region, x, y, w, h int32) {
281	if w <= 0 || h <= 0 {
282		return
283	}
284	cut := ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}
285	var out []ports.Rect
286	for _, r := range g.rects {
287		i := intersectRect(r, cut)
288		if i.W == 0 {
289			out = append(out, r)
290			continue
291		}
292		for _, piece := range []ports.Rect{
293			{X: r.X, Y: r.Y, W: r.W, H: i.Y - r.Y},
294			{X: r.X, Y: i.Y + i.H, W: r.W, H: r.Y + r.H - i.Y - i.H},
295			{X: r.X, Y: i.Y, W: i.X - r.X, H: i.H},
296			{X: i.X + i.W, Y: i.Y, W: r.X + r.W - i.X - i.W, H: i.H},
297		} {
298			if piece.W > 0 && piece.H > 0 {
299				out = append(out, piece)
300			}
301		}
302	}
303	g.rects = out
304}
305
306type shm struct{ server *Server }
307
308func (h shm) CreatePool(r *wayland.Shm, id uint32, fd int, size int32) {
309	file := os.NewFile(uintptr(fd), "wl_shm_pool")
310	if size <= 0 || !fileHolds(file, int64(size)) {
311		file.Close()
312		r.PostError(uint32(wayland.ShmErrorInvalidFd), "invalid pool size")
313		return
314	}
315	h.server.nextPool++
316	state := &pool{id: h.server.nextPool, file: file, size: size, shm: r, server: h.server}
317	p, err := wayland.NewShmPool(r.Client(), 1, id, state)
318	if err != nil {
319		file.Close()
320		return
321	}
322	p.OnDestroy = func() {
323		state.destroyed = true
324		if state.refs == 0 {
325			state.file.Close()
326		}
327	}
328}
329func (shm) Release(*wayland.Shm) {}
330
331// fileHolds reports whether the file has at least size bytes.
332func fileHolds(f *os.File, size int64) bool {
333	st, err := f.Stat()
334	return err == nil && st.Size() >= size
335}
336
337// pool is a wl_shm pool. neferwl never maps it: renderers map the file
338// and read the pixels when they draw (ports.SHMBuffer).
339type pool struct {
340	id        uint64
341	file      *os.File
342	size      int32
343	shm       *wayland.Shm
344	server    *Server
345	refs      int
346	destroyed bool
347}
348
349func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, offset, width, height, stride int32, format uint32) {
350	if format != uint32(wayland.ShmFormatArgb8888) && format != uint32(wayland.ShmFormatXrgb8888) {
351		p.shm.PostError(uint32(wayland.ShmErrorInvalidFormat), "unsupported format")
352		return
353	}
354	if width <= 0 || height <= 0 || int64(stride) < int64(width)*4 || offset < 0 || stride <= 0 || int64(offset)+int64(height-1)*int64(stride)+int64(width)*4 > int64(p.size) {
355		p.shm.PostError(uint32(wayland.ShmErrorInvalidStride), "invalid buffer dimensions")
356		return
357	}
358	state := &buffer{pool: p, offset: int(offset), width: int(width), height: int(height), stride: int(stride), format: format}
359	b, err := wayland.NewBuffer(r.Client(), 1, id, state)
360	if err != nil {
361		return
362	}
363	p.refs++
364	p.server.buffers[b.Resource] = state
365	b.OnDestroy = func() {
366		delete(p.server.buffers, b.Resource)
367		p.refs--
368		if p.destroyed && p.refs == 0 {
369			p.file.Close()
370		}
371	}
372}
373func (p *pool) Destroy(*wayland.ShmPool) {}
374func (p *pool) Resize(r *wayland.ShmPool, size int32) {
375	if size <= p.size {
376		p.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "pool must grow")
377		return
378	}
379	if !fileHolds(p.file, int64(size)) {
380		p.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "cannot resize pool")
381		return
382	}
383	p.size = size
384}
385
386type buffer struct {
387	pool                          *pool
388	offset, width, height, stride int
389	format                        uint32
390}
391
392func (*buffer) Destroy(*wayland.Buffer) {}
393
394func (b *buffer) size() (int, int) { return b.width, b.height }
395
396// clientBuffer is a wl_buffer's content source: wl_shm, linux-dmabuf or
397// single-pixel buffers.
398type clientBuffer interface {
399	// content returns what the renderer draws; false means the client
400	// broke its buffer (a truncated shm file).
401	content(ports.WindowID) (ports.SurfaceContent, bool)
402	size() (int, int)
403}
404
405// addBuffer tracks a buffer until the client destroys it.
406// A buffer that owns resources implements close() to release them.
407func (s *Server) addBuffer(r *wayland.Buffer, b clientBuffer) {
408	s.buffers[r.Resource] = b
409	r.OnDestroy = func() {
410		delete(s.buffers, r.Resource)
411		if c, ok := b.(interface{ close() }); ok {
412			c.close()
413		}
414	}
415}
416
417// content points the renderer at the pool; a file shrunk below the buffer
418// is refused.
419func (b *buffer) content(id ports.WindowID) (ports.SurfaceContent, bool) {
420	if !fileHolds(b.pool.file, int64(b.offset+(b.height-1)*b.stride+b.width*4)) {
421		return ports.SurfaceContent{}, false
422	}
423	shm := &ports.SHMBuffer{Pool: b.pool.id, File: b.pool.file, Offset: b.offset, Stride: b.stride}
424	return ports.SurfaceContent{ID: id, Width: b.width, Height: b.height, Opaque: b.format == uint32(wayland.ShmFormatXrgb8888), SHM: shm}, true
425}
426
427type seat struct{ server *Server }
428
429func (h seat) GetPointer(r *wayland.Seat, id uint32) {
430	p, err := wayland.NewPointer(r.Client(), r.Version(), id, pointer(h))
431	if err != nil {
432		return
433	}
434	s := h.server
435	s.seat.pointers[r.Client()] = append(s.seat.pointers[r.Client()], p)
436	p.OnDestroy = func() {
437		delete(s.seat.enters, p.Resource)
438		// Subscribed timestamps objects become inert with the device.
439		delete(s.seat.pointerStamps, p.Resource)
440		// Copy on write: callers may be ranging over the old list (see
441		// clientPointers).
442		list := slices.DeleteFunc(slices.Clone(s.seat.pointers[r.Client()]), func(x *wayland.Pointer) bool { return x == p })
443		if len(list) == 0 {
444			delete(s.seat.pointers, r.Client())
445		} else {
446			s.seat.pointers[r.Client()] = list
447		}
448	}
449	if s.hasPointerFocus(r.Client()) {
450		if surf, _, x, y := s.pointerSurface(s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY); surf != nil {
451			s.serial++
452			s.seat.enters[p.Resource] = s.serial
453			p.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y))
454			pointerFrame(p)
455		}
456	}
457}
458func (h seat) GetKeyboard(r *wayland.Seat, id uint32) {
459	k, err := wayland.NewKeyboard(r.Client(), r.Version(), id, keyboard{})
460	if err != nil {
461		return
462	}
463	s := h.server
464	k.OnDestroy = func() {
465		delete(s.seat.keyStamps, k.Resource)
466		// Copy on write: callers may be ranging over the old list (see
467		// clientKeyboards).
468		list := slices.DeleteFunc(slices.Clone(s.seat.keyboards[r.Client()]), func(x *wayland.Keyboard) bool { return x == k })
469		if len(list) == 0 {
470			delete(s.seat.keyboards, r.Client())
471		} else {
472			s.seat.keyboards[r.Client()] = list
473		}
474	}
475	if s.seat.keymapFD < 0 {
476		return
477	}
478	s.seat.keyboards[r.Client()] = append(s.seat.keyboards[r.Client()], k)
479	fd, size := s.currentKeymap()
480	k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size)
481	if r.Version() >= 4 {
482		k.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
483	}
484	if surf, _ := s.focusTarget(s.seat.focused); surf != nil && surf.Client() == r.Client() {
485		s.serial++
486		k.SendEnter(s.serial, surf, []byte{})
487		s.sendModifiers(k)
488	}
489}
490func (seat) GetTouch(r *wayland.Seat, id uint32) {
491	wayland.NewTouch(r.Client(), r.Version(), id, touch{})
492}
493func (seat) Release(*wayland.Seat) {}
494
495type keyboard struct{}
496
497func (keyboard) Release(*wayland.Keyboard) {}
498
499type touch struct{}
500
501func (touch) Release(*wayland.Touch) {}

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 25

Tags 10

↑↓ moveenter openesc close