detached 02045ff

fix(wayland): acknowledge the KDE decoration mode a client requests

Answering server-side to every request_mode made Firefox, which asks for
client-side, request again in a tight loop: 40% CPU with Firefox idle.
The requested mode is now echoed as the protocol describes; undefined
modes are ignored. The server default still removes GTK4 title bars.
commit
02045ff9bb87c3d31bb3835f683d60c1b4bbf777
author
brice · · 2026-09-25 08:24 UTC
parent
cc04d40
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +67 -3
M internal/adapters/wayland/decoration.go +10 -3
@@ -41,9 +41,16 @@ type kdeDecoration struct{}
4141
4242 func (kdeDecoration) Release(*kdedecoration.OrgKdeKwinServerDecoration) {}
4343
44-// Any requested mode is answered with server-side, like xdg-decoration.
45-func (kdeDecoration) RequestMode(r *kdedecoration.OrgKdeKwinServerDecoration, _ uint32) {
46- r.SendMode(uint32(kdedecoration.OrgKdeKwinServerDecorationModeServer))
44+// A requested mode is acknowledged as is: the protocol has no way to
45+// refuse, and answering server to a client that insists on client-side
46+// (Firefox) makes it ask again forever. The server default already makes
47+// GTK drop its title bar.
48+// Unknown modes are ignored: replacing them would restart the loop.
49+func (kdeDecoration) RequestMode(r *kdedecoration.OrgKdeKwinServerDecoration, mode uint32) {
50+ if mode > uint32(kdedecoration.OrgKdeKwinServerDecorationModeServer) {
51+ return
52+ }
53+ r.SendMode(mode)
4754 }
4855
4956 func (decorationManager) Destroy(*xdgdecoration.ZxdgDecorationManagerV1) {}
@@ -41,9 +41,16 @@ type kdeDecoration struct{}
41 41
42 func (kdeDecoration) Release(*kdedecoration.OrgKdeKwinServerDecoration) {}42 func (kdeDecoration) Release(*kdedecoration.OrgKdeKwinServerDecoration) {}
43 43
44-// Any requested mode is answered with server-side, like xdg-decoration.44+// A requested mode is acknowledged as is: the protocol has no way to
45-func (kdeDecoration) RequestMode(r *kdedecoration.OrgKdeKwinServerDecoration, _ uint32) {45+// refuse, and answering server to a client that insists on client-side
46- r.SendMode(uint32(kdedecoration.OrgKdeKwinServerDecorationModeServer))46+// (Firefox) makes it ask again forever. The server default already makes
47+// GTK drop its title bar.
48+// Unknown modes are ignored: replacing them would restart the loop.
49+func (kdeDecoration) RequestMode(r *kdedecoration.OrgKdeKwinServerDecoration, mode uint32) {
50+ if mode > uint32(kdedecoration.OrgKdeKwinServerDecorationModeServer) {
51+ return
52+ }
53+ r.SendMode(mode)
47 }54 }
48 55
49 func (decorationManager) Destroy(*xdgdecoration.ZxdgDecorationManagerV1) {}56 func (decorationManager) Destroy(*xdgdecoration.ZxdgDecorationManagerV1) {}
A internal/adapters/wayland/decoration_test.go +57 -0
@@ -0,0 +1,57 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+ "github.com/bnema/purego-libwayland/protocol/kdedecoration"
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
10+ "github.com/bnema/wlturbo"
11+)
12+
13+type kdeModeProxy struct {
14+ wlturbo.BaseProxy
15+ modes chan uint32
16+}
17+
18+func (p *kdeModeProxy) Dispatch(e *wlturbo.Event) { p.modes <- e.Uint32() }
19+
20+// KDE decoration defaults to server-side and acknowledges the mode a client
21+// asks for: answering server to Firefox's client request made it ask again
22+// in a tight loop.
23+func TestKDEDecorationAcknowledgesMode(t *testing.T) {
24+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{})
25+ c := protocolClient(t, s, dir)
26+ comp := bindProtocol(t, c, "wl_compositor")
27+ mgr := bindProtocol(t, c, "org_kde_kwin_server_decoration_manager")
28+ registerProtocol(t, c, mgr)
29+ surf := c.AllocateID()
30+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
31+ deco := c.AllocateID()
32+ proxy := &kdeModeProxy{modes: make(chan uint32, 8)}
33+ proxy.SetID(deco)
34+ c.Context().Register(proxy)
35+ requestProtocol(t, c, mgr, kdedecoration.OrgKdeKwinServerDecorationManagerRequestCreate, deco, surf)
36+ requestProtocol(t, c, deco, kdedecoration.OrgKdeKwinServerDecorationRequestRequestMode, uint32(kdedecoration.OrgKdeKwinServerDecorationModeClient))
37+ // An undefined mode gets no answer.
38+ requestProtocol(t, c, deco, kdedecoration.OrgKdeKwinServerDecorationRequestRequestMode, uint32(3))
39+ if err := c.Roundtrip(); err != nil {
40+ t.Fatal(err)
41+ }
42+ for _, want := range []kdedecoration.OrgKdeKwinServerDecorationMode{kdedecoration.OrgKdeKwinServerDecorationModeServer, kdedecoration.OrgKdeKwinServerDecorationModeClient} {
43+ select {
44+ case got := <-proxy.modes:
45+ if got != uint32(want) {
46+ t.Fatalf("mode %d want %d", got, want)
47+ }
48+ case <-time.After(2 * time.Second):
49+ t.Fatalf("no mode %d", want)
50+ }
51+ }
52+ select {
53+ case got := <-proxy.modes:
54+ t.Fatalf("extra mode %d", got)
55+ default:
56+ }
57+}
@@ -0,0 +1,57 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+ "github.com/bnema/purego-libwayland/protocol/kdedecoration"
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
10+ "github.com/bnema/wlturbo"
11+)
12+
13+type kdeModeProxy struct {
14+ wlturbo.BaseProxy
15+ modes chan uint32
16+}
17+
18+func (p *kdeModeProxy) Dispatch(e *wlturbo.Event) { p.modes <- e.Uint32() }
19+
20+// KDE decoration defaults to server-side and acknowledges the mode a client
21+// asks for: answering server to Firefox's client request made it ask again
22+// in a tight loop.
23+func TestKDEDecorationAcknowledgesMode(t *testing.T) {
24+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{})
25+ c := protocolClient(t, s, dir)
26+ comp := bindProtocol(t, c, "wl_compositor")
27+ mgr := bindProtocol(t, c, "org_kde_kwin_server_decoration_manager")
28+ registerProtocol(t, c, mgr)
29+ surf := c.AllocateID()
30+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
31+ deco := c.AllocateID()
32+ proxy := &kdeModeProxy{modes: make(chan uint32, 8)}
33+ proxy.SetID(deco)
34+ c.Context().Register(proxy)
35+ requestProtocol(t, c, mgr, kdedecoration.OrgKdeKwinServerDecorationManagerRequestCreate, deco, surf)
36+ requestProtocol(t, c, deco, kdedecoration.OrgKdeKwinServerDecorationRequestRequestMode, uint32(kdedecoration.OrgKdeKwinServerDecorationModeClient))
37+ // An undefined mode gets no answer.
38+ requestProtocol(t, c, deco, kdedecoration.OrgKdeKwinServerDecorationRequestRequestMode, uint32(3))
39+ if err := c.Roundtrip(); err != nil {
40+ t.Fatal(err)
41+ }
42+ for _, want := range []kdedecoration.OrgKdeKwinServerDecorationMode{kdedecoration.OrgKdeKwinServerDecorationModeServer, kdedecoration.OrgKdeKwinServerDecorationModeClient} {
43+ select {
44+ case got := <-proxy.modes:
45+ if got != uint32(want) {
46+ t.Fatalf("mode %d want %d", got, want)
47+ }
48+ case <-time.After(2 * time.Second):
49+ t.Fatalf("no mode %d", want)
50+ }
51+ }
52+ select {
53+ case got := <-proxy.modes:
54+ t.Fatalf("extra mode %d", got)
55+ default:
56+ }
57+}

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 23

Tags 10

↑↓ moveenter openesc close