detached 27b8b9e

Merge branch 'feat/x7-xdgoutput' into feat/x7

commit
27b8b9ee6e31c9c5808c7cd603f34dd22aeb894b
author
brice · · 2026-10-22 15:14 UTC
committer
brice · · 2026-10-22 17:54 UTC
merge of
83719a6 + 305c2c8 · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +226 -1
M internal/adapters/wayland/outputs.go +83 -1
@@ -42,6 +42,8 @@ type output struct {
4242 global *server.Global
4343 resources []*wayland.Output
4444 xdgOutputs []*xdgoutput.ZxdgOutputV1
45+ // xdgOutputsX are the private xdg-outputs of the Xwayland client (X space).
46+ xdgOutputsX []*xdgoutput.ZxdgOutputV1
4547 }
4648
4749 func (o *output) name() string { return o.place.Info.Name }
@@ -89,6 +91,51 @@ func (o *output) sendXDG(x *xdgoutput.ZxdgOutputV1, withName bool) {
8991 }
9092 }
9193
94+// xOrigin is the layout point that is X (0,0): the top-left of the layout
95+// extents, so every X coordinate is non-negative. A plain copy of the
96+// xwm.Space rule at scale 1 until that type merges.
97+func (s *Server) xOrigin() (ox, oy int) {
98+ for i, p := range s.outputPlaces {
99+ if i == 0 || p.X < ox {
100+ ox = p.X
101+ }
102+ if i == 0 || p.Y < oy {
103+ oy = p.Y
104+ }
105+ }
106+ return ox, oy
107+}
108+
109+// sendXDGX sends the private xdg-output state: the layout position moved to
110+// X space and the logical size.
111+func (o *output) sendXDGX(s *Server, x *xdgoutput.ZxdgOutputV1, withName bool) {
112+ ox, oy := s.xOrigin()
113+ x.SendLogicalPosition(int32(o.place.X-ox), int32(o.place.Y-oy))
114+ x.SendLogicalSize(int32(o.place.Width), int32(o.place.Height))
115+ if withName && x.Version() >= 2 {
116+ x.SendName(o.name())
117+ x.SendDescription(o.description())
118+ }
119+}
120+
121+// republishXDGX resends every private xdg-output after a layout change, so
122+// Xwayland resizes the root and its RandR outputs.
123+func (s *Server) republishXDGX() {
124+ for _, o := range s.outputs {
125+ for _, x := range o.xdgOutputsX {
126+ o.sendXDGX(s, x, false)
127+ if x.Version() < 3 {
128+ x.SendDone()
129+ }
130+ }
131+ for _, r := range o.resources {
132+ if r.Client() == s.xwayland.client && r.Version() >= 2 {
133+ r.SendDone()
134+ }
135+ }
136+ }
137+}
138+
92139 // outputOf returns the output a wl_output resource belongs to, or nil for a
93140 // removed one.
94141 func (s *Server) outputOf(r *wayland.Output) *output {
@@ -244,6 +291,9 @@ func (s *Server) setOutputs(c ports.SetOutputs) {
244291 for _, surf := range s.surfaces {
245292 surf.sendScale()
246293 }
294+ if changed {
295+ s.republishXDGX()
296+ }
247297 s.notifyColorFeedback()
248298 // Layer surfaces sized from the output follow its new logical size.
249299 adopted := false
@@ -279,10 +329,42 @@ type outputHandler struct{}
279329
280330 func (outputHandler) Release(*wayland.Output) {}
281331
332+// registerXDGOutput registers the public zxdg_output_manager_v1 and a second,
333+// private one for Xwayland whose outputs are in X space (see globalVisible).
282334 func registerXDGOutput(d *server.Display, s *Server) error {
283- return xdgoutput.NewZxdgOutputManagerV1Global(d, 3, func(c server.Client, v, id uint32) {
335+ var err error
336+ s.xdgPublic, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) {
284337 _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s})
285338 })
339+ if err != nil {
340+ return err
341+ }
342+ s.xdgPrivate, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) {
343+ _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManagerX{s})
344+ })
345+ return err
346+}
347+
348+type xdgOutputManagerX struct{ server *Server }
349+
350+func (xdgOutputManagerX) Destroy(*xdgoutput.ZxdgOutputManagerV1) {}
351+func (m xdgOutputManagerX) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint32, wl *wayland.Output) {
352+ x, err := xdgoutput.NewZxdgOutputV1(r.Client(), r.Version(), id, xdgOutput{})
353+ if err != nil {
354+ return
355+ }
356+ o := m.server.outputOf(wl)
357+ if o == nil {
358+ return
359+ }
360+ o.xdgOutputsX = append(o.xdgOutputsX, x)
361+ x.OnDestroy = func() { o.xdgOutputsX = removeItem(o.xdgOutputsX, x) }
362+ o.sendXDGX(m.server, x, true)
363+ if r.Version() < 3 || wl.Version() < 2 {
364+ x.SendDone()
365+ } else {
366+ wl.SendDone()
367+ }
286368 }
287369
288370 type xdgOutputManager struct{ server *Server }
@@ -42,6 +42,8 @@ type output struct {
42 global *server.Global42 global *server.Global
43 resources []*wayland.Output43 resources []*wayland.Output
44 xdgOutputs []*xdgoutput.ZxdgOutputV144 xdgOutputs []*xdgoutput.ZxdgOutputV1
45+ // xdgOutputsX are the private xdg-outputs of the Xwayland client (X space).
46+ xdgOutputsX []*xdgoutput.ZxdgOutputV1
45 }47 }
46 48
47 func (o *output) name() string { return o.place.Info.Name }49 func (o *output) name() string { return o.place.Info.Name }
@@ -89,6 +91,51 @@ func (o *output) sendXDG(x *xdgoutput.ZxdgOutputV1, withName bool) {
89 }91 }
90 }92 }
91 93
94+// xOrigin is the layout point that is X (0,0): the top-left of the layout
95+// extents, so every X coordinate is non-negative. A plain copy of the
96+// xwm.Space rule at scale 1 until that type merges.
97+func (s *Server) xOrigin() (ox, oy int) {
98+ for i, p := range s.outputPlaces {
99+ if i == 0 || p.X < ox {
100+ ox = p.X
101+ }
102+ if i == 0 || p.Y < oy {
103+ oy = p.Y
104+ }
105+ }
106+ return ox, oy
107+}
108+
109+// sendXDGX sends the private xdg-output state: the layout position moved to
110+// X space and the logical size.
111+func (o *output) sendXDGX(s *Server, x *xdgoutput.ZxdgOutputV1, withName bool) {
112+ ox, oy := s.xOrigin()
113+ x.SendLogicalPosition(int32(o.place.X-ox), int32(o.place.Y-oy))
114+ x.SendLogicalSize(int32(o.place.Width), int32(o.place.Height))
115+ if withName && x.Version() >= 2 {
116+ x.SendName(o.name())
117+ x.SendDescription(o.description())
118+ }
119+}
120+
121+// republishXDGX resends every private xdg-output after a layout change, so
122+// Xwayland resizes the root and its RandR outputs.
123+func (s *Server) republishXDGX() {
124+ for _, o := range s.outputs {
125+ for _, x := range o.xdgOutputsX {
126+ o.sendXDGX(s, x, false)
127+ if x.Version() < 3 {
128+ x.SendDone()
129+ }
130+ }
131+ for _, r := range o.resources {
132+ if r.Client() == s.xwayland.client && r.Version() >= 2 {
133+ r.SendDone()
134+ }
135+ }
136+ }
137+}
138+
92 // outputOf returns the output a wl_output resource belongs to, or nil for a139 // outputOf returns the output a wl_output resource belongs to, or nil for a
93 // removed one.140 // removed one.
94 func (s *Server) outputOf(r *wayland.Output) *output {141 func (s *Server) outputOf(r *wayland.Output) *output {
@@ -244,6 +291,9 @@ func (s *Server) setOutputs(c ports.SetOutputs) {
244 for _, surf := range s.surfaces {291 for _, surf := range s.surfaces {
245 surf.sendScale()292 surf.sendScale()
246 }293 }
294+ if changed {
295+ s.republishXDGX()
296+ }
247 s.notifyColorFeedback()297 s.notifyColorFeedback()
248 // Layer surfaces sized from the output follow its new logical size.298 // Layer surfaces sized from the output follow its new logical size.
249 adopted := false299 adopted := false
@@ -279,10 +329,42 @@ type outputHandler struct{}
279 329
280 func (outputHandler) Release(*wayland.Output) {}330 func (outputHandler) Release(*wayland.Output) {}
281 331
332+// registerXDGOutput registers the public zxdg_output_manager_v1 and a second,
333+// private one for Xwayland whose outputs are in X space (see globalVisible).
282 func registerXDGOutput(d *server.Display, s *Server) error {334 func registerXDGOutput(d *server.Display, s *Server) error {
283- return xdgoutput.NewZxdgOutputManagerV1Global(d, 3, func(c server.Client, v, id uint32) {335+ var err error
336+ s.xdgPublic, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) {
284 _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s})337 _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s})
285 })338 })
339+ if err != nil {
340+ return err
341+ }
342+ s.xdgPrivate, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) {
343+ _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManagerX{s})
344+ })
345+ return err
346+}
347+
348+type xdgOutputManagerX struct{ server *Server }
349+
350+func (xdgOutputManagerX) Destroy(*xdgoutput.ZxdgOutputManagerV1) {}
351+func (m xdgOutputManagerX) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint32, wl *wayland.Output) {
352+ x, err := xdgoutput.NewZxdgOutputV1(r.Client(), r.Version(), id, xdgOutput{})
353+ if err != nil {
354+ return
355+ }
356+ o := m.server.outputOf(wl)
357+ if o == nil {
358+ return
359+ }
360+ o.xdgOutputsX = append(o.xdgOutputsX, x)
361+ x.OnDestroy = func() { o.xdgOutputsX = removeItem(o.xdgOutputsX, x) }
362+ o.sendXDGX(m.server, x, true)
363+ if r.Version() < 3 || wl.Version() < 2 {
364+ x.SendDone()
365+ } else {
366+ wl.SendDone()
367+ }
286 }368 }
287 369
288 type xdgOutputManager struct{ server *Server }370 type xdgOutputManager struct{ server *Server }
M internal/adapters/wayland/server.go +3 -0
@@ -162,6 +162,9 @@ type Server struct {
162162 securityWG sync.WaitGroup
163163 securityLive atomic.Int32
164164 xwayland xwaylandClient // display owner
165+ // xdgPublic and xdgPrivate are the two zxdg_output_manager_v1 globals:
166+ // layout space for clients, X space for Xwayland.
167+ xdgPublic, xdgPrivate *server.Global
165168 // xwm manages the X11 windows of the Xwayland client; x11Slots maps its
166169 // slots to their windows. Display goroutine only.
167170 xwm *xwm.XWM
@@ -162,6 +162,9 @@ type Server struct {
162 securityWG sync.WaitGroup162 securityWG sync.WaitGroup
163 securityLive atomic.Int32163 securityLive atomic.Int32
164 xwayland xwaylandClient // display owner164 xwayland xwaylandClient // display owner
165+ // xdgPublic and xdgPrivate are the two zxdg_output_manager_v1 globals:
166+ // layout space for clients, X space for Xwayland.
167+ xdgPublic, xdgPrivate *server.Global
165 // xwm manages the X11 windows of the Xwayland client; x11Slots maps its168 // xwm manages the X11 windows of the Xwayland client; x11Slots maps its
166 // slots to their windows. Display goroutine only.169 // slots to their windows. Display goroutine only.
167 xwm *xwm.XWM170 xwm *xwm.XWM
M internal/adapters/wayland/x11window.go +6 -0
@@ -28,6 +28,12 @@ func xwaylandRestricted(iface string) bool {
2828 // globalVisible is the display's global filter: the Xwayland-only globals
2929 // are visible to, and bindable by, the Xwayland client alone.
3030 func (s *Server) globalVisible(c server.Client, g *server.Global) bool {
31+ if g == s.xdgPrivate {
32+ return s.xwayland.client != (server.Client{}) && c == s.xwayland.client
33+ }
34+ if g == s.xdgPublic {
35+ return c != s.xwayland.client
36+ }
3137 if xwaylandRestricted(g.Interface()) {
3238 return s.xwayland.client != (server.Client{}) && c == s.xwayland.client
3339 }
@@ -28,6 +28,12 @@ func xwaylandRestricted(iface string) bool {
28 // globalVisible is the display's global filter: the Xwayland-only globals28 // globalVisible is the display's global filter: the Xwayland-only globals
29 // are visible to, and bindable by, the Xwayland client alone.29 // are visible to, and bindable by, the Xwayland client alone.
30 func (s *Server) globalVisible(c server.Client, g *server.Global) bool {30 func (s *Server) globalVisible(c server.Client, g *server.Global) bool {
31+ if g == s.xdgPrivate {
32+ return s.xwayland.client != (server.Client{}) && c == s.xwayland.client
33+ }
34+ if g == s.xdgPublic {
35+ return c != s.xwayland.client
36+ }
31 if xwaylandRestricted(g.Interface()) {37 if xwaylandRestricted(g.Interface()) {
32 return s.xwayland.client != (server.Client{}) && c == s.xwayland.client38 return s.xwayland.client != (server.Client{}) && c == s.xwayland.client
33 }39 }
A internal/adapters/wayland/xdgoutputx_test.go +134 -0
@@ -0,0 +1,134 @@
1+package wayland
2+
3+import (
4+ "slices"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+ "github.com/bnema/purego-libwayland/server"
9+ "github.com/bnema/wlturbo"
10+)
11+
12+// xdgPosProxy records the logical_position events of one zxdg_output_v1.
13+type xdgPosProxy struct {
14+ wlturbo.BaseProxy
15+ pos [][2]int32
16+}
17+
18+func (p *xdgPosProxy) Dispatch(e *wlturbo.Event) {
19+ if e.Opcode == 0 {
20+ p.pos = append(p.pos, [2]int32{e.Int32(), e.Int32()})
21+ }
22+}
23+
24+// xdgOutputsOf binds the xdg-output manager and an xdg-output for every
25+// wl_output of c, and returns what each one reported.
26+func xdgOutputsOf(t *testing.T, c *wlturbo.Display) []*xdgPosProxy {
27+ t.Helper()
28+ if err := c.Roundtrip(); err != nil {
29+ t.Fatal(err)
30+ }
31+ var mgrs int
32+ var manager uint32
33+ for _, g := range c.Registry().GetGlobals() {
34+ if g.Interface == "zxdg_output_manager_v1" {
35+ mgrs++
36+ id, err := bindWireID(c, g.Name, g.Interface, 3)
37+ if err != nil {
38+ t.Fatal(err)
39+ }
40+ registerProtocol(t, c, id)
41+ manager = id
42+ }
43+ }
44+ if mgrs != 1 {
45+ t.Fatalf("client sees %d zxdg_output_manager_v1 globals, want 1", mgrs)
46+ }
47+ var out []*xdgPosProxy
48+ for _, g := range c.Registry().GetGlobals() {
49+ if g.Interface != "wl_output" {
50+ continue
51+ }
52+ wo, err := bindWireID(c, g.Name, g.Interface, 4)
53+ if err != nil {
54+ t.Fatal(err)
55+ }
56+ xp := &xdgPosProxy{}
57+ xp.SetID(c.AllocateID())
58+ registerWireProxy(c, xp)
59+ requestProtocol(t, c, manager, 1, xp.ID(), wo)
60+ out = append(out, xp)
61+ }
62+ if err := c.Roundtrip(); err != nil {
63+ t.Fatal(err)
64+ }
65+ return out
66+}
67+
68+func lastPositions(ps []*xdgPosProxy) [][2]int32 {
69+ var r [][2]int32
70+ for _, p := range ps {
71+ r = append(r, p.pos[len(p.pos)-1])
72+ }
73+ slices.SortFunc(r, func(a, b [2]int32) int { return int(a[0] - b[0]) })
74+ return r
75+}
76+
77+func twoOutputs(ax int) ports.SetOutputs {
78+ mk := func(name string, x int) ports.OutputPlacement {
79+ return ports.OutputPlacement{Info: ports.OutputInfo{Name: name, Width: 1920, Height: 1080}, X: x, Y: 0, Width: 1920, Height: 1080, Scale: 1}
80+ }
81+ return ports.SetOutputs{Outputs: ports.Layout{mk("A", ax), mk("B", 0)}}
82+}
83+
84+// Xwayland gets X-space positions (layout top-left is 0,0) and re-receives
85+// them on a layout change; every other client keeps the layout coordinates.
86+func TestXwaylandPrivateXDGOutput(t *testing.T) {
87+ r := newX11Rig(t)
88+ applyOutputs(t, r.s, r.commands, twoOutputs(-1920))
89+ other := protocolClient(t, r.s, r.dir)
90+ xo := xdgOutputsOf(t, r.xc)
91+ oo := xdgOutputsOf(t, other)
92+ if got, want := lastPositions(xo), [][2]int32{{0, 0}, {1920, 0}}; !slices.Equal(got, want) {
93+ t.Errorf("Xwayland positions %v, want %v", got, want)
94+ }
95+ if got, want := lastPositions(oo), [][2]int32{{-1920, 0}, {0, 0}}; !slices.Equal(got, want) {
96+ t.Errorf("ordinary positions %v, want %v", got, want)
97+ }
98+ applyOutputs(t, r.s, r.commands, twoOutputs(-3840))
99+ if err := r.xc.Roundtrip(); err != nil {
100+ t.Fatal(err)
101+ }
102+ if err := other.Roundtrip(); err != nil {
103+ t.Fatal(err)
104+ }
105+ if got, want := lastPositions(xo), [][2]int32{{0, 0}, {3840, 0}}; !slices.Equal(got, want) {
106+ t.Errorf("Xwayland positions after change %v, want %v", got, want)
107+ }
108+ if got, want := lastPositions(oo), [][2]int32{{-3840, 0}, {0, 0}}; !slices.Equal(got, want) {
109+ t.Errorf("ordinary positions after change %v, want %v", got, want)
110+ }
111+ // Hotplug: a third output on the right grows the extents.
112+ third := twoOutputs(-3840)
113+ third.Outputs = append(third.Outputs, ports.OutputPlacement{Info: ports.OutputInfo{Name: "C", Width: 1920, Height: 1080}, X: 1920, Width: 1920, Height: 1080, Scale: 1})
114+ applyOutputs(t, r.s, r.commands, third)
115+ if err := r.xc.Roundtrip(); err != nil {
116+ t.Fatal(err)
117+ }
118+ if got := lastPositions(xo); len(got) != 2 || got[1] != [2]int32{3840, 0} {
119+ t.Errorf("Xwayland positions after hotplug %v", got)
120+ }
121+}
122+
123+func TestGlobalFilterAllocations(t *testing.T) {
124+ if raceEnabled {
125+ t.Skip("the race detector allocates in the background")
126+ }
127+ s := &Server{xdgPublic: &server.Global{}, xdgPrivate: &server.Global{}}
128+ if allocs := testing.AllocsPerRun(100, func() {
129+ s.globalVisible(server.Client{}, s.xdgPrivate)
130+ s.globalVisible(server.Client{}, s.xdgPublic)
131+ }); allocs != 0 {
132+ t.Errorf("globalVisible: %.1f allocs, want 0", allocs)
133+ }
134+}
@@ -0,0 +1,134 @@
1+package wayland
2+
3+import (
4+ "slices"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+ "github.com/bnema/purego-libwayland/server"
9+ "github.com/bnema/wlturbo"
10+)
11+
12+// xdgPosProxy records the logical_position events of one zxdg_output_v1.
13+type xdgPosProxy struct {
14+ wlturbo.BaseProxy
15+ pos [][2]int32
16+}
17+
18+func (p *xdgPosProxy) Dispatch(e *wlturbo.Event) {
19+ if e.Opcode == 0 {
20+ p.pos = append(p.pos, [2]int32{e.Int32(), e.Int32()})
21+ }
22+}
23+
24+// xdgOutputsOf binds the xdg-output manager and an xdg-output for every
25+// wl_output of c, and returns what each one reported.
26+func xdgOutputsOf(t *testing.T, c *wlturbo.Display) []*xdgPosProxy {
27+ t.Helper()
28+ if err := c.Roundtrip(); err != nil {
29+ t.Fatal(err)
30+ }
31+ var mgrs int
32+ var manager uint32
33+ for _, g := range c.Registry().GetGlobals() {
34+ if g.Interface == "zxdg_output_manager_v1" {
35+ mgrs++
36+ id, err := bindWireID(c, g.Name, g.Interface, 3)
37+ if err != nil {
38+ t.Fatal(err)
39+ }
40+ registerProtocol(t, c, id)
41+ manager = id
42+ }
43+ }
44+ if mgrs != 1 {
45+ t.Fatalf("client sees %d zxdg_output_manager_v1 globals, want 1", mgrs)
46+ }
47+ var out []*xdgPosProxy
48+ for _, g := range c.Registry().GetGlobals() {
49+ if g.Interface != "wl_output" {
50+ continue
51+ }
52+ wo, err := bindWireID(c, g.Name, g.Interface, 4)
53+ if err != nil {
54+ t.Fatal(err)
55+ }
56+ xp := &xdgPosProxy{}
57+ xp.SetID(c.AllocateID())
58+ registerWireProxy(c, xp)
59+ requestProtocol(t, c, manager, 1, xp.ID(), wo)
60+ out = append(out, xp)
61+ }
62+ if err := c.Roundtrip(); err != nil {
63+ t.Fatal(err)
64+ }
65+ return out
66+}
67+
68+func lastPositions(ps []*xdgPosProxy) [][2]int32 {
69+ var r [][2]int32
70+ for _, p := range ps {
71+ r = append(r, p.pos[len(p.pos)-1])
72+ }
73+ slices.SortFunc(r, func(a, b [2]int32) int { return int(a[0] - b[0]) })
74+ return r
75+}
76+
77+func twoOutputs(ax int) ports.SetOutputs {
78+ mk := func(name string, x int) ports.OutputPlacement {
79+ return ports.OutputPlacement{Info: ports.OutputInfo{Name: name, Width: 1920, Height: 1080}, X: x, Y: 0, Width: 1920, Height: 1080, Scale: 1}
80+ }
81+ return ports.SetOutputs{Outputs: ports.Layout{mk("A", ax), mk("B", 0)}}
82+}
83+
84+// Xwayland gets X-space positions (layout top-left is 0,0) and re-receives
85+// them on a layout change; every other client keeps the layout coordinates.
86+func TestXwaylandPrivateXDGOutput(t *testing.T) {
87+ r := newX11Rig(t)
88+ applyOutputs(t, r.s, r.commands, twoOutputs(-1920))
89+ other := protocolClient(t, r.s, r.dir)
90+ xo := xdgOutputsOf(t, r.xc)
91+ oo := xdgOutputsOf(t, other)
92+ if got, want := lastPositions(xo), [][2]int32{{0, 0}, {1920, 0}}; !slices.Equal(got, want) {
93+ t.Errorf("Xwayland positions %v, want %v", got, want)
94+ }
95+ if got, want := lastPositions(oo), [][2]int32{{-1920, 0}, {0, 0}}; !slices.Equal(got, want) {
96+ t.Errorf("ordinary positions %v, want %v", got, want)
97+ }
98+ applyOutputs(t, r.s, r.commands, twoOutputs(-3840))
99+ if err := r.xc.Roundtrip(); err != nil {
100+ t.Fatal(err)
101+ }
102+ if err := other.Roundtrip(); err != nil {
103+ t.Fatal(err)
104+ }
105+ if got, want := lastPositions(xo), [][2]int32{{0, 0}, {3840, 0}}; !slices.Equal(got, want) {
106+ t.Errorf("Xwayland positions after change %v, want %v", got, want)
107+ }
108+ if got, want := lastPositions(oo), [][2]int32{{-3840, 0}, {0, 0}}; !slices.Equal(got, want) {
109+ t.Errorf("ordinary positions after change %v, want %v", got, want)
110+ }
111+ // Hotplug: a third output on the right grows the extents.
112+ third := twoOutputs(-3840)
113+ third.Outputs = append(third.Outputs, ports.OutputPlacement{Info: ports.OutputInfo{Name: "C", Width: 1920, Height: 1080}, X: 1920, Width: 1920, Height: 1080, Scale: 1})
114+ applyOutputs(t, r.s, r.commands, third)
115+ if err := r.xc.Roundtrip(); err != nil {
116+ t.Fatal(err)
117+ }
118+ if got := lastPositions(xo); len(got) != 2 || got[1] != [2]int32{3840, 0} {
119+ t.Errorf("Xwayland positions after hotplug %v", got)
120+ }
121+}
122+
123+func TestGlobalFilterAllocations(t *testing.T) {
124+ if raceEnabled {
125+ t.Skip("the race detector allocates in the background")
126+ }
127+ s := &Server{xdgPublic: &server.Global{}, xdgPrivate: &server.Global{}}
128+ if allocs := testing.AllocsPerRun(100, func() {
129+ s.globalVisible(server.Client{}, s.xdgPrivate)
130+ s.globalVisible(server.Client{}, s.xdgPublic)
131+ }); allocs != 0 {
132+ t.Errorf("globalVisible: %.1f allocs, want 0", allocs)
133+ }
134+}

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