v0.4.0 b6be3a8

foreigntoplevel.go RawHistoryFolder

1package wayland
2
3import (
4	"bytes"
5	"encoding/binary"
6	"slices"
7
8	"github.com/bnema/go-wayland-bindings/server/wayland"
9	wlr "github.com/bnema/go-wayland-bindings/server/wlrforeigntoplevelmanagement"
10	"github.com/bnema/neferwl/internal/ports"
11	"github.com/bnema/purego-libwayland/server"
12)
13
14// wlr-foreign-toplevel-management lists mapped toplevels for taskbars and
15// notification daemons (Dunst reads the fullscreen state). Every client
16// may bind it, as on other compositors: it reveals less than screencopy.
17// Requests reuse the messages of xdg-shell and xdg-activation.
18
19type toplevelManager struct {
20	s       *Server
21	res     *wlr.ZwlrForeignToplevelManagerV1
22	handles map[ports.WindowID]*toplevelHandle
23}
24
25// toplevelHandle is one window seen by one manager, with what it was last
26// sent so that only changes go out.
27type toplevelHandle struct {
28	res          *wlr.ZwlrForeignToplevelHandleV1
29	req          *toplevelRequests
30	title, appID string
31	state        []byte
32	outputs      map[*server.Resource]bool // output_enter sent
33}
34
35func registerForeignToplevel(d *server.Display, s *Server) error {
36	return wlr.NewZwlrForeignToplevelManagerV1Global(d, 3, func(c server.Client, v, id uint32) {
37		m := &toplevelManager{s: s, handles: map[ports.WindowID]*toplevelHandle{}}
38		r, err := wlr.NewZwlrForeignToplevelManagerV1(c, int32(v), id, m)
39		if err != nil {
40			return
41		}
42		m.res = r
43		s.toplevelManagers = append(s.toplevelManagers, m)
44		r.OnDestroy = func() { s.toplevelManagers = removeItem(s.toplevelManagers, m) }
45		ids := make([]ports.WindowID, 0, len(s.windows))
46		for id, w := range s.windows {
47			if w.toplevel != nil && w.mapped {
48				ids = append(ids, id)
49			}
50		}
51		slices.Sort(ids)
52		for _, id := range ids {
53			m.refresh(s.windows[id])
54		}
55	})
56}
57
58func (m *toplevelManager) Stop(*wlr.ZwlrForeignToplevelManagerV1) {
59	m.res.SendFinished()
60	m.res.Destroy()
61}
62
63// refresh announces w or sends what changed since the last done.
64func (m *toplevelManager) refresh(w *window) {
65	if m.s.protected() || !m.res.Alive() {
66		return
67	}
68	h := m.handles[w.id]
69	if h != nil && h.req.closed {
70		m.closed(w.id)
71		h = nil
72	}
73	fresh := h == nil
74	if fresh {
75		req := &toplevelRequests{s: m.s, id: w.id}
76		r, err := wlr.NewZwlrForeignToplevelHandleV1(m.res.Client(), m.res.Version(), 0, req)
77		if err != nil {
78			return
79		}
80		h = &toplevelHandle{res: r, req: req, outputs: map[*server.Resource]bool{}}
81		m.handles[w.id] = h
82		m.res.SendToplevel(r)
83	}
84	if !h.res.Alive() {
85		return
86	}
87	changed := fresh
88	if fresh || h.title != w.title {
89		h.title = w.title
90		h.res.SendTitle(w.title)
91		changed = true
92	}
93	if fresh || h.appID != w.appID {
94		h.appID = w.appID
95		h.res.SendAppId(w.appID)
96		changed = true
97	}
98	if state := toplevelState(w, h.res.Version()); fresh || !bytes.Equal(h.state, state) {
99		h.state = state
100		h.res.SendState(state)
101		changed = true
102	}
103	if h.syncOutputs(m.s, m.res.Client(), w) {
104		changed = true
105	}
106	if changed {
107		h.res.SendDone()
108	}
109}
110
111// closed sends closed for w; the client then destroys the handle.
112func (m *toplevelManager) closed(id ports.WindowID) {
113	h := m.handles[id]
114	if h == nil {
115		return
116	}
117	// A remapped window keeps its ID but gets a new handle. Mark requests
118	// inert immediately, but defer the wire event to avoid leaking lock-time
119	// unmap/disconnect timing. Keep the handle for the unlock refresh.
120	h.req.closed = true
121	if m.s.protected() {
122		return
123	}
124	delete(m.handles, id)
125	if h.res.Alive() {
126		h.res.SendClosed()
127	}
128}
129
130// syncOutputs sends output_enter for every wl_output the client bound for
131// the window's output and output_leave for the others.
132func (h *toplevelHandle) syncOutputs(s *Server, c server.Client, w *window) bool {
133	want := map[*server.Resource]bool{}
134	if o := s.outputByNameExact(w.last.Output); o != nil && w.hasLast {
135		for _, r := range o.resources {
136			if r.Client() == c && r.Alive() {
137				want[r.Resource] = true
138			}
139		}
140	}
141	changed := false
142	for r := range h.outputs {
143		if !want[r] {
144			delete(h.outputs, r)
145			if r.Alive() {
146				h.res.SendOutputLeave(wayland.WrapOutput(r))
147				changed = true
148			}
149		}
150	}
151	for r := range want {
152		if !h.outputs[r] {
153			h.outputs[r] = true
154			h.res.SendOutputEnter(wayland.WrapOutput(r))
155			changed = true
156		}
157	}
158	return changed
159}
160
161// toplevelState mirrors the xdg_toplevel states of the last configure:
162// tiled windows are maximized. Fullscreen exists since version 2.
163func toplevelState(w *window, version int32) []byte {
164	if !w.hasLast {
165		return []byte{}
166	}
167	c := w.last
168	var states []byte
169	add := func(st wlr.ZwlrForeignToplevelHandleV1State) {
170		states = binary.NativeEndian.AppendUint32(states, uint32(st))
171	}
172	if !c.Fullscreen && !c.Floating {
173		add(wlr.ZwlrForeignToplevelHandleV1StateMaximized)
174	}
175	if c.Activated {
176		add(wlr.ZwlrForeignToplevelHandleV1StateActivated)
177	}
178	if c.Fullscreen && version >= 2 {
179		add(wlr.ZwlrForeignToplevelHandleV1StateFullscreen)
180	}
181	if states == nil {
182		return []byte{}
183	}
184	return states
185}
186
187// toplevelChanged updates every manager after w mapped or changed.
188func (s *Server) toplevelChanged(w *window) {
189	if w.toplevel == nil || !w.mapped {
190		return
191	}
192	for _, m := range s.toplevelManagers {
193		m.refresh(w)
194	}
195	for _, l := range s.extToplevelLists {
196		l.refresh(w)
197	}
198}
199
200// toplevelClosed tells every manager that w unmapped.
201func (s *Server) toplevelClosed(id ports.WindowID) {
202	for _, m := range s.toplevelManagers {
203		m.closed(id)
204	}
205	for _, l := range s.extToplevelLists {
206		l.closed(id)
207	}
208	s.windowCaptureGone(id)
209}
210
211// refreshToplevels reconciles inventory after output changes or unlock:
212// retire handles closed during protection, then announce current windows,
213// including those mapped (or managers bound) while inventory was suppressed.
214func (s *Server) refreshToplevels() {
215	if s.protected() {
216		return
217	}
218	ids := s.mappedToplevels()
219	for _, l := range s.extToplevelLists {
220		for id, h := range l.handles {
221			w := s.windows[id]
222			if h.closed || w == nil || w.toplevel == nil || !w.mapped {
223				l.closed(id)
224			}
225		}
226		for _, id := range ids {
227			l.refresh(s.windows[id])
228		}
229	}
230	for _, m := range s.toplevelManagers {
231		for id, h := range m.handles {
232			w := s.windows[id]
233			if h.req.closed || w == nil || w.toplevel == nil || !w.mapped {
234				m.closed(id)
235			}
236		}
237		for _, id := range ids {
238			m.refresh(s.windows[id])
239		}
240	}
241}
242
243// toplevelRequests handles requests on one handle. Requests on a closed
244// handle are ignored.
245type toplevelRequests struct {
246	s      *Server
247	id     ports.WindowID
248	closed bool
249}
250
251func (t *toplevelRequests) window() *window {
252	if t.s.protected() || t.closed {
253		return nil
254	}
255	if w := t.s.windows[t.id]; w != nil && w.toplevel != nil && w.mapped && w.toplevel.Resource.Alive() {
256		return w
257	}
258	return nil
259}
260
261// Tiled windows are always maximized and nothing minimizes: these are no-ops.
262func (*toplevelRequests) SetMaximized(*wlr.ZwlrForeignToplevelHandleV1)   {}
263func (*toplevelRequests) UnsetMaximized(*wlr.ZwlrForeignToplevelHandleV1) {}
264func (*toplevelRequests) SetMinimized(*wlr.ZwlrForeignToplevelHandleV1)   {}
265func (*toplevelRequests) UnsetMinimized(*wlr.ZwlrForeignToplevelHandleV1) {}
266func (*toplevelRequests) Destroy(*wlr.ZwlrForeignToplevelHandleV1)        {}
267
268func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Seat) {
269	if w := t.window(); w != nil {
270		t.s.emit(ports.WindowActivate{ID: w.id})
271	}
272}
273func (t *toplevelRequests) Close(*wlr.ZwlrForeignToplevelHandleV1) {
274	if w := t.window(); w != nil {
275		w.toplevel.SendClose()
276	}
277}
278func (t *toplevelRequests) SetRectangle(r *wlr.ZwlrForeignToplevelHandleV1, _ *wayland.Surface, _, _, w, h int32) {
279	if !t.closed && (w < 0 || h < 0) {
280		r.PostError(uint32(wlr.ZwlrForeignToplevelHandleV1ErrorInvalidRectangle), "negative rectangle size")
281	}
282}
283func (t *toplevelRequests) SetFullscreen(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Output) {
284	if w := t.window(); w != nil {
285		t.s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: true, External: true})
286	}
287}
288func (t *toplevelRequests) UnsetFullscreen(*wlr.ZwlrForeignToplevelHandleV1) {
289	if w := t.window(); w != nil {
290		t.s.emit(ports.WindowFullscreenRequest{ID: w.id, External: true})
291	}
292}

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