detached 0a100ce

Merge e264045642b9b7340e24f3d336fb638cd69c5375 into 007c134bb83968897ef9efe3d134f65ad337aa79

commit
0a100ce6525ef46efe32fba3a55d9b606f97cd57
author
ermite · · 2026-10-22 15:26 UTC
merge of
007c134 + e264045 · diff against the first parent
signature
unsigned
85 files · +13289 -149
M .mockery.yml +20 -0
@@ -182,3 +182,23 @@ packages:
182182 dir: internal/adapters/screencast
183183 pkgname: screencast
184184 filename: pipewire_opener_mock_test.go
185+
186+ github.com/bnema/neferwl/internal/adapters/xwayland:
187+ interfaces:
188+ starter:
189+ config:
190+ dir: internal/adapters/xwayland
191+ pkgname: xwayland
192+ filename: starter_mock_test.go
193+ github.com/bnema/neferwl/internal/adapters/wayland/xwm:
194+ interfaces:
195+ host:
196+ config:
197+ dir: internal/adapters/wayland/xwm
198+ pkgname: xwm
199+ filename: host_mock_test.go
200+ fdSource:
201+ config:
202+ dir: internal/adapters/wayland/xwm
203+ pkgname: xwm
204+ filename: fdsource_mock_test.go
M Makefile +9 -2
@@ -1,4 +1,4 @@
1-.PHONY: build test vet race mocks mocks-check spv-check fakes-check log-check arch adapter-imports check perf-check color-check bin tty logs pkg install
1+.PHONY: build test vet race e2e-xwayland mocks mocks-check spv-check fakes-check log-check arch adapter-imports check perf-check color-check bin tty logs pkg install
22
33 # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net.
44 TTY_TIMEOUT ?= 0
@@ -26,6 +26,7 @@ test:
2626 CGO_ENABLED=0 go test ./...
2727 vet:
2828 CGO_ENABLED=0 go vet ./...
29+ CGO_ENABLED=0 go vet -tags xwayland ./internal/app
2930 race:
3031 # cgo is enabled only for the race test binary.
3132 CGO_ENABLED=1 go test -race ./...
@@ -69,7 +70,8 @@ adapter-imports:
6970 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
7071 # Fast, mandatory guards for tiled content publications and SHM copies.
7172 perf-check:
72- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing)$$' -count=1
73+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations|TestOverrideMovedUnchangedAllocations)$$' -count=1
74+ CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1
7375 check: vet test arch fakes-check log-check perf-check
7476
7577 # On-demand SDR/HDR colour check with examples/testpattern on a headless
@@ -77,6 +79,11 @@ check: vet test arch fakes-check log-check perf-check
7779 color-check:
7880 CGO_ENABLED=0 go test -tags colorcheck ./internal/app -run '^TestColorCheck' -count=1 -v
7981
82+# End-to-end test of the built-in XWM: a real Xwayland and an X11 probe on a
83+# headless NeferWL. Needs Xwayland and a GPU; not part of check.
84+e2e-xwayland:
85+ CGO_ENABLED=1 go test -tags xwayland -race ./internal/app -run TestXwayland -count=1 -v
86+
8087 # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules
8188 # come from the module proxy in prepare(); the build itself runs offline.
8289 # pacman-ordered version: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0.
M README.md +1 -1
@@ -149,7 +149,7 @@ See [Configuration](docs/config.md) for all keys, actions, bindings and automati
149149 - Explicit sync carries client fences to the GPU and KMS, enabled by default with no flag. HDR10 on capable displays includes configurable SDR brightness.
150150 - Separate workspaces and fractional scale per output; workspaces move when a monitor is unplugged and return when it reconnects.
151151 - Hidden apps get frame callbacks once per second and the suspended state, whether on another workspace, in a hidden stash, off screen or under fullscreen. Captured windows keep their output's refresh rate.
152-- Steam and other X11 clients run through xwayland-satellite; Wine can run natively on Wayland.
152+- Steam and other X11 clients run through xwayland-satellite, or through the experimental built-in window manager (`xwayland = builtin`; shows menus, popups and tooltips; no clipboard or XSETTINGS yet). Wine can run natively on Wayland.
153153
154154 ### Windows and workspaces
155155
M docs/config.md +3 -1
@@ -27,7 +27,7 @@ See [examples/config](../examples/config) for every key with its default.
2727 | `terminal` | `$TERMINAL` or `foot` | Terminal command |
2828 | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) |
2929 | `startup` | none | Commands run at session start, separated by `;` |
30-| `xwayland` | `xwayland-satellite` | X11 support; `off` disables it |
30+| `xwayland` | `satellite` | `off`, `satellite`, `builtin` or a satellite binary path |
3131 | `background` | `#111111` | Background color |
3232 | `border.radius` | `0` | Corner radius in logical pixels, `0` to `64`; floats always, tiles only with gaps |
3333 | `border.width` | `2` | Border width in pixels; `0` hides borders |
@@ -103,6 +103,8 @@ destination output, even if that output has no windows. Same-screen workspace
103103 switches and mouse focus do not trigger a flash.
104104 It runs when `animations = on`.
105105
106+`xwayland = satellite` uses `xwayland-satellite` from `PATH`; a custom path also selects satellite. `off` disables X11. `builtin` (experimental): neferwl starts `Xwayland` itself and manages X11 windows as its window manager: windows tile, take focus, close with `close-window` (`WM_DELETE_WINDOW`) and show their title and class. Menus, popups and tooltips (override-redirect windows) are drawn above their window at the right place and never appear as windows in `neferwl state`. Clipboard and XSETTINGS are not supported; use `satellite` if you need them.
107+
106108 ## Automatic terminal
107109
108110 The command comes from `terminal`, then `$TERMINAL`, then `foot`.
M docs/desktop.md +3 -1
@@ -52,12 +52,14 @@ Candidate popups are not drawn yet.
5252
5353 ## X11 apps
5454
55-X11 apps such as Steam run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7+, found in `PATH`. NeferWL starts it when the first X11 app connects; do not start it yourself.
55+By default, X11 apps such as Steam run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7+, found in `PATH`. NeferWL starts it when the first X11 app connects; do not start it yourself.
5656
5757 ```text
5858 xwayland = off # no X11 support
5959 ```
6060
61+With `xwayland = builtin` (experimental), neferwl starts `Xwayland` itself and manages X11 windows: they tile, take focus and close like Wayland windows. Menus, popups and tooltips of X11 apps are shown above their window and do not take the keyboard focus. Clipboard and XSETTINGS are not supported; use `satellite` if you need them.
62+
6163 ## Session locking
6264
6365 NeferWL supports `ext-session-lock-v1` with an external locker such as swaylock or hyprlock. The locker checks the password; NeferWL hides the desktop and blocks input.
M docs/dev/headless.md +16 -0
@@ -62,6 +62,22 @@ WAYLAND_DISPLAY=<name> go run ./examples/testime -preedit nihon -commit 日本
6262
6363 `-count <n>` exits after n commits; `-delay` sets the time between preedit and commit.
6464
65+## X11 check
66+
67+`make e2e-xwayland` runs the end-to-end test of the built-in window manager (`xwayland = builtin`). It is behind the `xwayland` build tag, so `go test ./...` and `make check` only compile it (`go vet -tags xwayland`). It needs `Xwayland` on `PATH` and a GPU, and skips without Xwayland.
68+
69+The test starts a headless NeferWL with a real `Xwayland` on the first free X display (a live session's `:0` stays untouched) and runs `internal/app/testdata/x11probe`, a small X11 client (`-class`, `-title`, `-size`, `-sleep`) that paints its window green, prints `mapped WxH` and `focus`, and exits 0 on `WM_DELETE_WINDOW`. With `-override` it also maps a magenta override-redirect window (`-override-at`, `-override-size`, `-override-type menu|tooltip`, `-override-for`, `-override-shape`, `-override-dismiss-on-focusout`) and prints `override mapped`, `override unmapped` or `dismissed`. It checks:
70+
71+- the state lists one visible `X11Probe` window with the probe's pid, and the screenshot is mostly green inside its tile;
72+- the probe receives focus;
73+- a second probe tiles beside the first;
74+- `key Super+q` closes the focused probe within 2 seconds and the state drops to one window;
75+- an override-redirect menu is drawn magenta above the green toplevel at its X position, is not listed in the state, leaves focus on the toplevel and disappears from the screenshot once the probe unmaps it;
76+- a menu is dismissed when its owner gets `FocusOut` because another X11 window took the focus;
77+- a menu with an empty input shape is still drawn and still not a window.
78+
79+Run it by hand with `go test -tags xwayland -race ./internal/app -run TestXwayland -count=1 -v`.
80+
6581 ## Colour check
6682
6783 `make color-check` renders `examples/testpattern` on a headless output and checks the colours, in SDR and in HDR (`--headless-hdr`). It needs a GPU and `/dev/udmabuf`, and is not part of `make check`.
M go.mod +4 -3
@@ -3,10 +3,11 @@ module github.com/bnema/neferwl
33 go 1.27
44
55 require (
6+ bnema.dev/xturbo v0.1.0
67 github.com/bnema/go-wayland-bindings v0.1.0
78 github.com/bnema/kvconf v0.1.2
8- github.com/bnema/purego v0.13.0-bnema.1
9- github.com/bnema/purego-libwayland v0.11.0
9+ github.com/bnema/purego v0.13.0-bnema.2
10+ github.com/bnema/purego-libwayland v0.12.0
1011 github.com/bnema/purego-pipewire v0.2.0
1112 github.com/bnema/purego-vulkan v0.6.0
1213 github.com/bnema/wlturbo v0.5.0
@@ -16,7 +17,7 @@ require (
1617 github.com/rs/zerolog v1.35.1
1718 github.com/stretchr/testify v1.12.1
1819 golang.org/x/image v0.46.0
19- golang.org/x/sys v0.48.0
20+ golang.org/x/sys v0.49.0
2021 golang.org/x/term v0.46.0
2122 )
2223
M go.sum +8 -6 generated file: collapsed
M internal/adapters/config/config.go +8 -4
@@ -167,7 +167,7 @@ func Defaults() ports.Config {
167167 // Hint mode is experimental: off unless the config turns it on.
168168 c.Hint.Enable = false
169169 c.Hint.TextClick = ports.ModCtrl | ports.ModShift
170- c.Xwayland = "xwayland-satellite"
170+ c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite"}
171171 c.Background.Color = "#111111"
172172 c.Floating.Dim = 0.3
173173 c.Stash.Width = 80
@@ -726,11 +726,15 @@ func set(c *ports.Config, key, v string) error {
726726 case "xwayland":
727727 switch {
728728 case v == "off":
729- c.Xwayland = ""
729+ c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandOff}
730+ case v == "satellite":
731+ c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite"}
732+ case v == "builtin":
733+ c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandBuiltin, Binary: "Xwayland"}
730734 case v == "" || strings.ContainsAny(v, " \t"):
731- return fmt.Errorf("must be off or the xwayland-satellite program")
735+ return fmt.Errorf("must be off, satellite, builtin or a satellite program")
732736 default:
733- c.Xwayland = v
737+ c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: v}
734738 }
735739 case "background":
736740 if !color.MatchString(v) {
M internal/adapters/config/config_test.go +16 -6
@@ -340,14 +340,24 @@ func TestWorkspacePrevNextAction(t *testing.T) {
340340 }
341341
342342 func TestXwayland(t *testing.T) {
343- if Defaults().Xwayland != "xwayland-satellite" {
343+ if Defaults().Xwayland != (ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite"}) {
344344 t.Fatal(Defaults().Xwayland)
345345 }
346- if c, w := parseString(t, "xwayland = off"); c.Xwayland != "" || len(w) != 0 {
347- t.Fatal(c.Xwayland, w)
348- }
349- if c, w := parseString(t, "xwayland = /opt/xwls"); c.Xwayland != "/opt/xwls" || len(w) != 0 {
350- t.Fatal(c.Xwayland, w)
346+ for _, tt := range []struct {
347+ value string
348+ want ports.XwaylandConfig
349+ }{
350+ {"off", ports.XwaylandConfig{Backend: ports.XwaylandOff}},
351+ {"satellite", ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite"}},
352+ {"builtin", ports.XwaylandConfig{Backend: ports.XwaylandBuiltin, Binary: "Xwayland"}},
353+ {"/opt/xwls", ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "/opt/xwls"}},
354+ } {
355+ t.Run(tt.value, func(t *testing.T) {
356+ c, w := parseString(t, "xwayland = "+tt.value)
357+ if c.Xwayland != tt.want || len(w) != 0 {
358+ t.Fatal(c.Xwayland, w)
359+ }
360+ })
351361 }
352362 }
353363
M internal/adapters/vulkan/capture_test.go +3 -0
@@ -358,6 +358,9 @@ func openFDs(t *testing.T) int {
358358 // slot keeps its sync file for every lease. No mock stands in the measured
359359 // closure.
360360 func TestCaptureCycleAllocations(t *testing.T) {
361+ if raceEnabled {
362+ t.Skip("allocation budgets are not stable under the race runtime")
363+ }
361364 for _, tc := range []struct {
362365 name string
363366 new func(*testing.T) *Renderer
A internal/adapters/vulkan/norace_test.go +5 -0
@@ -0,0 +1,5 @@
1+//go:build !race
2+
3+package vulkan
4+
5+const raceEnabled = false
A internal/adapters/vulkan/race_test.go +8 -0
@@ -0,0 +1,8 @@
1+//go:build race
2+
3+package vulkan
4+
5+// raceEnabled: sync.Pool drops items at random under the race runtime, so the
6+// allocation budgets of calls that go through purego cannot hold. perf-check
7+// runs them without it.
8+const raceEnabled = true
M internal/adapters/vulkan/scene_perf_test.go +6 -0
@@ -16,6 +16,9 @@ import (
1616 // A new scene Seq with one of two windows moved is a partial redraw seeded by
1717 // sceneDelta and allocates nothing either.
1818 func TestRenderSteadyStateAllocations(t *testing.T) {
19+ if raceEnabled {
20+ t.Skip("allocation budgets are not stable under the race runtime")
21+ }
1922 for _, tc := range []struct {
2023 name string
2124 transform ports.BufferTransform
@@ -145,6 +148,9 @@ func TestRenderSteadyStateAllocations(t *testing.T) {
145148 // keeps one alloc of headroom for drivers whose import path differs.
146149 // Skipped without a GPU or /dev/udmabuf.
147150 func TestRenderSteadyStateDMABufAllocations(t *testing.T) {
151+ if raceEnabled {
152+ t.Skip("allocation budgets are not stable under the race runtime")
153+ }
148154 r, err := New(80, 32)
149155 if err != nil {
150156 t.Skipf("Vulkan unavailable: %v", err)
M internal/adapters/wayland/activation.go +3 -3
@@ -66,7 +66,7 @@ func (a activation) Activate(_ *xdgactivation.ActivationV1, token string, surf *
6666 }
6767 var w *window
6868 if reason == "" {
69- if w = s.windows[state.root().windowID()]; w == nil || w.toplevel == nil || !w.mapped {
69+ if w = s.windows[state.root().windowID()]; w == nil || !w.isToplevel() || !w.mapped {
7070 reason = "not_toplevel"
7171 }
7272 }
@@ -116,8 +116,8 @@ func (t *tokenRequest) Commit(r *xdgactivation.ActivationTokenV1) {
116116
117117 // focusClient is the client with the keyboard focus.
118118 func (s *Server) focusClient() server.Client {
119- if w := s.windows[s.seat.focused]; w != nil && w.xdg.resource.Resource.Alive() {
120- return w.xdg.resource.Client()
119+ if w := s.windows[s.seat.focused]; w != nil && w.alive() {
120+ return w.client()
121121 }
122122 if l := s.layers[s.seat.focused]; l != nil && l.resource.Resource.Alive() {
123123 return l.resource.Client()
M internal/adapters/wayland/constraints.go +14 -13
@@ -61,11 +61,11 @@ func (h pointerWarp) WarpPointer(r *pointerwarp.WpPointerWarpV1, surf *wayland.S
6161 return
6262 }
6363 w := s.windows[s.seat.pointerFocus]
64- if w == nil || !w.mapped || w.xdg.resource.Client() != r.Client() || s.surfaces[surf.Resource] != w.xdg.surface {
64+ if w == nil || !w.mapped || w.client() != r.Client() || s.surfaces[surf.Resource] != w.surf() {
6565 return
6666 }
6767 // Surface-local to window-local (core's, from the geometry origin).
68- g := w.xdg.geometry
68+ g := w.geometry()
6969 s.emit(ports.PointerWarp{ID: w.id, X: x.Float() - float64(g.X), Y: y.Float() - float64(g.Y)})
7070 }
7171
@@ -80,12 +80,12 @@ func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool {
8080 if s.protected() || c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 {
8181 return false
8282 }
83- if !w.mapped || !w.xdg.resource.Resource.Alive() {
83+ if !w.mapped || !w.alive() {
8484 return false
8585 }
8686 sent := false
8787 usec := uint64(c.Time / time.Microsecond)
88- for _, rel := range s.relatives[w.xdg.resource.Client()] {
88+ for _, rel := range s.relatives[w.client()] {
8989 if rel.Resource.Alive() {
9090 rel.SendRelativeMotion(uint32(usec>>32), uint32(usec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY))
9191 sent = true
@@ -272,7 +272,7 @@ func (s *Server) updateConstraint() {
272272 }
273273 var want *constraint
274274 if w := s.windows[s.seat.pointerFocus]; w != nil && w.mapped && s.seat.pointerFocus == s.seat.focused {
275- if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) {
275+ if c := s.constraints[w.surf()]; c != nil && !c.defunct && (c.active || c.contains(w)) {
276276 want = c
277277 }
278278 }
@@ -298,8 +298,8 @@ func (s *Server) emitConstraint(c *constraint) {
298298 if s.protected() {
299299 return
300300 }
301- x := c.surface.xdg
302- if x == nil || x.window == nil {
301+ w := c.surface.toplevelWindow()
302+ if w == nil {
303303 return
304304 }
305305 mode := ports.ConstraintConfine
@@ -308,8 +308,9 @@ func (s *Server) emitConstraint(c *constraint) {
308308 }
309309 r := c.region
310310 if r.W > 0 && r.H > 0 {
311- r.X -= x.geometry.X
312- r.Y -= x.geometry.Y
311+ g := w.geometry()
312+ r.X -= g.X
313+ r.Y -= g.Y
313314 }
314315 // Core supports one confinement rect, so this bounds the intersection;
315316 // contains uses the exact input region to decide activation.
@@ -333,17 +334,17 @@ func (s *Server) emitConstraint(c *constraint) {
333334 r = box
334335 }
335336 }
336- s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}})
337+ s.emit(ports.PointerConstrained{ID: w.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}})
337338 }
338339
339340 // contains reports whether the pointer is in the region: a constraint
340341 // activates only there, so it never warps the pointer.
341342 func (c *constraint) contains(w *window) bool {
342343 r := c.region
343- x, y := w.surfacePoint(w.xdg.server.seat.pointerX, w.xdg.server.seat.pointerY)
344+ x, y := w.surfacePoint(c.surface.server.seat.pointerX, c.surface.server.seat.pointerY)
344345 all, rects := c.surface.effectiveInput()
345346 if !all {
346- localX, localY := x-float64(w.xdg.geometry.X), y-float64(w.xdg.geometry.Y)
347+ localX, localY := x-float64(w.geometry().X), y-float64(w.geometry().Y)
347348 inside := false
348349 for _, input := range rects {
349350 if localX >= float64(input.X) && localX < float64(input.X+input.W) && localY >= float64(input.Y) && localY < float64(input.Y+input.H) {
@@ -363,5 +364,5 @@ func (c *constraint) contains(w *window) bool {
363364
364365 // locked reports whether the window's pointer is locked.
365366 func (s *Server) locked(w *window) bool {
366- return !s.protected() && s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.xdg.surface
367+ return !s.protected() && s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.surf()
367368 }
M internal/adapters/wayland/decoration.go +1 -1
@@ -80,7 +80,7 @@ func (d *decoration) UnsetMode(r *xdgdecoration.ZxdgToplevelDecorationV1)
8080 func (d *decoration) send(r *xdgdecoration.ZxdgToplevelDecorationV1) {
8181 r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide))
8282 for _, w := range d.s.windows {
83- if w.toplevel == d.toplevel && w.hasLast && w.toplevel.Resource.Alive() && w.xdg.resource.Resource.Alive() {
83+ if w.toplevel == d.toplevel && w.hasLast && w.toplevelAlive() && w.alive() {
8484 w.sendConfigure()
8585 return
8686 }
M internal/adapters/wayland/dialog.go +1 -1
@@ -77,6 +77,6 @@ func (w *window) setModal(modal bool) {
7777 }
7878 w.modal = modal
7979 if w.mapped {
80- w.xdg.server.emit(ports.WindowModal{ID: w.id, Modal: modal})
80+ w.srv().emit(ports.WindowModal{ID: w.id, Modal: modal})
8181 }
8282 }
M internal/adapters/wayland/dmabuf.go +3 -2
@@ -223,10 +223,11 @@ func (g *dmabufGlobal) fullscreenOutput(surf *surface) (string, bool) {
223223 return "", false
224224 }
225225 root := toplevelRoot(surf)
226- if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast || !root.xdg.window.last.Fullscreen || g.server.invisible(surf) {
226+ w := root.toplevelWindow()
227+ if w == nil || !w.hasLast || !w.last.Fullscreen || g.server.invisible(surf) {
227228 return "", false
228229 }
229- return root.xdg.window.last.Output, true
230+ return w.last.Output, true
230231 }
231232
232233 // scanoutFor is the scanout offer for a surface: its window is fullscreen
M internal/adapters/wayland/extforeigntoplevel.go +1 -1
@@ -72,7 +72,7 @@ func (*extToplevelList) Destroy(*ext.ExtForeignToplevelListV1) {}
7272 func (s *Server) mappedToplevels() []ports.WindowID {
7373 ids := make([]ports.WindowID, 0, len(s.windows))
7474 for id, w := range s.windows {
75- if w.toplevel != nil && w.mapped {
75+ if w.isToplevel() && w.mapped {
7676 ids = append(ids, id)
7777 }
7878 }
M internal/adapters/wayland/foreigntoplevel.go +6 -6
@@ -44,7 +44,7 @@ func registerForeignToplevel(d *server.Display, s *Server) error {
4444 r.OnDestroy = func() { s.toplevelManagers = removeItem(s.toplevelManagers, m) }
4545 ids := make([]ports.WindowID, 0, len(s.windows))
4646 for id, w := range s.windows {
47- if w.toplevel != nil && w.mapped {
47+ if w.isToplevel() && w.mapped {
4848 ids = append(ids, id)
4949 }
5050 }
@@ -186,7 +186,7 @@ func toplevelState(w *window, version int32) []byte {
186186
187187 // toplevelChanged updates every manager after w mapped or changed.
188188 func (s *Server) toplevelChanged(w *window) {
189- if w.toplevel == nil || !w.mapped {
189+ if !w.isToplevel() || !w.mapped {
190190 return
191191 }
192192 for _, m := range s.toplevelManagers {
@@ -220,7 +220,7 @@ func (s *Server) refreshToplevels() {
220220 for _, l := range s.extToplevelLists {
221221 for id, h := range l.handles {
222222 w := s.windows[id]
223- if h.closed || w == nil || w.toplevel == nil || !w.mapped {
223+ if h.closed || w == nil || !w.isToplevel() || !w.mapped {
224224 l.closed(id)
225225 }
226226 }
@@ -231,7 +231,7 @@ func (s *Server) refreshToplevels() {
231231 for _, m := range s.toplevelManagers {
232232 for id, h := range m.handles {
233233 w := s.windows[id]
234- if h.req.closed || w == nil || w.toplevel == nil || !w.mapped {
234+ if h.req.closed || w == nil || !w.isToplevel() || !w.mapped {
235235 m.closed(id)
236236 }
237237 }
@@ -253,7 +253,7 @@ func (t *toplevelRequests) window() *window {
253253 if t.s.protected() || t.closed {
254254 return nil
255255 }
256- if w := t.s.windows[t.id]; w != nil && w.toplevel != nil && w.mapped && w.toplevel.Resource.Alive() {
256+ if w := t.s.windows[t.id]; w != nil && w.toplevelAlive() && w.mapped {
257257 return w
258258 }
259259 return nil
@@ -274,7 +274,7 @@ func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.S
274274 }
275275 func (t *toplevelRequests) Close(*wlr.ZwlrForeignToplevelHandleV1) {
276276 if w := t.window(); w != nil {
277- w.toplevel.SendClose()
277+ w.sendClose()
278278 }
279279 }
280280 func (t *toplevelRequests) SetRectangle(r *wlr.ZwlrForeignToplevelHandleV1, _ *wayland.Surface, _, _, w, h int32) {
M internal/adapters/wayland/frames.go +10 -6
@@ -183,7 +183,7 @@ func (s *Server) pacePass(flipped map[string]bool, reports []ports.OutputPresent
183183 func toplevelRoot(surf *surface) *surface {
184184 surf = surf.root()
185185 for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil {
186- surf = surf.xdg.window.popup.parent.xdg.surface.root()
186+ surf = surf.xdg.window.popup.parent.surf().root()
187187 }
188188 return surf
189189 }
@@ -193,7 +193,8 @@ func toplevelRoot(surf *surface) *surface {
193193 // for a capture session is invisible.
194194 func (s *Server) invisible(surf *surface) bool {
195195 root := toplevelRoot(surf)
196- return root.xdg != nil && root.xdg.window != nil && root.xdg.window.hasLast && !root.xdg.window.last.Visible
196+ w := root.toplevelWindow()
197+ return w != nil && w.hasLast && !w.last.Visible
197198 }
198199
199200 // suspended reports that nothing draws the toplevel tree, not even a capture
@@ -201,10 +202,11 @@ func (s *Server) invisible(surf *surface) bool {
201202 // frame callbacks of its output so the capture keeps moving.
202203 func (s *Server) suspended(surf *surface) bool {
203204 root := toplevelRoot(surf)
204- if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast {
205+ w := root.toplevelWindow()
206+ if w == nil || !w.hasLast {
205207 return false
206208 }
207- return !root.xdg.window.last.Visible && !root.xdg.window.last.Captured
209+ return !w.last.Visible && !w.last.Captured
208210 }
209211
210212 // frameOutput is the pacing class for a surface; "" is the 60 Hz
@@ -212,11 +214,13 @@ func (s *Server) suspended(surf *surface) bool {
212214 func (s *Server) frameOutput(surf *surface) string {
213215 surf = toplevelRoot(surf)
214216 switch {
215- case surf.xdg != nil && surf.xdg.window != nil:
217+ case surf.x11win != nil && surf.x11win.isOverride():
218+ return s.overrideOutput(surf.x11win)
219+ case surf.toplevelWindow() != nil:
216220 if s.suspended(surf) {
217221 return suspendedFrameQueue
218222 }
219- return surf.xdg.window.last.Output
223+ return surf.toplevelWindow().last.Output
220224 case surf.layer != nil && surf.layer.output != nil:
221225 return surf.layer.output.name()
222226 case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed:
M internal/adapters/wayland/globals.go +10 -1
@@ -13,6 +13,7 @@ import (
1313 func registerGlobals(d *server.Display, o Options, s *Server) error {
1414 for _, register := range []func() error{
1515 func() error { return registerXDG(d, s) },
16+ func() error { return registerXwaylandShell(d, s) },
1617 func() error { return registerLayer(d, s) },
1718 func() error {
1819 // Advertise only with the production owner gate and authoritative
@@ -131,8 +132,16 @@ func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) {
131132 state.identity = c.server.nextSurface
132133 state.wl = w
133134 c.server.surfaces[w.Resource] = state
135+ c.server.surfaceByIdentity[state.identity] = state
134136 state.sendScale()
135- w.OnDestroy = func() { delete(c.server.surfaces, w.Resource); state.Destroy(w) }
137+ w.OnDestroy = func() {
138+ delete(c.server.surfaces, w.Resource)
139+ delete(c.server.surfaceByIdentity, state.identity)
140+ state.Destroy(w)
141+ if state.kind == roleX11 && c.server.xwm != nil {
142+ c.server.xwm.SurfaceGone(state.identity)
143+ }
144+ }
136145 }
137146 }
138147 func (c compositor) CreateRegion(r *wayland.Compositor, id uint32) {
M internal/adapters/wayland/imagecopy.go +1 -1
@@ -351,7 +351,7 @@ func (c *captureTarget) geometry() (o *output, rect image.Rectangle, hidden, ok
351351 case srcWindow:
352352 // Core's state gives the size; before it, the last configure.
353353 w := c.s.windows[src.window]
354- if w == nil || !w.mapped || w.toplevel == nil || w.gen != src.gen {
354+ if w == nil || !w.mapped || !w.isToplevel() || w.gen != src.gen {
355355 return nil, image.Rectangle{}, false, false
356356 }
357357 name, size := w.last.Output, ports.Rect{W: w.last.Width, H: w.last.Height}
M internal/adapters/wayland/outputs.go +72 -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,36 @@ func (o *output) sendXDG(x *xdgoutput.ZxdgOutputV1, withName bool) {
8991 }
9092 }
9193
94+// sendXDGX sends the private xdg-output state: the layout position moved to
95+// X space and the logical size.
96+func (o *output) sendXDGX(s *Server, x *xdgoutput.ZxdgOutputV1, withName bool) {
97+ ox, oy := s.xspace.Origin()
98+ x.SendLogicalPosition(int32(o.place.X-ox), int32(o.place.Y-oy))
99+ x.SendLogicalSize(int32(o.place.Width), int32(o.place.Height))
100+ if withName && x.Version() >= 2 {
101+ x.SendName(o.name())
102+ x.SendDescription(o.description())
103+ }
104+}
105+
106+// republishXDGX resends every private xdg-output after a layout change, so
107+// Xwayland resizes the root and its RandR outputs.
108+func (s *Server) republishXDGX() {
109+ for _, o := range s.outputs {
110+ for _, x := range o.xdgOutputsX {
111+ o.sendXDGX(s, x, false)
112+ if x.Version() < 3 {
113+ x.SendDone()
114+ }
115+ }
116+ for _, r := range o.resources {
117+ if r.Client() == s.xwayland.client && r.Version() >= 2 {
118+ r.SendDone()
119+ }
120+ }
121+ }
122+}
123+
92124 // outputOf returns the output a wl_output resource belongs to, or nil for a
93125 // removed one.
94126 func (s *Server) outputOf(r *wayland.Output) *output {
@@ -162,6 +194,10 @@ func (s *Server) setOutputs(c ports.SetOutputs) {
162194 changed := !slices.Equal(s.outputPlaces, c.Outputs)
163195 focusChanged := s.focusedOutput != c.Focused
164196 s.outputPlaces = append(ports.Layout(nil), c.Outputs...)
197+ xChanged := s.xspace.Set(s.outputPlaces)
198+ if s.xwm != nil {
199+ s.xwm.LayoutChanged(xChanged)
200+ }
165201 defer func() {
166202 if changed {
167203 s.refreshOutputManagers()
@@ -244,6 +280,9 @@ func (s *Server) setOutputs(c ports.SetOutputs) {
244280 for _, surf := range s.surfaces {
245281 surf.sendScale()
246282 }
283+ if changed {
284+ s.republishXDGX()
285+ }
247286 s.notifyColorFeedback()
248287 // Layer surfaces sized from the output follow its new logical size.
249288 adopted := false
@@ -279,10 +318,42 @@ type outputHandler struct{}
279318
280319 func (outputHandler) Release(*wayland.Output) {}
281320
321+// registerXDGOutput registers the public zxdg_output_manager_v1 and a second,
322+// private one for Xwayland whose outputs are in X space (see globalVisible).
282323 func registerXDGOutput(d *server.Display, s *Server) error {
283- return xdgoutput.NewZxdgOutputManagerV1Global(d, 3, func(c server.Client, v, id uint32) {
324+ var err error
325+ s.xdgPublic, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) {
284326 _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s})
285327 })
328+ if err != nil {
329+ return err
330+ }
331+ s.xdgPrivate, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) {
332+ _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManagerX{s})
333+ })
334+ return err
335+}
336+
337+type xdgOutputManagerX struct{ server *Server }
338+
339+func (xdgOutputManagerX) Destroy(*xdgoutput.ZxdgOutputManagerV1) {}
340+func (m xdgOutputManagerX) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint32, wl *wayland.Output) {
341+ x, err := xdgoutput.NewZxdgOutputV1(r.Client(), r.Version(), id, xdgOutput{})
342+ if err != nil {
343+ return
344+ }
345+ o := m.server.outputOf(wl)
346+ if o == nil {
347+ return
348+ }
349+ o.xdgOutputsX = append(o.xdgOutputsX, x)
350+ x.OnDestroy = func() { o.xdgOutputsX = removeItem(o.xdgOutputsX, x) }
351+ o.sendXDGX(m.server, x, true)
352+ if r.Version() < 3 || wl.Version() < 2 {
353+ x.SendDone()
354+ } else {
355+ wl.SendDone()
356+ }
286357 }
287358
288359 type xdgOutputManager struct{ server *Server }
M internal/adapters/wayland/popup.go +2 -2
@@ -31,7 +31,7 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX
3131 var parent *window
3232 if parentXDG != nil {
3333 for _, w := range s.windows {
34- if w.xdg.resource != nil && w.xdg.resource.Resource == parentXDG.Resource {
34+ if w.xdg != nil && w.xdg.resource != nil && w.xdg.resource.Resource == parentXDG.Resource {
3535 parent = w
3636 }
3737 }
@@ -166,7 +166,7 @@ func (p *popup) configure(c ports.ConfigurePopup) {
166166 func (s *Server) topGrab(client server.Client, parent *window) bool {
167167 var top *window
168168 for _, w := range s.windows {
169- if w.popup != nil && w.popup.grab && !w.popup.done && w.xdg.resource.Client() == client && (top == nil || w.id > top.id) {
169+ if w.popup != nil && w.popup.grab && !w.popup.done && w.client() == client && (top == nil || w.id > top.id) {
170170 top = w
171171 }
172172 }
M internal/adapters/wayland/scale.go +5 -3
@@ -21,12 +21,14 @@ func (s *Server) outputOfSurface(surf *surface) *output {
2121 surf = surf.root()
2222 // Popups are on their parent's output.
2323 for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil {
24- surf = surf.xdg.window.popup.parent.xdg.surface.root()
24+ surf = surf.xdg.window.popup.parent.surf().root()
2525 }
2626 name := ""
2727 switch {
28- case surf.xdg != nil && surf.xdg.window != nil:
29- name = surf.xdg.window.last.Output
28+ case surf.x11win != nil && surf.x11win.isOverride():
29+ name = s.overrideOutput(surf.x11win)
30+ case surf.toplevelWindow() != nil:
31+ name = surf.toplevelWindow().last.Output
3032 case surf.layer != nil && surf.layer.output != nil:
3133 return surf.layer.output
3234 case surf.lock != nil:
M internal/adapters/wayland/screencast.go +1 -1
@@ -146,7 +146,7 @@ func (s *Server) openCast(cmd ports.CastCommand) {
146146 gone := ports.CaptureReasonOutputGone
147147 if cmd.Window != 0 {
148148 gone = ports.CaptureReasonWindowGone
149- if w := s.windows[cmd.Window]; w != nil && w.mapped && w.toplevel != nil {
149+ if w := s.windows[cmd.Window]; w != nil && w.mapped && w.isToplevel() {
150150 src = &captureSource{kind: srcWindow, window: cmd.Window, gen: w.gen}
151151 open.Window = cmd.Window
152152 }
M internal/adapters/wayland/seat.go +10 -9
@@ -276,10 +276,10 @@ func (s *Server) setModifiers(c ports.SetModifiers) {
276276 }
277277
278278 func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
279- if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
279+ if w == nil || !w.mapped || !w.alive() {
280280 return nil
281281 }
282- return s.clientKeyboards(w.xdg.resource.Client())
282+ return s.clientKeyboards(w.client())
283283 }
284284
285285 // clientKeyboards returns c's wl_keyboards without allocating. The list holds
@@ -300,8 +300,8 @@ func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Ke
300300 // already dead. Admission is enforced before entering or delivering.
301301 return l.surface.wl, s.clientKeyboards(l.surface.wl.Client())
302302 }
303- if w := s.windows[id]; w != nil && w.mapped {
304- if surf := w.xdg.surfaceResource(); surf != nil {
303+ if w := s.windows[id]; w != nil && w.mapped && !w.isOverride() {
304+ if surf := w.surfaceResource(); surf != nil {
305305 return surf, s.windowKeyboards(w)
306306 }
307307 }
@@ -319,8 +319,8 @@ func (s *Server) pointerTarget(id ports.WindowID) (server.Client, []*wayland.Poi
319319 if l := s.lockSurfaces[id]; l != nil && s.lockInputTarget(id) {
320320 return l.resource.Client(), s.clientPointers(l.resource.Client()), true
321321 }
322- if w := s.windows[id]; w != nil && w.mapped && w.xdg.resource.Resource.Alive() {
323- return w.xdg.resource.Client(), s.windowPointers(w), true
322+ if w := s.windows[id]; w != nil && w.mapped && w.alive() {
323+ return w.client(), s.windowPointers(w), true
324324 }
325325 if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
326326 return l.resource.Client(), s.layerPointers(l), true
@@ -335,7 +335,7 @@ func (s *Server) pointerSurface(id ports.WindowID, x, y float64) (*wayland.Surfa
335335 return l.surface.wl, s.clientPointers(l.surface.wl.Client()), x, y
336336 }
337337 if w := s.windows[id]; w != nil {
338- if surf := w.xdg.surfaceResource(); surf != nil && surf.Resource.Alive() {
338+ if surf := w.surfaceResource(); surf != nil && surf.Resource.Alive() {
339339 x, y := w.surfacePoint(x, y)
340340 return surf, s.windowPointers(w), x, y
341341 }
@@ -455,6 +455,7 @@ func (s *Server) changeFocus(id ports.WindowID) {
455455 }
456456 }
457457 s.textInputFocus()
458+ s.x11Focus()
458459 s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus")
459460 s.updateConstraint()
460461 }
@@ -543,10 +544,10 @@ func pointerFrame(p *wayland.Pointer) {
543544 }
544545 }
545546 func (s *Server) windowPointers(w *window) []*wayland.Pointer {
546- if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
547+ if w == nil || !w.mapped || !w.alive() {
547548 return nil
548549 }
549- return s.clientPointers(w.xdg.resource.Client())
550+ return s.clientPointers(w.client())
550551 }
551552
552553 func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer {
M internal/adapters/wayland/server.go +40 -16
@@ -16,6 +16,7 @@ import (
1616 "github.com/bnema/neferwl/internal/adapters/captureallow"
1717 "github.com/bnema/neferwl/internal/adapters/clock"
1818 "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock"
19+ "github.com/bnema/neferwl/internal/adapters/wayland/xwm"
1920 "github.com/bnema/neferwl/internal/adapters/workspaceid"
2021 "github.com/bnema/neferwl/internal/ports"
2122 "github.com/bnema/purego-libwayland/server"
@@ -160,13 +161,21 @@ type Server struct {
160161 securityStop int
161162 securityWG sync.WaitGroup
162163 securityLive atomic.Int32
163- display *server.Display
164- env procEnv
165- name string
166- cleanup func()
167- log zerowrap.Logger
168- logDamage bool
169- channels Channels
164+ 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
168+ // xwm manages the X11 windows of the Xwayland client; x11Slots maps its
169+ // slots to their windows. Display goroutine only.
170+ xwm *xwm.XWM
171+ x11Slots map[xwm.Slot]*window
172+ display *server.Display
173+ env procEnv
174+ name string
175+ cleanup func()
176+ log zerowrap.Logger
177+ logDamage bool
178+ channels Channels
170179 // awaiting holds frame callbacks by output name, due at its next
171180 // frame (frameDue, or its page flip).
172181 awaiting map[string][]*wayland.Callback
@@ -214,8 +223,11 @@ type Server struct {
214223 // foreignExports are the live xdg-foreign handles (foreign.go).
215224 foreignExports map[string]*foreignExport
216225 surfaces map[*server.Resource]*surface
217- buffers map[*server.Resource]clientBuffer
218- dmabuf *dmabufGlobal
226+ // surfaceByIdentity finds a surface by its identity (xwayland_shell_v1
227+ // pairing); kept with surfaces.
228+ surfaceByIdentity map[uint64]*surface
229+ buffers map[*server.Resource]clientBuffer
230+ dmabuf *dmabufGlobal
219231 // hdrOutputs records confirmed DRM modesets; absent outputs are SDR.
220232 hdrOutputs map[string]ports.OutputHDR
221233 colorID uint64
@@ -281,6 +293,7 @@ type Server struct {
281293 workspaceIDs *workspaceid.IDs
282294 outputHeads ports.OutputHeads
283295 outputPlaces ports.Layout
296+ xspace xwm.Space // the one layout <-> X mapping, shared with the XWM
284297 managementSerial uint32
285298 nextOutputApply uint64
286299 outputReplies map[uint64]*outputConfiguration
@@ -366,8 +379,10 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
366379 logDamage: opts.LogDamage,
367380 channels: ch,
368381 surfaces: make(map[*server.Resource]*surface),
382+ surfaceByIdentity: make(map[uint64]*surface),
369383 buffers: make(map[*server.Resource]clientBuffer),
370384 windows: make(map[ports.WindowID]*window),
385+ x11Slots: make(map[xwm.Slot]*window),
371386 layers: make(map[ports.WindowID]*layerSurface),
372387 nextWindow: 1,
373388 eventReady: make(chan struct{}, 1),
@@ -498,6 +513,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
498513 if err = s.openPaceTimer(); err != nil {
499514 return abort(err)
500515 }
516+ d.SetGlobalFilter(s.globalVisible)
501517 if err = registerGlobals(d, opts, s); err != nil {
502518 return abort(err)
503519 }
@@ -585,6 +601,12 @@ func (s *Server) Run(ctx context.Context) error {
585601 }
586602 s.castInventoryChanged()
587603 err := s.display.Run(ctx)
604+ // The display is gone: only the X connection is left to close.
605+ if s.xwm != nil {
606+ s.xwm.CloseConn()
607+ s.xwm = nil
608+ }
609+ s.forgetXwayland()
588610 s.stopSecurityContexts()
589611 wg.Wait()
590612 return err
@@ -774,9 +796,11 @@ func (s *Server) apply(cmd ports.ClientCommand) {
774796 if w := s.windows[c.ID]; w != nil && w.popup != nil {
775797 w.popup.dismiss()
776798 }
799+ case ports.PlaceWindow:
800+ s.x11Place(c)
777801 case ports.ConfigureWindow:
778802 w := s.windows[c.ID]
779- if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() {
803+ if w == nil || !w.toplevelAlive() || !w.alive() {
780804 s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window")
781805 return
782806 }
@@ -787,16 +811,16 @@ func (s *Server) apply(cmd ports.ClientCommand) {
787811 scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output || w.last.Visible != c.Visible
788812 w.last, w.hasLast = c, true
789813 if visibilityChanged {
790- s.relocateCallbacks(w.xdg.surface)
814+ s.relocateCallbacks(w.surf())
791815 s.dropFeedbacks(time.Now())
792816 s.wakePacer() // reconsider FIFO barriers at the new period
793817 }
794818 w.sendConfigure()
795819 s.toplevelChanged(w)
796- if scanoutChanged && w.xdg.surface != nil {
797- s.dmabuf.resendSurface(w.xdg.surface)
820+ if scanoutChanged && w.surf() != nil {
821+ s.dmabuf.resendSurface(w.surf())
798822 }
799- if surf := w.xdg.surface; surf != nil && c.Output != "" {
823+ if surf := w.surf(); surf != nil && c.Output != "" {
800824 surf.sendTreeScale()
801825 }
802826 if scanoutChanged {
@@ -817,10 +841,10 @@ func (s *Server) apply(cmd ports.ClientCommand) {
817841 s.setShortcutsInhibit(c)
818842 case ports.CloseWindow:
819843 w := s.windows[c.ID]
820- if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() {
844+ if w == nil || !w.toplevelAlive() {
821845 s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window")
822846 return
823847 }
824- w.toplevel.SendClose()
848+ w.sendClose()
825849 }
826850 }
M internal/adapters/wayland/surface.go +15 -2
@@ -28,6 +28,9 @@ const (
2828 roleSessionLock
2929 // roleDragIcon is the icon of a data drag (dnd.go).
3030 roleDragIcon
31+ // roleX11 is the surface of an X11 window paired through
32+ // xwayland_shell_v1.
33+ roleX11
3134 )
3235
3336 type surface struct {
@@ -47,8 +50,12 @@ type surface struct {
4750 formatOpaque bool
4851 kind roleKind
4952 xdg *xdgSurface
50- layer *layerSurface
51- lock *lockSurface
53+ // x11win is the X11 window the surface is associated with; x11Serial is
54+ // the serial of xwayland_surface_v1.set_serial, 0 until set.
55+ x11win *window
56+ x11Serial uint64
57+ layer *layerSurface
58+ lock *lockSurface
5259 // bufferCommitted is permanent, including commits later discarded by FIFO.
5360 bufferCommitted bool
5461 server *Server
@@ -192,6 +199,9 @@ func (s *surface) windowID() ports.WindowID {
192199 if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped {
193200 return s.xdg.window.id
194201 }
202+ if s.x11win != nil && s.x11win.mapped {
203+ return s.x11win.id
204+ }
195205 if s.layer != nil && s.layer.mapped {
196206 return s.layer.id
197207 }
@@ -566,6 +576,9 @@ func (s *surface) applyCommit(u *update) {
566576 if s.xdg != nil && s.xdg.window != nil {
567577 s.xdg.window.afterCommit()
568578 }
579+ if s.x11win != nil {
580+ s.x11win.afterCommit()
581+ }
569582 if s.lock != nil {
570583 // Role callbacks precede content construction; publish lock placement
571584 // only once validated current dimensions are available.
A internal/adapters/wayland/window.go +112 -0
@@ -0,0 +1,112 @@
1+package wayland
2+
3+import (
4+ "github.com/bnema/go-wayland-bindings/server/wayland"
5+ "github.com/bnema/neferwl/internal/ports"
6+ "github.com/bnema/purego-libwayland/server"
7+)
8+
9+// Role-independent accessors of a window. Code outside xdg.go goes through
10+// them instead of dereferencing w.xdg, which is nil for windows of another
11+// role.
12+
13+// isToplevel reports whether w is a toplevel (xdg or X11), not a popup.
14+func (w *window) isToplevel() bool { return w.toplevel != nil || (w.x11 != nil && !w.x11.override) }
15+
16+// srv is the server the window belongs to.
17+func (w *window) srv() *Server {
18+ if w.xdg != nil {
19+ return w.xdg.server
20+ }
21+ return w.x11.server
22+}
23+
24+// surf is the window's surface, nil when it has none.
25+func (w *window) surf() *surface {
26+ switch {
27+ case w.xdg != nil:
28+ return w.xdg.surface
29+ case w.x11 != nil:
30+ return w.x11.surface
31+ }
32+ return nil
33+}
34+
35+// alive reports whether the window's role object is alive.
36+func (w *window) alive() bool {
37+ switch {
38+ case w.xdg != nil:
39+ return w.xdg.resource.Resource.Alive()
40+ case w.x11 != nil:
41+ return w.x11.surface != nil && w.x11.surface.wl.Resource.Alive()
42+ }
43+ return false
44+}
45+
46+// client is the window's client; the zero Client when it has no role object.
47+func (w *window) client() server.Client {
48+ switch {
49+ case w.xdg != nil:
50+ return w.xdg.resource.Client()
51+ case w.x11 != nil && w.x11.surface != nil:
52+ return w.x11.surface.wl.Client()
53+ }
54+ return server.Client{}
55+}
56+
57+// geometry is the committed window geometry; empty means the whole surface.
58+func (w *window) geometry() ports.Rect {
59+ if w.xdg != nil {
60+ return w.xdg.geometry
61+ }
62+ return ports.Rect{}
63+}
64+
65+// sendConfigure sends w.last to the client.
66+func (w *window) sendConfigure() {
67+ switch {
68+ case w.xdg != nil:
69+ w.sendXDGConfigure()
70+ case w.x11 != nil:
71+ w.x11.server.x11Configure(w)
72+ }
73+}
74+
75+// sendClose asks the client to close the window.
76+func (w *window) sendClose() {
77+ switch {
78+ case w.toplevel != nil:
79+ w.toplevel.SendClose()
80+ case w.x11 != nil:
81+ w.x11.server.x11Close(w)
82+ }
83+}
84+
85+// toplevelAlive reports whether the window is a toplevel whose protocol
86+// object is alive.
87+func (w *window) toplevelAlive() bool {
88+ if w.x11 != nil {
89+ return w.alive()
90+ }
91+ return w.toplevel != nil && w.toplevel.Resource.Alive()
92+}
93+
94+// surfaceResource is the window's wl_surface, nil when it has none or it died.
95+func (w *window) surfaceResource() *wayland.Surface {
96+ switch {
97+ case w.xdg != nil:
98+ return w.xdg.surfaceResource()
99+ case w.x11 != nil && w.alive():
100+ return w.x11.surface.wl
101+ }
102+ return nil
103+}
104+
105+// toplevelWindow is the toplevel window (xdg or X11) the surface is the role
106+// surface of, nil for other roles and popups' parents are not followed.
107+func (s *surface) toplevelWindow() *window {
108+ if s.xdg != nil && s.xdg.window != nil {
109+ return s.xdg.window
110+ }
111+ return s.x11win
112+}
M internal/adapters/wayland/wire_client_test.go +3 -0
@@ -55,6 +55,7 @@ import (
5555 wire_xdgforeign "github.com/bnema/go-wayland-bindings/server/xdgforeign"
5656 wire_xdgoutput "github.com/bnema/go-wayland-bindings/server/xdgoutput"
5757 wire_xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell"
58+ wire_xwaylandshell "github.com/bnema/go-wayland-bindings/server/xwaylandshell"
5859 wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture"
5960 "github.com/bnema/purego-libwayland/server"
6061 "github.com/bnema/wlturbo"
@@ -253,6 +254,8 @@ var wireSchemas = map[string]wireSchema{
253254 "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil},
254255 "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil},
255256 "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil},
257+ "xwayland_shell_v1": {wire_xwaylandshell.XwaylandShellV1Interface, nil, map[uint16]bool{0: true}, nil},
258+ "xwayland_surface_v1": {wire_xwaylandshell.XwaylandSurfaceV1Interface, nil, map[uint16]bool{1: true}, nil},
256259 "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil},
257260 "zxdg_output_v1": {wire_xdgoutput.ZxdgOutputV1Interface, &client_xdgoutput.ZxdgOutput{}, map[uint16]bool{0: true}, nil},
258261 }
A internal/adapters/wayland/x11override_test.go +107 -0
@@ -0,0 +1,107 @@
1+package wayland
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/go-wayland-bindings/server/wayland"
7+ "github.com/bnema/neferwl/internal/adapters/wayland/xwm"
8+ "github.com/bnema/neferwl/internal/ports"
9+)
10+
11+const ovWin = 0x500002
12+
13+// overrideWindow maps an override-redirect X window and pairs its surface.
14+func (r *x11Rig) overrideWindow(win uint32, x, y int16, serial uint64) uint32 {
15+ r.t.Helper()
16+ r.peer.createOverride(win, x, y)
17+ r.peer.mapNotify(win)
18+ return r.pair(win, serial)
19+}
20+
21+func (r *x11Rig) state(f func()) {
22+ r.t.Helper()
23+ r.s.display.Do(f)
24+}
25+
26+func TestOverrideMapsOnFirstCommitAndMoves(t *testing.T) {
27+ r := newX11Rig(t)
28+ r.overrideWindow(ovWin, 30, 40, x11Serial)
29+
30+ m := awaitEvent[ports.UnmanagedMapped](t, r.events)
31+ if m.X != 30 || m.Y != 40 || m.Width != 100 || m.Height != 50 || m.NoInput {
32+ t.Fatalf("mapped %+v", m)
33+ }
34+ r.state(func() {
35+ w := r.s.windows[m.ID]
36+ if w == nil || !w.isOverride() || w.isToplevel() || !w.mapped {
37+ t.Errorf("override window %+v", w)
38+ }
39+ if surf, _ := r.s.focusTarget(m.ID); surf != nil {
40+ t.Error("override window is a keyboard focus target")
41+ }
42+ })
43+
44+ r.peer.configureNotify(ovWin, 60, 70, 100, 50)
45+ if mv := awaitEvent[ports.UnmanagedMoved](t, r.events); mv.ID != m.ID || mv.X != 60 || mv.Y != 70 {
46+ t.Fatalf("moved %+v", mv)
47+ }
48+
49+ r.peer.unmapNotify(ovWin)
50+ if u := awaitEvent[ports.UnmanagedUnmapped](t, r.events); u.ID != m.ID {
51+ t.Fatalf("unmapped %+v", u)
52+ }
53+ r.state(func() {
54+ if r.s.windows[m.ID] != nil {
55+ t.Error("override window kept after unmap")
56+ }
57+ })
58+}
59+
60+func TestOverrideSurfaceDestroyUnmaps(t *testing.T) {
61+ r := newX11Rig(t)
62+ surf := r.overrideWindow(ovWin, 0, 0, x11Serial)
63+ m := awaitEvent[ports.UnmanagedMapped](t, r.events)
64+ requestProtocol(t, r.xc, surf, wayland.SurfaceRequestDestroy)
65+ if err := r.xc.Roundtrip(); err != nil {
66+ t.Fatal(err)
67+ }
68+ if u := awaitEvent[ports.UnmanagedUnmapped](t, r.events); u.ID != m.ID {
69+ t.Fatalf("unmapped %+v", u)
70+ }
71+}
72+
73+func TestOverrideTakesPointerAndOutput(t *testing.T) {
74+ r := newX11Rig(t)
75+ r.commands <- ports.SetOutputs{Outputs: ports.Layout{
76+ {Info: ports.OutputInfo{Name: "A"}, X: 0, Y: 0, Width: 1920, Height: 1080, Scale: 1},
77+ {Info: ports.OutputInfo{Name: "B"}, X: 1920, Y: 0, Width: 1920, Height: 1080, Scale: 1},
78+ }, Focused: "A"}
79+ r.overrideWindow(ovWin, 100, 10, x11Serial)
80+ m := awaitEvent[ports.UnmanagedMapped](t, r.events)
81+ r.state(func() {
82+ w := r.s.windows[m.ID]
83+ if got := r.s.frameOutput(w.x11.surface); got != "A" {
84+ t.Errorf("frame output %q, want A", got)
85+ }
86+ if surf, _, _, _ := r.s.pointerSurface(m.ID, 1, 1); surf == nil {
87+ t.Error("pointer cannot enter the override surface")
88+ }
89+ })
90+ r.peer.configureNotify(ovWin, 2000, 10, 100, 50)
91+ awaitEvent[ports.UnmanagedMoved](t, r.events)
92+ r.state(func() {
93+ if got := r.s.frameOutput(r.s.windows[m.ID].x11.surface); got != "B" {
94+ t.Errorf("frame output %q after move, want B", got)
95+ }
96+ })
97+}
98+
99+func TestOverrideMovedUnchangedAllocations(t *testing.T) {
100+ s := &Server{windows: map[ports.WindowID]*window{}, x11Slots: map[xwm.Slot]*window{}}
101+ w := &window{id: 1, x11: &x11Window{override: true, rect: ports.Rect{X: 1, Y: 2, W: 3, H: 4}}}
102+ s.x11Slots[1] = w
103+ h := hostImpl{s}
104+ if n := testing.AllocsPerRun(1000, func() { h.OverrideMoved(1, 1, 2, 3, 4) }); n != 0 {
105+ t.Fatalf("%v allocs", n)
106+ }
107+}
A internal/adapters/wayland/x11peer_test.go +332 -0
@@ -0,0 +1,332 @@
1+package wayland
2+
3+import (
4+ "encoding/binary"
5+ "io"
6+ "os"
7+ "sync"
8+ "testing"
9+ "time"
10+
11+ "golang.org/x/sys/unix"
12+)
13+
14+// x11Peer is a scripted X server speaking the real wire protocol over a
15+// socketpair: setup, extensions, atoms, properties, geometry. It records every
16+// request. It is a protocol peer, not a double of a project interface.
17+type x11Peer struct {
18+ t *testing.T
19+ f *os.File
20+ mu sync.Mutex
21+
22+ seq uint16
23+ reqs []x11Req
24+ atoms map[string]uint32
25+ nextAtom uint32
26+ props map[uint32]map[uint32][]byte // window -> atom -> typed data (type, format, data)
27+ propMeta map[uint32]map[uint32][2]uint32
28+ done chan struct{}
29+}
30+
31+type x11Req struct {
32+ major, minor uint8
33+ raw []byte
34+}
35+
36+func (r x11Req) u32(off int) uint32 { return binary.LittleEndian.Uint32(r.raw[off:]) }
37+
38+const (
39+ x11PeerRoot = 0x100
40+ x11Win = 0x500001
41+ // Opcodes the tests look for.
42+ x11OpConfigureWindow = 12
43+ x11OpSetInputFocus = 42
44+ x11OpKillClient = 113
45+)
46+
47+func startX11Peer(t *testing.T) (int, *x11Peer) {
48+ t.Helper()
49+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
50+ if err != nil {
51+ t.Fatal(err)
52+ }
53+ p := &x11Peer{
54+ t: t, f: os.NewFile(uintptr(fds[1]), "x11peer"), atoms: map[string]uint32{}, nextAtom: 1000,
55+ props: map[uint32]map[uint32][]byte{}, propMeta: map[uint32]map[uint32][2]uint32{}, done: make(chan struct{}),
56+ }
57+ t.Cleanup(func() { p.f.Close(); <-p.done })
58+ go p.serve()
59+ return fds[0], p
60+}
61+
62+func (p *x11Peer) setupReply() []byte {
63+ vendor := []byte("x11pr\x00\x00\x00")
64+ b := make([]byte, 40+len(vendor)+40)
65+ b[0], b[2] = 1, 11
66+ binary.LittleEndian.PutUint16(b[6:], uint16((len(b)-8)/4))
67+ binary.LittleEndian.PutUint32(b[12:], 0x200000)
68+ binary.LittleEndian.PutUint32(b[16:], 0x1fffff)
69+ binary.LittleEndian.PutUint16(b[24:], 5)
70+ b[28] = 1
71+ copy(b[40:], vendor)
72+ scr := b[40+len(vendor):]
73+ binary.LittleEndian.PutUint32(scr, x11PeerRoot)
74+ binary.LittleEndian.PutUint32(scr[32:], 0x22)
75+ scr[38] = 24
76+ return b
77+}
78+
79+func (p *x11Peer) serve() {
80+ defer close(p.done)
81+ var hdr [12]byte
82+ if _, err := io.ReadFull(p.f, hdr[:]); err != nil {
83+ return
84+ }
85+ if _, err := p.f.Write(p.setupReply()); err != nil {
86+ return
87+ }
88+ for {
89+ var h [4]byte
90+ if _, err := io.ReadFull(p.f, h[:]); err != nil {
91+ return
92+ }
93+ raw := h[:]
94+ n := int(binary.LittleEndian.Uint16(h[2:])) * 4
95+ if n == 0 {
96+ var l [4]byte
97+ if _, err := io.ReadFull(p.f, l[:]); err != nil {
98+ return
99+ }
100+ raw = append(raw, l[:]...)
101+ n = int(binary.LittleEndian.Uint32(l[:])) * 4
102+ }
103+ rest := make([]byte, n-len(raw))
104+ if _, err := io.ReadFull(p.f, rest); err != nil {
105+ return
106+ }
107+ raw = append(append([]byte(nil), raw...), rest...)
108+ p.mu.Lock()
109+ p.seq++
110+ r := x11Req{h[0], h[1], raw}
111+ p.reqs = append(p.reqs, r)
112+ p.answer(r, raw[len(raw)-len(rest):])
113+ p.mu.Unlock()
114+ }
115+}
116+
117+func (p *x11Peer) reply(data uint8, body []byte) {
118+ words := (max(len(body), 24) - 24 + 3) / 4
119+ b := make([]byte, 32+4*words)
120+ b[0], b[1] = 1, data
121+ binary.LittleEndian.PutUint16(b[2:], p.seq)
122+ binary.LittleEndian.PutUint32(b[4:], uint32(words))
123+ copy(b[8:], body)
124+ _, _ = p.f.Write(b)
125+}
126+
127+func x11le32(v ...uint32) []byte {
128+ b := make([]byte, 0, 4*len(v))
129+ for _, x := range v {
130+ b = binary.LittleEndian.AppendUint32(b, x)
131+ }
132+ return b
133+}
134+
135+var x11Exts = map[string]uint8{"BIG-REQUESTS": 133, "XC-MISC": 136, "Composite": 142}
136+
137+func (p *x11Peer) answer(r x11Req, body []byte) {
138+ switch {
139+ case r.major == 98: // QueryExtension
140+ major, ok := x11Exts[string(body[4:4+binary.LittleEndian.Uint16(body)])]
141+ present := uint8(0)
142+ if ok {
143+ present = 1
144+ }
145+ p.reply(0, []byte{present, major, 0, 0})
146+ case r.major == 16: // InternAtom
147+ name := string(body[4 : 4+binary.LittleEndian.Uint16(body)])
148+ a, ok := p.atoms[name]
149+ if !ok {
150+ p.nextAtom++
151+ a = p.nextAtom
152+ p.atoms[name] = a
153+ }
154+ p.reply(0, x11le32(a))
155+ case r.major == 43: // GetInputFocus (Sync)
156+ p.reply(1, x11le32(0))
157+ case r.major == 15: // QueryTree
158+ b := make([]byte, 24)
159+ binary.LittleEndian.PutUint32(b, x11PeerRoot)
160+ p.reply(0, b)
161+ case r.major == 14: // GetGeometry
162+ b := make([]byte, 24)
163+ binary.LittleEndian.PutUint32(b, x11PeerRoot)
164+ binary.LittleEndian.PutUint16(b[8:], 300)
165+ binary.LittleEndian.PutUint16(b[10:], 200)
166+ p.reply(24, b)
167+ case r.major == 18: // ChangeProperty
168+ win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:])
169+ format, n := body[12], int(binary.LittleEndian.Uint32(body[16:]))
170+ p.store(win, prop, binary.LittleEndian.Uint32(body[8:]), uint32(format), body[20:20+n*int(format)/8])
171+ case r.major == 20: // GetProperty
172+ win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:])
173+ data, ok := p.props[win][prop]
174+ b := make([]byte, 24+(len(data)+3)&^3)
175+ var format uint8
176+ if ok {
177+ meta := p.propMeta[win][prop]
178+ format = uint8(meta[1])
179+ binary.LittleEndian.PutUint32(b[0:], meta[0])
180+ binary.LittleEndian.PutUint32(b[8:], uint32(len(data)/(int(format)/8)))
181+ copy(b[24:], data)
182+ }
183+ p.reply(format, b)
184+ case r.major >= 128:
185+ switch r.major {
186+ case 133:
187+ p.reply(0, x11le32(0x100000))
188+ case 136:
189+ if r.minor == 1 {
190+ p.reply(0, x11le32(0, 1))
191+ }
192+ case 142:
193+ if r.minor == 0 {
194+ p.reply(0, x11le32(0, 4))
195+ }
196+ }
197+ }
198+}
199+
200+func (p *x11Peer) store(win, prop, typ, format uint32, data []byte) {
201+ if p.props[win] == nil {
202+ p.props[win], p.propMeta[win] = map[uint32][]byte{}, map[uint32][2]uint32{}
203+ }
204+ p.props[win][prop] = append([]byte(nil), data...)
205+ p.propMeta[win][prop] = [2]uint32{typ, format}
206+}
207+
208+// atom interns name the way the XWM would have.
209+func (p *x11Peer) atom(name string) uint32 {
210+ p.mu.Lock()
211+ defer p.mu.Unlock()
212+ a, ok := p.atoms[name]
213+ if !ok {
214+ p.nextAtom++
215+ a = p.nextAtom
216+ p.atoms[name] = a
217+ }
218+ return a
219+}
220+
221+// setProp scripts a window property (typ is an atom number, format 8 or 32).
222+func (p *x11Peer) setProp(win, prop, typ, format uint32, data []byte) {
223+ p.mu.Lock()
224+ defer p.mu.Unlock()
225+ p.store(win, prop, typ, format, data)
226+}
227+
228+func (p *x11Peer) event(code uint8, fill func(b *[32]byte)) {
229+ p.mu.Lock()
230+ defer p.mu.Unlock()
231+ var b [32]byte
232+ b[0] = code
233+ binary.LittleEndian.PutUint16(b[2:], p.seq)
234+ fill(&b)
235+ _, _ = p.f.Write(b[:])
236+}
237+
238+func (p *x11Peer) createNotify(win uint32) {
239+ p.event(16, func(b *[32]byte) {
240+ binary.LittleEndian.PutUint32(b[4:], x11PeerRoot)
241+ binary.LittleEndian.PutUint32(b[8:], win)
242+ binary.LittleEndian.PutUint16(b[16:], 100)
243+ binary.LittleEndian.PutUint16(b[18:], 50)
244+ })
245+}
246+
247+// createOverride announces an override-redirect window of 100x50 at x,y.
248+func (p *x11Peer) createOverride(win uint32, x, y int16) {
249+ p.event(16, func(b *[32]byte) {
250+ binary.LittleEndian.PutUint32(b[4:], x11PeerRoot)
251+ binary.LittleEndian.PutUint32(b[8:], win)
252+ binary.LittleEndian.PutUint16(b[12:], uint16(x))
253+ binary.LittleEndian.PutUint16(b[14:], uint16(y))
254+ binary.LittleEndian.PutUint16(b[16:], 100)
255+ binary.LittleEndian.PutUint16(b[18:], 50)
256+ b[22] = 1
257+ })
258+}
259+
260+func (p *x11Peer) mapNotify(win uint32) {
261+ p.event(19, func(b *[32]byte) {
262+ binary.LittleEndian.PutUint32(b[4:], x11PeerRoot)
263+ binary.LittleEndian.PutUint32(b[8:], win)
264+ b[12] = 1
265+ })
266+}
267+
268+// configureNotify moves and resizes a window (root SubstructureNotify form).
269+func (p *x11Peer) configureNotify(win uint32, x, y int16, w, h uint16) {
270+ p.event(22, func(b *[32]byte) {
271+ binary.LittleEndian.PutUint32(b[4:], x11PeerRoot)
272+ binary.LittleEndian.PutUint32(b[8:], win)
273+ binary.LittleEndian.PutUint16(b[16:], uint16(x))
274+ binary.LittleEndian.PutUint16(b[18:], uint16(y))
275+ binary.LittleEndian.PutUint16(b[20:], w)
276+ binary.LittleEndian.PutUint16(b[22:], h)
277+ b[24] = 1
278+ })
279+}
280+
281+func (p *x11Peer) windowEvent(code uint8, win uint32) {
282+ p.event(code, func(b *[32]byte) {
283+ binary.LittleEndian.PutUint32(b[4:], x11PeerRoot)
284+ binary.LittleEndian.PutUint32(b[8:], win)
285+ })
286+}
287+
288+func (p *x11Peer) mapRequest(win uint32) { p.windowEvent(20, win) }
289+func (p *x11Peer) unmapNotify(win uint32) { p.windowEvent(18, win) }
290+func (p *x11Peer) destroyNotify(win uint32) { p.windowEvent(17, win) }
291+
292+func (p *x11Peer) propertyNotify(win, atom uint32) {
293+ p.event(28, func(b *[32]byte) {
294+ binary.LittleEndian.PutUint32(b[4:], win)
295+ binary.LittleEndian.PutUint32(b[8:], atom)
296+ })
297+}
298+
299+func (p *x11Peer) clientMessage(win uint32, typ string, data ...uint32) {
300+ t := p.atom(typ)
301+ p.event(33, func(b *[32]byte) {
302+ b[1] = 32
303+ binary.LittleEndian.PutUint32(b[4:], win)
304+ binary.LittleEndian.PutUint32(b[8:], t)
305+ for i, d := range data {
306+ binary.LittleEndian.PutUint32(b[12+4*i:], d)
307+ }
308+ })
309+}
310+
311+func (p *x11Peer) requests() []x11Req {
312+ p.mu.Lock()
313+ defer p.mu.Unlock()
314+ return append([]x11Req(nil), p.reqs...)
315+}
316+
317+// waitReq waits for a request after the first skip ones that satisfies match.
318+func (p *x11Peer) waitReq(skip int, match func(x11Req) bool) (x11Req, int) {
319+ p.t.Helper()
320+ deadline := time.Now().Add(5 * time.Second)
321+ for time.Now().Before(deadline) {
322+ reqs := p.requests()
323+ for i := skip; i < len(reqs); i++ {
324+ if match(reqs[i]) {
325+ return reqs[i], i + 1
326+ }
327+ }
328+ time.Sleep(time.Millisecond)
329+ }
330+ p.t.Fatal("x11 peer: expected request never arrived")
331+ return x11Req{}, 0
332+}
A internal/adapters/wayland/x11window.go +439 -0
@@ -0,0 +1,439 @@
1+package wayland
2+
3+import (
4+ "github.com/bnema/go-wayland-bindings/server/wayland"
5+ "github.com/bnema/go-wayland-bindings/server/xwaylandshell"
6+ "github.com/bnema/neferwl/internal/adapters/wayland/xwm"
7+ "github.com/bnema/neferwl/internal/ports"
8+ "github.com/bnema/purego-libwayland/server"
9+)
10+
11+// x11Window is the X11 role of a window: the XWM slot, the window's X id and
12+// the wl_surface Xwayland paired with it. Display goroutine only.
13+type x11Window struct {
14+ server *Server
15+ slot xwm.Slot
16+ xid uint32
17+ surface *surface
18+ info xwm.WindowInfo
19+ // ready is set once the XWM read the window's properties (Ready).
20+ ready bool
21+ // override marks an override-redirect window: tracked and drawn, never a
22+ // toplevel nor keyboard focus. rect is its layout rect, noInput keeps the
23+ // pointer off it.
24+ override bool
25+ noInput bool
26+ rect ports.Rect
27+}
28+
29+// xwaylandRestricted are the globals only the Xwayland client may see.
30+func xwaylandRestricted(iface string) bool {
31+ return iface == "xwayland_shell_v1" || iface == "zwp_xwayland_keyboard_grab_manager_v1"
32+}
33+
34+// globalVisible is the display's global filter: the Xwayland-only globals
35+// are visible to, and bindable by, the Xwayland client alone.
36+func (s *Server) globalVisible(c server.Client, g *server.Global) bool {
37+ if g == s.xdgPrivate {
38+ return s.xwayland.client != (server.Client{}) && c == s.xwayland.client
39+ }
40+ if g == s.xdgPublic {
41+ return c != s.xwayland.client
42+ }
43+ if xwaylandRestricted(g.Interface()) {
44+ return s.xwayland.client != (server.Client{}) && c == s.xwayland.client
45+ }
46+ return true
47+}
48+
49+func registerXwaylandShell(d *server.Display, s *Server) error {
50+ return xwaylandshell.NewXwaylandShellV1Global(d, 1, func(c server.Client, v, id uint32) {
51+ _, _ = xwaylandshell.NewXwaylandShellV1(c, int32(v), id, xwaylandShell{s})
52+ })
53+}
54+
55+type xwaylandShell struct{ s *Server }
56+
57+func (xwaylandShell) Destroy(*xwaylandshell.XwaylandShellV1) {}
58+
59+func (h xwaylandShell) GetXwaylandSurface(r *xwaylandshell.XwaylandShellV1, id uint32, w *wayland.Surface) {
60+ surf := h.s.surfaceOf(w)
61+ if surf == nil {
62+ return
63+ }
64+ if surf.kind != roleNone {
65+ r.PostError(uint32(xwaylandshell.XwaylandShellV1ErrorRole), "surface already has a role")
66+ return
67+ }
68+ if _, err := xwaylandshell.NewXwaylandSurfaceV1(r.Client(), r.Version(), id, xwaylandSurface{h.s, surf}); err != nil {
69+ return
70+ }
71+ surf.kind = roleX11
72+}
73+
74+type xwaylandSurface struct {
75+ s *Server
76+ surf *surface
77+}
78+
79+func (x xwaylandSurface) SetSerial(r *xwaylandshell.XwaylandSurfaceV1, lo, hi uint32) {
80+ serial := uint64(hi)<<32 | uint64(lo)
81+ switch {
82+ case x.surf.x11Serial != 0:
83+ r.PostError(uint32(xwaylandshell.XwaylandSurfaceV1ErrorAlreadyAssociated), "serial already set")
84+ case serial == 0:
85+ r.PostError(uint32(xwaylandshell.XwaylandSurfaceV1ErrorInvalidSerial), "serial is zero")
86+ default:
87+ x.surf.x11Serial = serial
88+ if x.s.xwm != nil {
89+ x.s.xwm.SetSerial(x.surf.identity, serial)
90+ }
91+ }
92+}
93+
94+func (xwaylandSurface) Destroy(*xwaylandshell.XwaylandSurfaceV1) {}
95+
96+// installXWM makes x the server's window manager. Display goroutine only.
97+func (s *Server) installXWM(x *xwm.XWM) {
98+ s.teardownXWM()
99+ s.xwm = x
100+ x.UseSpace(&s.xspace)
101+ if err := x.Attach(s.display, hostImpl{s}); err != nil {
102+ s.log.Warn().Err(err).Msg("X11 window management disabled")
103+ s.xwm = nil
104+ x.CloseConn()
105+ return
106+ }
107+ // Serials sent before the XWM was attached pair now.
108+ for _, surf := range s.surfaces {
109+ if surf.kind == roleX11 && surf.x11Serial != 0 && surf.x11win == nil {
110+ x.SetSerial(surf.identity, surf.x11Serial)
111+ }
112+ }
113+ x.LayoutChanged(true)
114+}
115+
116+// teardownXWM closes the window manager, dropping its windows. Idempotent.
117+func (s *Server) teardownXWM() {
118+ if x := s.xwm; x != nil {
119+ s.xwm = nil
120+ x.Close()
121+ }
122+}
123+
124+// x11Configure forwards the latest core configure of an X11 window.
125+func (s *Server) x11Configure(w *window) {
126+ if s.xwm != nil {
127+ s.xwm.Configure(w.x11.slot, w.last)
128+ }
129+}
130+
131+// x11Close asks an X11 window to close.
132+func (s *Server) x11Close(w *window) {
133+ if s.xwm != nil {
134+ s.xwm.CloseWindow(w.x11.slot)
135+ }
136+}
137+
138+// x11Place forwards core's position of an X11 window's client rect.
139+func (s *Server) x11Place(c ports.PlaceWindow) {
140+ if w := s.windows[c.ID]; w != nil && w.x11 != nil && s.xwm != nil {
141+ s.xwm.Place(w.x11.slot, c.X, c.Y)
142+ }
143+}
144+
145+// x11Focus tells the XWM which X window holds the keyboard focus, none when a
146+// native window does.
147+func (s *Server) x11Focus() {
148+ if s.xwm == nil {
149+ return
150+ }
151+ var slot xwm.Slot
152+ if w := s.windows[s.seat.focused]; w != nil && w.x11 != nil {
153+ slot = w.x11.slot
154+ }
155+ s.xwm.Focus(slot)
156+}
157+
158+// hostImpl is the server as the XWM sees it.
159+type hostImpl struct{ s *Server }
160+
161+func (h hostImpl) window(slot xwm.Slot) *window { return h.s.x11Slots[slot] }
162+
163+func (h hostImpl) Created(slot xwm.Slot, xid uint32) ports.WindowID {
164+ s := h.s
165+ w := &window{id: s.nextWindow}
166+ w.x11 = &x11Window{server: s, slot: slot, xid: xid}
167+ s.nextWindow++
168+ s.windows[w.id] = w
169+ s.x11Slots[slot] = w
170+ return w.id
171+}
172+
173+func (h hostImpl) Ready(slot xwm.Slot, info xwm.WindowInfo) {
174+ w := h.window(slot)
175+ if w == nil {
176+ return
177+ }
178+ w.x11.info, w.x11.ready = info, true
179+ w.title, w.appID, w.modal = info.Title, info.AppID, info.Modal
180+ w.parent = h.window(info.Parent)
181+ // A surface associated and committed earlier maps now.
182+ w.x11.tryMap()
183+}
184+
185+func (h hostImpl) Associate(slot xwm.Slot, surfaceID uint64) {
186+ w := h.window(slot)
187+ surf := h.s.surfaceByIdentity[surfaceID]
188+ if w == nil || surf == nil || surf.kind != roleX11 {
189+ return
190+ }
191+ x := w.x11
192+ if x.surface != nil && x.surface != surf {
193+ // A new surface replaces the shown one: core sees a remap.
194+ w.unmap()
195+ }
196+ x.drop()
197+ x.surface, surf.x11win = surf, w
198+ surf.role = func(buffer bool) {
199+ switch {
200+ case surf.destroyed:
201+ w.unmap()
202+ if x.surface == surf {
203+ x.surface = nil
204+ }
205+ surf.x11win = nil
206+ case buffer:
207+ x.tryMap()
208+ case w.mapped:
209+ w.unmap()
210+ }
211+ }
212+ // The buffer may have been committed before the pairing.
213+ x.tryMap()
214+}
215+
216+func (h hostImpl) Dissociate(slot xwm.Slot, destroyed bool) {
217+ w := h.window(slot)
218+ if w == nil {
219+ return
220+ }
221+ w.unmap()
222+ w.x11.drop()
223+ w.x11.ready = false
224+ if destroyed {
225+ for _, c := range h.s.windows {
226+ if c.parent == w {
227+ c.parent = nil
228+ }
229+ }
230+ delete(h.s.windows, w.id)
231+ delete(h.s.x11Slots, slot)
232+ h.s.foreignToplevelGone(w)
233+ }
234+}
235+
236+func (h hostImpl) Title(slot xwm.Slot, title string) {
237+ if w := h.window(slot); w != nil && w.title != title {
238+ w.title = title
239+ h.s.toplevelChanged(w)
240+ if w.mapped {
241+ h.s.castInventoryChanged()
242+ }
243+ }
244+}
245+
246+func (h hostImpl) AppID(slot xwm.Slot, appID string) {
247+ w := h.window(slot)
248+ if w == nil || w.appID == appID {
249+ return
250+ }
251+ w.appID = appID
252+ if w.mapped {
253+ h.s.emit(ports.WindowAppID{ID: w.id, AppID: appID})
254+ h.s.toplevelChanged(w)
255+ h.s.castInventoryChanged()
256+ }
257+}
258+
259+func (h hostImpl) Parent(slot xwm.Slot, parent xwm.Slot) {
260+ if w := h.window(slot); w != nil {
261+ w.setParent(h.window(parent))
262+ }
263+}
264+
265+func (h hostImpl) Fullscreen(slot xwm.Slot, on bool) {
266+ w := h.window(slot)
267+ if w == nil {
268+ return
269+ }
270+ w.x11.info.Fullscreen = on
271+ if w.mapped {
272+ h.s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: on})
273+ }
274+}
275+
276+// drop unlinks the window from its surface.
277+func (x *x11Window) drop() {
278+ if surf := x.surface; surf != nil {
279+ surf.x11win, surf.role = nil, nil
280+ x.surface = nil
281+ }
282+}
283+
284+// tryMap maps the window once the XWM knows it and its surface shows a buffer.
285+func (x *x11Window) tryMap() {
286+ surf := x.surface
287+ if surf == nil {
288+ return
289+ }
290+ w := surf.x11win
291+ if w == nil || w.mapped || !x.ready || surf.destroyed || surf.current == nil {
292+ return
293+ }
294+ s := x.server
295+ w.mapped = true
296+ w.gen++
297+ w.floating = x.info.Floating
298+ w.floatW, w.floatH = 0, 0
299+ if w.floating {
300+ w.floatW, w.floatH = x.info.Width, x.info.Height
301+ }
302+ var parent ports.WindowID
303+ if w.parent != nil && w.parent.mapped {
304+ parent = w.parent.id
305+ }
306+ s.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, PID: x.info.PID, Floating: w.floating, Width: w.floatW, Height: w.floatH, Parent: parent, Positioned: true, Modal: w.modal, Remap: w.gen > 1})
307+ if x.info.Fullscreen {
308+ s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: true})
309+ }
310+ s.syncInhibitors()
311+ s.toplevelChanged(w)
312+ s.castInventoryChanged()
313+ s.log.Info().Uint64("id", uint64(w.id)).Uint32("xid", x.xid).Str("app_id", w.appID).Bool("floating", w.floating).Msg("x11 window mapped")
314+ // Called from a commit, the content follows right after; called from the
315+ // pairing, the surface already shows a buffer and is published now.
316+ if surf.has {
317+ surf.resetInputEmission()
318+ surf.redraw()
319+ surf.emitInput()
320+ }
321+}
322+
323+// OverrideMapped starts tracking an override-redirect window. It is announced
324+// to core on the paired surface's first buffer commit, so it is never drawn
325+// without content.
326+func (h hostImpl) OverrideMapped(slot xwm.Slot, xid uint32, surfaceID uint64, x, y, w, h2 int, noInput bool) {
327+ s := h.s
328+ surf := s.surfaceByIdentity[surfaceID]
329+ if surf == nil || surf.kind != roleX11 || surf.x11win != nil {
330+ return
331+ }
332+ if old := s.x11Slots[slot]; old != nil {
333+ h.OverrideGone(slot)
334+ }
335+ win := &window{id: s.nextWindow}
336+ win.x11 = &x11Window{server: s, slot: slot, xid: xid, override: true, noInput: noInput, rect: ports.Rect{X: x, Y: y, W: w, H: h2}}
337+ s.nextWindow++
338+ s.windows[win.id] = win
339+ s.x11Slots[slot] = win
340+ ox := win.x11
341+ ox.surface, surf.x11win = surf, win
342+ surf.role = func(buffer bool) {
343+ switch {
344+ case surf.destroyed:
345+ ox.hide()
346+ ox.surface = nil
347+ surf.x11win = nil
348+ case buffer:
349+ ox.show()
350+ case win.mapped:
351+ ox.hide()
352+ }
353+ }
354+ ox.show()
355+}
356+
357+// OverrideMoved records the new layout rect of an override window and, once
358+// shown, tells core. An unchanged rect costs nothing.
359+func (h hostImpl) OverrideMoved(slot xwm.Slot, x, y, w, h2 int) {
360+ win := h.s.x11Slots[slot]
361+ if win == nil || win.x11 == nil || !win.x11.override {
362+ return
363+ }
364+ r := ports.Rect{X: x, Y: y, W: w, H: h2}
365+ if r == win.x11.rect {
366+ return
367+ }
368+ win.x11.rect = r
369+ if win.mapped {
370+ h.s.emit(ports.UnmanagedMoved{ID: win.id, X: x, Y: y, Width: w, Height: h2})
371+ win.x11.surface.sendTreeScale()
372+ }
373+}
374+
375+// OverrideGone ends an override window: unmapped, or destroyed.
376+func (h hostImpl) OverrideGone(slot xwm.Slot) {
377+ s := h.s
378+ win := s.x11Slots[slot]
379+ if win == nil || win.x11 == nil || !win.x11.override {
380+ return
381+ }
382+ win.x11.hide()
383+ win.x11.drop()
384+ delete(s.windows, win.id)
385+ delete(s.x11Slots, slot)
386+}
387+
388+// show announces the window to core once its surface has content.
389+func (x *x11Window) show() {
390+ surf := x.surface
391+ if surf == nil || surf.x11win == nil || surf.destroyed || surf.current == nil {
392+ return
393+ }
394+ w := surf.x11win
395+ if w.mapped {
396+ return
397+ }
398+ s := x.server
399+ w.mapped = true
400+ r := x.rect
401+ s.emit(ports.UnmanagedMapped{ID: w.id, X: r.X, Y: r.Y, Width: r.W, Height: r.H, NoInput: x.noInput})
402+ surf.sendTreeScale()
403+ if surf.has {
404+ surf.resetInputEmission()
405+ surf.redraw()
406+ surf.emitInput()
407+ }
408+}
409+
410+// hide withdraws a shown override window from core and the seat.
411+func (x *x11Window) hide() {
412+ if x.surface == nil || x.surface.x11win == nil {
413+ return
414+ }
415+ w := x.surface.x11win
416+ s := x.server
417+ if s.seat.pointerFocus == w.id {
418+ s.changePointerFocus(0, 0, 0)
419+ }
420+ if !w.mapped {
421+ return
422+ }
423+ w.mapped = false
424+ x.surface.resetInputEmission()
425+ s.emit(ports.UnmanagedUnmapped{ID: w.id})
426+ s.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true})
427+}
428+
429+// isOverride reports whether w is an override-redirect X11 window.
430+func (w *window) isOverride() bool { return w.x11 != nil && w.x11.override }
431+
432+// overrideOutput is the name of the output under the origin of an override
433+// window's rect, "" when none.
434+func (s *Server) overrideOutput(w *window) string {
435+ if o, ok := s.outputPlaces.At(float64(w.x11.rect.X), float64(w.x11.rect.Y)); ok {
436+ return o.Info.Name
437+ }
438+ return ""
439+}
A internal/adapters/wayland/x11window_test.go +353 -0
@@ -0,0 +1,353 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "encoding/binary"
6+ "net"
7+ "os"
8+ "testing"
9+ "time"
10+
11+ "github.com/bnema/go-wayland-bindings/server/pointerconstraints"
12+ "github.com/bnema/go-wayland-bindings/server/wayland"
13+ "github.com/bnema/go-wayland-bindings/server/xdgshell"
14+ "github.com/bnema/go-wayland-bindings/server/xwaylandshell"
15+ "github.com/bnema/neferwl/internal/ports"
16+ "github.com/bnema/wlturbo"
17+ "golang.org/x/sys/unix"
18+)
19+
20+var x11Serial = uint64(0x1234_0000_0042)
21+
22+// x11Rig is a server with an Xwayland client (a real wlturbo client on the
23+// adopted descriptor), a scripted X peer on the WM descriptor and the XWM
24+// between them.
25+type x11Rig struct {
26+ t *testing.T
27+ s *Server
28+ events chan ports.ClientEvent
29+ commands chan ports.ClientCommand
30+ dir string
31+ xc *wlturbo.Display
32+ peer *x11Peer
33+ stop func()
34+ shell uint32
35+ comp uint32
36+}
37+
38+func newX11Rig(t *testing.T) *x11Rig {
39+ t.Helper()
40+ s, events, commands, dir := lifecycleServer(t)
41+ r := &x11Rig{t: t, s: s, events: events, commands: commands, dir: dir}
42+ wl := xwaylandPair(t)
43+ s.AttachXwayland(wl[0])
44+ f := os.NewFile(uintptr(wl[1]), "xwayland client")
45+ wl[1] = -1
46+ conn, err := net.FileConn(f)
47+ f.Close()
48+ if err != nil {
49+ t.Fatal(err)
50+ }
51+ if r.xc, err = wlturbo.ConnectFromConn(conn); err != nil {
52+ t.Fatal(err)
53+ }
54+ t.Cleanup(func() { _ = r.xc.Close(); forgetWireClient(r.xc) })
55+ if err := r.xc.Roundtrip(); err != nil {
56+ t.Fatal(err)
57+ }
58+ wm, peer := startX11Peer(t)
59+ r.peer = peer
60+ r.stop = s.AttachXWM(context.Background(), wm)
61+ t.Cleanup(func() { r.stop() })
62+ r.shell = bindProtocol(t, r.xc, "xwayland_shell_v1")
63+ r.comp = bindProtocol(t, r.xc, "wl_compositor")
64+ return r
65+}
66+
67+// x11Window announces and maps X window win with the properties of an
68+// ordinary client, and waits for the XWM to be ready for it.
69+func (r *x11Rig) x11Window(win uint32) {
70+ r.t.Helper()
71+ p := r.peer
72+ str := uint32(31) // XA_STRING
73+ p.setProp(win, 67, str, 8, []byte("inst\x00Class\x00")) // WM_CLASS
74+ p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("net title"))
75+ p.setProp(win, p.atom("_NET_WM_PID"), 6, 32, x11le32(4242))
76+ p.createNotify(win)
77+ skip := len(p.requests())
78+ p.mapRequest(win)
79+ p.waitReq(skip, func(q x11Req) bool { return q.major == 8 && q.u32(4) == win }) // MapWindow
80+}
81+
82+// pair sends the serial from both sides and commits a buffer on the surface.
83+// It returns the wl_surface id of the Xwayland client.
84+func (r *x11Rig) pair(win uint32, serial uint64) uint32 {
85+ r.t.Helper()
86+ r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32))
87+ surf, xs := r.xc.AllocateID(), r.xc.AllocateID()
88+ requestProtocol(r.t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf)
89+ registerProtocol(r.t, r.xc, surf)
90+ requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf)
91+ registerProtocol(r.t, r.xc, xs)
92+ requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32))
93+ requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(r.t, r.xc), int32(0), int32(0))
94+ requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestCommit)
95+ if err := r.xc.Roundtrip(); err != nil {
96+ r.t.Fatal(err)
97+ }
98+ return surf
99+}
100+
101+func awaitEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T {
102+ t.Helper()
103+ for deadline := time.After(3 * time.Second); ; {
104+ select {
105+ case ev := <-events:
106+ if v, ok := ev.(T); ok {
107+ return v
108+ }
109+ case <-deadline:
110+ var zero T
111+ t.Fatalf("no %T event", zero)
112+ }
113+ }
114+}
115+
116+func TestXwaylandGlobalFilter(t *testing.T) {
117+ s, _, _, dir := lifecycleServer(t)
118+ other := protocolClient(t, s, dir)
119+ for _, iface := range []string{"xwayland_shell_v1", "zwp_xwayland_keyboard_grab_manager_v1"} {
120+ if _, ok := other.Registry().FindGlobal(iface); ok {
121+ t.Errorf("ordinary client sees %s", iface)
122+ }
123+ }
124+ if _, ok := other.Registry().FindGlobal("xdg_wm_base"); !ok {
125+ t.Error("ordinary globals filtered out")
126+ }
127+ r := newX11Rig(t)
128+ if _, ok := r.xc.Registry().FindGlobal("xwayland_shell_v1"); !ok {
129+ t.Error("Xwayland client misses xwayland_shell_v1")
130+ }
131+ // The ordinary client of the same server still does not see it.
132+ late := protocolClient(t, r.s, r.dir)
133+ if _, ok := late.Registry().FindGlobal("xwayland_shell_v1"); ok {
134+ t.Error("ordinary client sees xwayland_shell_v1 while Xwayland is attached")
135+ }
136+}
137+
138+func TestXwaylandShellRoleErrors(t *testing.T) {
139+ for _, scenario := range []string{"xdg role first", "serial twice", "zero serial"} {
140+ t.Run(scenario, func(t *testing.T) {
141+ r := newX11Rig(t)
142+ c := r.xc
143+ wmBase := bindProtocol(t, c, "xdg_wm_base")
144+ surf := c.AllocateID()
145+ requestProtocol(t, c, r.comp, wayland.CompositorRequestCreateSurface, surf)
146+ registerProtocol(t, c, surf)
147+ xs := c.AllocateID()
148+ switch scenario {
149+ case "xdg role first":
150+ xdg := xs
151+ requestProtocol(t, c, wmBase, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
152+ requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, c.AllocateID(), surf)
153+ expectProtocolError(t, c, r.shell, uint32(xwaylandshell.XwaylandShellV1ErrorRole))
154+ case "serial twice":
155+ requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf)
156+ registerProtocol(t, c, xs)
157+ requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(1), uint32(0))
158+ requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(2), uint32(0))
159+ expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorAlreadyAssociated))
160+ case "zero serial":
161+ requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf)
162+ registerProtocol(t, c, xs)
163+ requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(0), uint32(0))
164+ expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorInvalidSerial))
165+ }
166+ })
167+ }
168+}
169+
170+// A window announced by the X server and a surface carrying the same serial
171+// map as one neferwl window with the X11 info; core commands reach the X side.
172+func TestX11WindowLifecycle(t *testing.T) {
173+ r := newX11Rig(t)
174+ win := uint32(x11Win)
175+ r.x11Window(win)
176+ surf := r.pair(win, x11Serial)
177+
178+ m := awaitEvent[ports.WindowMapped](t, r.events)
179+ if m.AppID != "Class" || m.PID != 4242 || !m.Positioned || m.Floating || m.Slot != "" {
180+ t.Fatalf("mapped %+v", m)
181+ }
182+
183+ // Title and app id changes.
184+ p := r.peer
185+ p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("renamed"))
186+ p.propertyNotify(win, p.atom("_NET_WM_NAME"))
187+ p.setProp(win, 67, 31, 8, []byte("inst\x00Other\x00"))
188+ p.propertyNotify(win, 67)
189+ if a := awaitEvent[ports.WindowAppID](t, r.events); a.ID != m.ID || a.AppID != "Other" {
190+ t.Fatalf("app id %+v", a)
191+ }
192+ var title string
193+ waitTrue(t, "title", func() bool {
194+ r.s.display.Do(func() {
195+ if w := r.s.windows[m.ID]; w != nil {
196+ title = w.title
197+ }
198+ })
199+ return title == "renamed"
200+ })
201+
202+ // Fullscreen request from the client.
203+ p.clientMessage(win, "_NET_WM_STATE", 1, p.atom("_NET_WM_STATE_FULLSCREEN"))
204+ if f := awaitEvent[ports.WindowFullscreenRequest](t, r.events); f.ID != m.ID || !f.Fullscreen || f.External {
205+ t.Fatalf("fullscreen %+v", f)
206+ }
207+
208+ // Configure and Place reach the X server as ConfigureWindow.
209+ skip := len(p.requests())
210+ r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 640, Height: 360, Visible: true}
211+ cfg, skip := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win })
212+ if w := binary.LittleEndian.Uint32(cfg.raw[20:]); w != 640 {
213+ t.Fatalf("configured width %d, want 640 (%x)", w, cfg.raw)
214+ }
215+ r.commands <- ports.PlaceWindow{ID: m.ID, X: 10, Y: 20}
216+ cfg, _ = p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win })
217+ if x, y := int32(binary.LittleEndian.Uint32(cfg.raw[12:])), int32(binary.LittleEndian.Uint32(cfg.raw[16:])); x != 10 || y != 20 {
218+ t.Fatalf("placed at %d,%d, want 10,20", x, y)
219+ }
220+
221+ // Focus.
222+ skip = len(p.requests())
223+ r.commands <- ports.FocusWindow{ID: m.ID}
224+ if f, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpSetInputFocus }); f.u32(4) != win {
225+ t.Fatalf("focus on %#x, want %#x", f.u32(4), win)
226+ }
227+
228+ // Close: without WM_DELETE_WINDOW the client is killed.
229+ skip = len(p.requests())
230+ r.commands <- ports.CloseWindow{ID: m.ID}
231+ if k, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpKillClient }); k.u32(4) != win {
232+ t.Fatalf("killed %#x, want %#x", k.u32(4), win)
233+ }
234+
235+ // Destroying the surface unmaps the window.
236+ requestProtocol(t, r.xc, surf, wayland.SurfaceRequestDestroy)
237+ if err := r.xc.Roundtrip(); err != nil {
238+ t.Fatal(err)
239+ }
240+ if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID {
241+ t.Fatalf("unmapped %+v", u)
242+ }
243+}
244+
245+// The serial may arrive on the Wayland side before the X side knows it, and
246+// an UnmapNotify unmaps; a remap pairs again.
247+func TestX11WindowSurfaceFirstAndRemap(t *testing.T) {
248+ r := newX11Rig(t)
249+ win := uint32(x11Win)
250+ p := r.peer
251+ surf, xs := r.xc.AllocateID(), r.xc.AllocateID()
252+ requestProtocol(t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf)
253+ registerProtocol(t, r.xc, surf)
254+ requestProtocol(t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf)
255+ registerProtocol(t, r.xc, xs)
256+ requestProtocol(t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(x11Serial), uint32(x11Serial>>32))
257+ requestProtocol(t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(t, r.xc), int32(0), int32(0))
258+ requestProtocol(t, r.xc, surf, wayland.SurfaceRequestCommit)
259+ if err := r.xc.Roundtrip(); err != nil {
260+ t.Fatal(err)
261+ }
262+ r.x11Window(win)
263+ p.clientMessage(win, "WL_SURFACE_SERIAL", uint32(x11Serial), uint32(x11Serial>>32))
264+ m := awaitEvent[ports.WindowMapped](t, r.events)
265+ if m.Remap {
266+ t.Fatalf("first map flagged as remap: %+v", m)
267+ }
268+ p.unmapNotify(win)
269+ if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID {
270+ t.Fatalf("unmapped %+v", u)
271+ }
272+ p.destroyNotify(win)
273+ waitTrue(t, "window forgotten", func() bool {
274+ gone := false
275+ r.s.display.Do(func() { gone = r.s.windows[m.ID] == nil })
276+ return gone
277+ })
278+}
279+
280+// stop closes the XWM: its windows unmap, and the server keeps serving.
281+func TestX11StopUnmapsWindows(t *testing.T) {
282+ r := newX11Rig(t)
283+ win := uint32(x11Win)
284+ r.x11Window(win)
285+ r.pair(win, x11Serial)
286+ m := awaitEvent[ports.WindowMapped](t, r.events)
287+ r.stop()
288+ if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID {
289+ t.Fatalf("unmapped %+v", u)
290+ }
291+ r.s.display.Do(func() {
292+ if r.s.xwm != nil || r.s.windows[m.ID] != nil || len(r.s.x11Slots) != 0 {
293+ t.Error("XWM state left behind")
294+ }
295+ })
296+ r.stop() // idempotent
297+}
298+
299+// Boot gives up as soon as its context ends, even against a silent server,
300+// and leaves the server serving.
301+func TestAttachXWMBootAbortsWithContext(t *testing.T) {
302+ s, _, _, dir := lifecycleServer(t)
303+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
304+ if err != nil {
305+ t.Fatal(err)
306+ }
307+ defer unix.Close(fds[1]) // the silent server end
308+ ctx, cancel := context.WithCancel(context.Background())
309+ done := make(chan func(), 1)
310+ go func() { done <- s.AttachXWM(ctx, fds[0]) }()
311+ select {
312+ case <-done:
313+ t.Fatal("AttachXWM returned before the context ended")
314+ case <-time.After(150 * time.Millisecond):
315+ }
316+ cancel()
317+ select {
318+ case stop := <-done:
319+ stop()
320+ case <-time.After(2 * time.Second):
321+ t.Fatal("AttachXWM did not abort with its context")
322+ }
323+ assertFDClosed(t, fds[0])
324+ protocolClient(t, s, dir) // still serving
325+}
326+
327+// Xwayland locks the pointer for games: the constraint reaches core for the
328+// X11 window like for an xdg toplevel.
329+func TestX11WindowPointerLock(t *testing.T) {
330+ r := newX11Rig(t)
331+ win := uint32(x11Win)
332+ r.x11Window(win)
333+ surf := r.pair(win, x11Serial)
334+ m := awaitEvent[ports.WindowMapped](t, r.events)
335+
336+ seat := bindProtocol(t, r.xc, "wl_seat")
337+ registerProtocol(t, r.xc, seat)
338+ pointer := r.xc.AllocateID()
339+ requestProtocol(t, r.xc, seat, wayland.SeatRequestGetPointer, pointer)
340+ registerProtocol(t, r.xc, pointer)
341+ constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1")
342+ lock := r.xc.AllocateID()
343+ requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent))
344+ registerProtocol(t, r.xc, lock)
345+ if err := r.xc.Roundtrip(); err != nil {
346+ t.Fatal(err)
347+ }
348+ r.commands <- ports.PointerFocus{ID: m.ID}
349+ r.commands <- ports.FocusWindow{ID: m.ID}
350+ if got := constrained(t, r.events); got.ID != m.ID || got.Mode != ports.ConstraintLock {
351+ t.Fatalf("constrained %+v", got)
352+ }
353+}
M internal/adapters/wayland/xdg.go +32 -21
@@ -128,10 +128,12 @@ func (x *xdgSurface) Destroy(r *xdgshell.Surface) {
128128
129129 // window is an xdg_toplevel, or an xdg_popup when popup is set.
130130 type window struct {
131- id ports.WindowID
132- toplevel *xdgshell.Toplevel
133- popup *popup
134- xdg *xdgSurface
131+ id ports.WindowID
132+ toplevel *xdgshell.Toplevel
133+ popup *popup
134+ xdg *xdgSurface
135+ // x11 is set for the window of an X11 client (x11window.go).
136+ x11 *x11Window
135137 appID, title string
136138 mapped bool
137139 // gen counts the mappings: an unmap and a remap is a new toplevel to
@@ -151,8 +153,8 @@ type window struct {
151153 hasLast bool
152154 }
153155
154-// sendConfigure sends w.last as xdg_toplevel + xdg_surface configure.
155-func (w *window) sendConfigure() {
156+// sendXDGConfigure sends w.last as xdg_toplevel + xdg_surface configure.
157+func (w *window) sendXDGConfigure() {
156158 c := w.last
157159 w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), toplevelStates(c, w.toplevel.Version()))
158160 s := w.xdg.server
@@ -192,24 +194,29 @@ func toplevelStates(c ports.ConfigureWindow, version int32) []byte {
192194 }
193195
194196 func (w *window) unmap() {
195- if w.xdg.server.seat.pointerFocus == w.id {
196- w.xdg.server.changePointerFocus(0, 0, 0)
197+ s := w.srv()
198+ if s.seat.pointerFocus == w.id {
199+ s.changePointerFocus(0, 0, 0)
197200 }
198- if w.xdg.server.seat.focused == w.id {
199- w.xdg.server.changeFocus(0)
201+ if s.seat.focused == w.id {
202+ s.changeFocus(0)
200203 }
201204 if !w.mapped {
202205 return
203206 }
204207 w.mapped = false
205- w.xdg.server.toplevelClosed(w.id)
206- w.xdg.surface.resetInputEmission()
207- w.xdg.configured = false
208- w.xdg.acked = false
209- w.xdg.serials = nil
210- w.xdg.server.emit(ports.WindowUnmapped{ID: w.id})
211- w.xdg.server.syncInhibitors()
212- w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true})
208+ s.toplevelClosed(w.id)
209+ if surf := w.surf(); surf != nil {
210+ surf.resetInputEmission()
211+ }
212+ if x := w.xdg; x != nil {
213+ x.configured = false
214+ x.acked = false
215+ x.serials = nil
216+ }
217+ s.emit(ports.WindowUnmapped{ID: w.id})
218+ s.syncInhibitors()
219+ s.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true})
213220 }
214221 func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
215222 if x.window != nil {
@@ -337,7 +344,7 @@ func (w *window) setParent(p *window) {
337344 if p != nil && p.mapped {
338345 id = p.id
339346 }
340- w.xdg.server.emit(ports.WindowParent{ID: w.id, Parent: id})
347+ w.srv().emit(ports.WindowParent{ID: w.id, Parent: id})
341348 }
342349 func (t top) SetTitle(_ *xdgshell.Toplevel, title string) {
343350 if title == t.w.title {
@@ -417,7 +424,7 @@ func (w *window) afterCommit() {
417424 }
418425 if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) {
419426 w.floatW, w.floatH = fw, fh
420- w.xdg.server.emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh})
427+ w.srv().emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh})
421428 }
422429 }
423430
@@ -431,6 +438,10 @@ func (w *window) floats() bool {
431438
432439 // size is the window geometry size, else the surface size, in logical pixels.
433440 func (w *window) size() (int, int) {
441+ if w.xdg == nil {
442+ c := w.surf().content
443+ return c.LogicalW, c.LogicalH
444+ }
434445 if g := w.xdg.geometry; g.W > 0 && g.H > 0 {
435446 return g.W, g.H
436447 }
@@ -441,7 +452,7 @@ func (w *window) size() (int, int) {
441452 // surfacePoint turns window coordinates (core's, from the geometry origin)
442453 // into surface coordinates.
443454 func (w *window) surfacePoint(x, y float64) (float64, float64) {
444- g := w.xdg.geometry
455+ g := w.geometry()
445456 return x + float64(g.X), y + float64(g.Y)
446457 }
447458
A internal/adapters/wayland/xdgoutputx_test.go +143 -0
@@ -0,0 +1,143 @@
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+}
135+
136+// The X origin is derived by one Space, kept without a window manager.
137+func TestXSpaceFollowsLayoutWithoutXWM(t *testing.T) {
138+ r := newX11Rig(t)
139+ applyOutputs(t, r.s, r.commands, twoOutputs(-1920))
140+ if ox, oy := r.s.xspace.Origin(); ox != -1920 || oy != 0 {
141+ t.Fatalf("origin %d,%d, want -1920,0", ox, oy)
142+ }
143+}
A internal/adapters/wayland/xwayland.go +75 -0
@@ -0,0 +1,75 @@
1+package wayland
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/neferwl/internal/adapters/logging"
7+ "github.com/bnema/neferwl/internal/adapters/wayland/xwm"
8+ "github.com/bnema/purego-libwayland/server"
9+ "golang.org/x/sys/unix"
10+)
11+
12+// xwaylandClient belongs to the display goroutine.
13+type xwaylandClient struct {
14+ client server.Client
15+}
16+
17+// AttachXwayland transfers ownership of the Wayland descriptor to the display.
18+// It may be called from any goroutine.
19+func (s *Server) AttachXwayland(wlFD int) {
20+ ran := s.display.Do(func() {
21+ c, err := s.display.CreateClient(wlFD)
22+ if err != nil {
23+ s.log.Warn().Err(err).Msg("Xwayland connection not adopted")
24+ _ = unix.Close(wlFD) // CreateClient only adopts on success.
25+ return
26+ }
27+ s.forgetXwayland()
28+ s.xwayland = xwaylandClient{client: c}
29+ c.OnDestroy(func() {
30+ if s.xwayland.client == c {
31+ s.teardownXWM()
32+ s.forgetXwayland()
33+ }
34+ })
35+ s.log.Info().Msg("Xwayland attached")
36+ })
37+ if !ran {
38+ _ = unix.Close(wlFD)
39+ }
40+}
41+
42+// forgetXwayland runs on the display owner, or after Run has joined it.
43+func (s *Server) forgetXwayland() {
44+ if s.xwayland.client != (server.Client{}) {
45+ s.xwayland = xwaylandClient{}
46+ }
47+}
48+
49+// AttachXWM bootstraps the X11 window manager on the window manager
50+// descriptor, which it owns. It runs on the starter goroutine once Xwayland is
51+// ready and blocks until the XWM is installed, ctx ends or Boot fails. stop
52+// closes the XWM on the display goroutine and waits for it.
53+func (s *Server) AttachXWM(ctx context.Context, wmFD int) (stop func()) {
54+ log := logging.For(ctx, "xwm")
55+ x, err := xwm.Boot(ctx, wmFD, log)
56+ if err != nil {
57+ if ctx.Err() != nil {
58+ log.Info().Err(err).Msg("X11 window management not started: Xwayland is going away")
59+ } else {
60+ log.Warn().Err(err).Msg("X11 window management disabled")
61+ }
62+ return func() {}
63+ }
64+ if !s.display.Do(func() { s.installXWM(x) }) {
65+ x.CloseConn()
66+ return func() {}
67+ }
68+ return func() {
69+ s.display.Do(func() {
70+ if s.xwm == x {
71+ s.teardownXWM()
72+ }
73+ })
74+ }
75+}
A internal/adapters/wayland/xwayland_test.go +84 -0
@@ -0,0 +1,84 @@
1+package wayland
2+
3+import (
4+ "errors"
5+ "testing"
6+
7+ "github.com/bnema/purego-libwayland/server"
8+ "golang.org/x/sys/unix"
9+)
10+
11+func xwaylandPair(t *testing.T) *[2]int {
12+ t.Helper()
13+ p, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
14+ if err != nil {
15+ t.Fatal(err)
16+ }
17+ t.Cleanup(func() {
18+ if p[1] >= 0 {
19+ unix.Close(p[1])
20+ }
21+ })
22+ return &p
23+}
24+
25+func assertFDClosed(t *testing.T, fd int) {
26+ t.Helper()
27+ if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); !errors.Is(err, unix.EBADF) {
28+ t.Fatalf("fd %d still open: %v", fd, err)
29+ }
30+}
31+
32+func TestAttachXwaylandLifecycle(t *testing.T) {
33+ h := newAdmissionHarness(t, 1)
34+ wl := xwaylandPair(t)
35+ h.s.AttachXwayland(wl[0])
36+ var first server.Client
37+ h.s.display.Do(func() { first = h.s.xwayland.client })
38+ if first == (server.Client{}) {
39+ t.Fatal("client not recorded")
40+ }
41+ wl2 := xwaylandPair(t)
42+ h.s.AttachXwayland(wl2[0])
43+ unix.Close(wl[1])
44+ wl[1] = -1 // Old client's destroy must not clear the new record.
45+ waitTrue(t, "old client destroyed", func() bool {
46+ _, err := unix.FcntlInt(uintptr(wl[0]), unix.F_GETFD, 0)
47+ return errors.Is(err, unix.EBADF)
48+ })
49+ h.s.display.Do(func() {
50+ if h.s.xwayland.client == (server.Client{}) || h.s.xwayland.client == first {
51+ t.Error("replacement lost")
52+ }
53+ })
54+ unix.Close(wl2[1])
55+ wl2[1] = -1
56+ waitTrue(t, "Xwayland disconnected", func() bool {
57+ clear := false
58+ h.s.display.Do(func() { clear = h.s.xwayland.client == (server.Client{}) })
59+ return clear
60+ })
61+}
62+
63+func TestAttachXwaylandFailureClosesFDs(t *testing.T) {
64+ h := newAdmissionHarness(t, 1)
65+ var p [2]int
66+ if err := unix.Pipe2(p[:], unix.O_CLOEXEC); err != nil {
67+ t.Fatal(err)
68+ }
69+ defer unix.Close(p[1])
70+ h.s.AttachXwayland(p[0])
71+ assertFDClosed(t, p[0])
72+}
73+
74+func TestAttachXwaylandStopped(t *testing.T) {
75+ d, err := server.NewDisplay()
76+ if err != nil {
77+ t.Fatal(err)
78+ }
79+ d.Close()
80+ s := &Server{display: d}
81+ wl := xwaylandPair(t)
82+ s.AttachXwayland(wl[0])
83+ assertFDClosed(t, wl[0])
84+}
A internal/adapters/wayland/xwm/atoms.go +96 -0
@@ -0,0 +1,96 @@
1+package xwm
2+
3+// Atoms interned at boot in one round trip, in this fixed order. The table
4+// includes the clipboard and selection atoms.
5+const (
6+ atomWMProtocols = iota
7+ atomWMDeleteWindow
8+ atomWMTakeFocus
9+ atomWMState
10+ atomWMChangeState
11+ atomWMS0
12+ atomUTF8String
13+ atomNetWMCMS0
14+ atomNetSupported
15+ atomNetSupportingWMCheck
16+ atomNetWMName
17+ atomNetWMPID
18+ atomNetWMWindowType
19+ atomNetWMWindowTypeNormal
20+ atomNetWMWindowTypeDialog
21+ atomNetWMWindowTypeUtility
22+ atomNetWMWindowTypeSplash
23+ atomNetWMWindowTypeToolbar
24+ atomNetWMWindowTypeMenu
25+ atomNetWMWindowTypeDropdownMenu
26+ atomNetWMWindowTypePopupMenu
27+ atomNetWMWindowTypeTooltip
28+ atomNetWMWindowTypeNotification
29+ atomNetWMWindowTypeCombo
30+ atomNetWMWindowTypeDND
31+ atomNetWMState
32+ atomNetWMStateModal
33+ atomNetWMStateFullscreen
34+ atomNetWMStateFocused
35+ atomNetWMStateMaximizedVert
36+ atomNetWMStateMaximizedHorz
37+ atomNetActiveWindow
38+ atomNetClientList
39+ atomNetClientListStacking
40+ atomNetWMMoveresize
41+ atomNetWMBypassCompositor
42+ atomWLSurfaceID
43+ atomWLSurfaceSerial
44+ atomNetWMIcon
45+ atomMotifWMHints
46+ atomClipboard
47+ atomPrimary
48+ atomTargets
49+ atomCount
50+)
51+
52+var atomNames = [atomCount]string{
53+ atomWMProtocols: "WM_PROTOCOLS",
54+ atomWMDeleteWindow: "WM_DELETE_WINDOW",
55+ atomWMTakeFocus: "WM_TAKE_FOCUS",
56+ atomWMState: "WM_STATE",
57+ atomWMChangeState: "WM_CHANGE_STATE",
58+ atomWMS0: "WM_S0",
59+ atomUTF8String: "UTF8_STRING",
60+ atomNetWMCMS0: "_NET_WM_CM_S0",
61+ atomNetSupported: "_NET_SUPPORTED",
62+ atomNetSupportingWMCheck: "_NET_SUPPORTING_WM_CHECK",
63+ atomNetWMName: "_NET_WM_NAME",
64+ atomNetWMPID: "_NET_WM_PID",
65+ atomNetWMWindowType: "_NET_WM_WINDOW_TYPE",
66+ atomNetWMWindowTypeNormal: "_NET_WM_WINDOW_TYPE_NORMAL",
67+ atomNetWMWindowTypeDialog: "_NET_WM_WINDOW_TYPE_DIALOG",
68+ atomNetWMWindowTypeUtility: "_NET_WM_WINDOW_TYPE_UTILITY",
69+ atomNetWMWindowTypeSplash: "_NET_WM_WINDOW_TYPE_SPLASH",
70+ atomNetWMWindowTypeToolbar: "_NET_WM_WINDOW_TYPE_TOOLBAR",
71+ atomNetWMWindowTypeMenu: "_NET_WM_WINDOW_TYPE_MENU",
72+ atomNetWMWindowTypeDropdownMenu: "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU",
73+ atomNetWMWindowTypePopupMenu: "_NET_WM_WINDOW_TYPE_POPUP_MENU",
74+ atomNetWMWindowTypeTooltip: "_NET_WM_WINDOW_TYPE_TOOLTIP",
75+ atomNetWMWindowTypeNotification: "_NET_WM_WINDOW_TYPE_NOTIFICATION",
76+ atomNetWMWindowTypeCombo: "_NET_WM_WINDOW_TYPE_COMBO",
77+ atomNetWMWindowTypeDND: "_NET_WM_WINDOW_TYPE_DND",
78+ atomNetWMState: "_NET_WM_STATE",
79+ atomNetWMStateModal: "_NET_WM_STATE_MODAL",
80+ atomNetWMStateFullscreen: "_NET_WM_STATE_FULLSCREEN",
81+ atomNetWMStateFocused: "_NET_WM_STATE_FOCUSED",
82+ atomNetWMStateMaximizedVert: "_NET_WM_STATE_MAXIMIZED_VERT",
83+ atomNetWMStateMaximizedHorz: "_NET_WM_STATE_MAXIMIZED_HORZ",
84+ atomNetActiveWindow: "_NET_ACTIVE_WINDOW",
85+ atomNetClientList: "_NET_CLIENT_LIST",
86+ atomNetClientListStacking: "_NET_CLIENT_LIST_STACKING",
87+ atomNetWMMoveresize: "_NET_WM_MOVERESIZE",
88+ atomNetWMBypassCompositor: "_NET_WM_BYPASS_COMPOSITOR",
89+ atomWLSurfaceID: "WL_SURFACE_ID",
90+ atomWLSurfaceSerial: "WL_SURFACE_SERIAL",
91+ atomNetWMIcon: "_NET_WM_ICON",
92+ atomMotifWMHints: "_MOTIF_WM_HINTS",
93+ atomClipboard: "CLIPBOARD",
94+ atomPrimary: "PRIMARY",
95+ atomTargets: "TARGETS",
96+}
A internal/adapters/wayland/xwm/configure.go +317 -0
@@ -0,0 +1,317 @@
1+package xwm
2+
3+import (
4+ "math"
5+
6+ "bnema.dev/xturbo"
7+ "bnema.dev/xturbo/xproto"
8+ "github.com/bnema/neferwl/internal/ports"
9+)
10+
11+// maxClientSize bounds the size a floating window may ask for.
12+const maxClientSize = 16384
13+
14+// _NET_WM_STATE bits tracked in record.stateSent.
15+const (
16+ stateFullscreen uint8 = 1 << iota
17+ stateFocused
18+)
19+
20+// size is the size to send: the requested one, else the last known.
21+func (r *record) size() (w, h uint32) {
22+ w, h = r.pend.w, r.pend.h
23+ if w == 0 {
24+ w = r.w
25+ }
26+ if h == 0 {
27+ h = r.h
28+ }
29+ return w, h
30+}
31+
32+// differs reports whether the pending configure is not what the X server has.
33+func (r *record) differs() bool {
34+ w, h := r.size()
35+ p, s := &r.pend, &r.sent
36+ return p.x != s.x || p.y != s.y || w != s.w || h != s.h ||
37+ p.fullscreen != s.fullscreen || p.activated != s.activated
38+}
39+
40+// markDirty queues s for the next flush when its pending configure differs.
41+func (x *XWM) markDirty(s Slot, r *record) {
42+ if r.pend.dirty || !r.differs() {
43+ return
44+ }
45+ if len(x.dirty) == cap(x.dirty) {
46+ // Stale entries of freed and reused slots filled the queue: send
47+ // what is queued rather than grow it. r stays valid, nothing here
48+ // grows the table.
49+ x.flushConfigures()
50+ }
51+ r.pend.dirty = true
52+ x.dirty = append(x.dirty, s)
53+}
54+
55+// managedRec returns the record of a live managed window.
56+func (x *XWM) managedRec(s Slot) *record {
57+ if x.closed {
58+ return nil
59+ }
60+ if r := x.tab.rec(s); r != nil && r.kind == kindManaged {
61+ return r
62+ }
63+ return nil
64+}
65+
66+// Configure records the size and state core wants for s; sent at flush. A
67+// zero size keeps the current one.
68+func (x *XWM) Configure(s Slot, c ports.ConfigureWindow) {
69+ r := x.managedRec(s)
70+ if r == nil {
71+ return
72+ }
73+ if c.Width > 0 {
74+ r.pend.w = uint32(min(c.Width, maxClientSize))
75+ }
76+ if c.Height > 0 {
77+ r.pend.h = uint32(min(c.Height, maxClientSize))
78+ }
79+ r.pend.fullscreen, r.pend.activated = c.Fullscreen, c.Activated
80+ x.markDirty(s, r)
81+ x.wake()
82+}
83+
84+// Place records the layout position of the client rectangle of s.
85+func (x *XWM) Place(s Slot, px, py int) {
86+ r := x.managedRec(s)
87+ if r == nil {
88+ return
89+ }
90+ r.lx, r.ly, r.placed = int32(px), int32(py), true
91+ r.pend.x, r.pend.y = x.toX(r)
92+ x.markDirty(s, r)
93+ x.wake()
94+}
95+
96+// toX converts the layout position of r to X coordinates.
97+func (x *XWM) toX(r *record) (px, py int32) {
98+ ix, iy := x.space.ToX(int(r.lx), int(r.ly))
99+ return int32(ix), int32(iy)
100+}
101+
102+// SetLayout installs the output layout that X coordinates derive from. When it
103+// changes, placed managed windows are repositioned and mapped override windows
104+// are re-reported in layout coordinates. Display goroutine only.
105+func (x *XWM) SetLayout(l ports.Layout) {
106+ if x.closed {
107+ return
108+ }
109+ x.LayoutChanged(x.space.Set(l))
110+}
111+
112+// UseSpace makes sp the coordinate mapping, owned by the host, which calls
113+// Set on it and then LayoutChanged. Call before Attach.
114+func (x *XWM) UseSpace(sp *Space) { x.space = sp }
115+
116+// LayoutChanged repositions windows after the shared Space was set; changed
117+// is Set's result. Display goroutine only.
118+func (x *XWM) LayoutChanged(changed bool) {
119+ if x.closed {
120+ return
121+ }
122+ if n := x.space.Dropped(); n > 0 && !x.warnedSpace {
123+ x.warnedSpace = true
124+ x.log.Warn().Int("dropped", n).Int("max", maxRegions).Msg("more outputs than the X11 layout holds, extra outputs ignored")
125+ }
126+ if !changed {
127+ return
128+ }
129+ for i := range x.tab.recs {
130+ r, s := &x.tab.recs[i], Slot(i+1)
131+ switch {
132+ case r.kind == kindManaged && r.placed:
133+ r.pend.x, r.pend.y = x.toX(r)
134+ x.markDirty(s, r)
135+ case r.kind == kindUnmanaged && r.ovPaired:
136+ x.queueOverride(s, r)
137+ }
138+ }
139+ x.wake()
140+}
141+
142+// flushConfigures sends the pending configures, once per display iteration,
143+// then what focus left to do.
144+func (x *XWM) flushConfigures() {
145+ for _, s := range x.dirty {
146+ if r := x.tab.rec(s); r != nil && r.kind == kindManaged && r.pend.dirty {
147+ x.sendPending(s, r)
148+ }
149+ }
150+ x.dirty = x.dirty[:0]
151+ x.flushOverrides()
152+ x.flushFocus()
153+}
154+
155+// sendPending writes the geometry (with the synthetic ConfigureNotify ICCCM
156+// 4.1.5 asks for, since Xwayland may not move the window) and the
157+// _NET_WM_STATE bits that changed.
158+func (x *XWM) sendPending(s Slot, r *record) {
159+ r.pend.dirty = false
160+ p := &r.pend
161+ w, h := r.size()
162+ if w > 0 && h > 0 && (p.x != r.sent.x || p.y != r.sent.y || w != r.sent.w || h != r.sent.h) {
163+ var v xproto.ConfigureWindowValues
164+ v.SetX(p.x).SetY(p.y).SetWidth(w).SetHeight(h).SetBorderWidth(0)
165+ x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v), kindConfigure, x.tab.tag(s), 0)
166+ r.x, r.y, r.w, r.h = p.x, p.y, w, h
167+ x.sendConfigureNotify(r)
168+ r.sent.x, r.sent.y, r.sent.w, r.sent.h = p.x, p.y, w, h
169+ }
170+ var bits uint8
171+ if p.fullscreen {
172+ bits |= stateFullscreen
173+ }
174+ if p.activated {
175+ bits |= stateFocused
176+ }
177+ r.sent.fullscreen, r.sent.activated = p.fullscreen, p.activated
178+ r.fullscreen = p.fullscreen // ours now: the property is not read back
179+ if bits != r.stateSent {
180+ r.stateSent = bits
181+ x.writeState(r)
182+ }
183+}
184+
185+// writeState rewrites _NET_WM_STATE of r from its bits.
186+func (x *XWM) writeState(r *record) {
187+ var list [3]uint32
188+ n := 0
189+ if r.modal {
190+ list[n] = x.atoms[atomNetWMStateModal]
191+ n++
192+ }
193+ if r.stateSent&stateFullscreen != 0 {
194+ list[n] = x.atoms[atomNetWMStateFullscreen]
195+ n++
196+ }
197+ if r.stateSent&stateFocused != 0 {
198+ list[n] = x.atoms[atomNetWMStateFocused]
199+ n++
200+ }
201+ x.setProp32(r.xid, atomNetWMState, xproto.AtomAtom, list[:n])
202+}
203+
204+// sendConfigureNotify tells the client its geometry as the XWM sees it.
205+func (x *XWM) sendConfigureNotify(r *record) {
206+ var raw [32]byte
207+ d := xproto.ConfigureNotifyEventData{
208+ Event: xproto.Window(r.xid), Window: xproto.Window(r.xid),
209+ X: int16(min(max(r.x, math.MinInt16), math.MaxInt16)), Y: int16(min(max(r.y, math.MinInt16), math.MaxInt16)),
210+ Width: uint16(min(r.w, math.MaxUint16)), Height: uint16(min(r.h, math.MaxUint16)),
211+ }
212+ d.Put(&raw)
213+ xproto.SendEvent(x.conn.Buf(), false, xproto.Window(r.xid), xproto.EventMaskStructureNotify, &raw)
214+}
215+
216+// onConfigureRequest answers a client resizing itself. Tiled windows are told
217+// their current geometry; floating ones get the size they ask for; unmanaged
218+// and unknown windows are configured as requested.
219+func (x *XWM) onConfigureRequest(ev *xturbo.Event) {
220+ e := xproto.ConfigureRequestOf(ev)
221+ xid := uint32(e.Window())
222+ mask := e.ValueMask()
223+ s := x.tab.lookup(xid)
224+ r := x.managedRec(s)
225+ if r == nil {
226+ var v xproto.ConfigureWindowValues
227+ if mask&xproto.ConfigWindowX != 0 {
228+ v.SetX(int32(e.X()))
229+ }
230+ if mask&xproto.ConfigWindowY != 0 {
231+ v.SetY(int32(e.Y()))
232+ }
233+ if mask&xproto.ConfigWindowWidth != 0 {
234+ v.SetWidth(uint32(e.Width()))
235+ }
236+ if mask&xproto.ConfigWindowHeight != 0 {
237+ v.SetHeight(uint32(e.Height()))
238+ }
239+ if mask&xproto.ConfigWindowBorderWidth != 0 {
240+ v.SetBorderWidth(uint32(e.BorderWidth()))
241+ }
242+ if mask&xproto.ConfigWindowSibling != 0 {
243+ v.SetSibling(e.Sibling())
244+ }
245+ if mask&xproto.ConfigWindowStackMode != 0 {
246+ v.SetStackMode(e.StackMode())
247+ }
248+ xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v)
249+ return
250+ }
251+ if !r.ready {
252+ x.configureBeforeReady(s, r, e)
253+ return
254+ }
255+ if r.floating && mask&(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight) != 0 {
256+ w, h := r.w, r.h
257+ if mask&xproto.ConfigWindowWidth != 0 {
258+ w = uint32(min(max(int(e.Width()), 1), maxClientSize))
259+ }
260+ if mask&xproto.ConfigWindowHeight != 0 {
261+ h = uint32(min(max(int(e.Height()), 1), maxClientSize))
262+ }
263+ if w > 0 && h > 0 {
264+ var v xproto.ConfigureWindowValues
265+ v.SetWidth(w).SetHeight(h)
266+ x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v), kindConfigure, x.tab.tag(s), 0)
267+ r.w, r.h = w, h
268+ // The size is the client's own: core learns it from the commit
269+ // and Configure only resends it when core wants another.
270+ r.pend.w, r.pend.h = w, h
271+ r.sent.w, r.sent.h = w, h
272+ }
273+ }
274+ x.sendConfigureNotify(r)
275+}
276+
277+// configureBeforeReady honours the geometry a window asks for before it is
278+// ready (XMoveResize before XMapWindow), floating or not: core only sees the
279+// window at Ready and the client must find the geometry it set.
280+func (x *XWM) configureBeforeReady(s Slot, r *record, e xproto.ConfigureRequestEvent) {
281+ mask := e.ValueMask()
282+ var v xproto.ConfigureWindowValues
283+ if mask&xproto.ConfigWindowX != 0 {
284+ r.x = int32(e.X())
285+ v.SetX(r.x)
286+ }
287+ if mask&xproto.ConfigWindowY != 0 {
288+ r.y = int32(e.Y())
289+ v.SetY(r.y)
290+ }
291+ if mask&xproto.ConfigWindowWidth != 0 {
292+ r.w, r.sized = uint32(min(max(int(e.Width()), 1), maxClientSize)), true
293+ v.SetWidth(r.w)
294+ }
295+ if mask&xproto.ConfigWindowHeight != 0 {
296+ r.h, r.sized = uint32(min(max(int(e.Height()), 1), maxClientSize)), true
297+ v.SetHeight(r.h)
298+ }
299+ if mask&(xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth|xproto.ConfigWindowHeight) != 0 {
300+ x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v), kindConfigure, x.tab.tag(s), 0)
301+ }
302+ x.sendConfigureNotify(r)
303+}
304+
305+// onConfigureNotify keeps the known geometry of every top-level window.
306+func (x *XWM) onConfigureNotify(ev *xturbo.Event) {
307+ e := xproto.ConfigureNotifyOf(ev)
308+ if e.Event() != xproto.Window(x.root) {
309+ return
310+ }
311+ if r := x.tab.rec(x.tab.lookup(uint32(e.Window()))); r != nil && r.kind != kindFree {
312+ r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height())
313+ if r.kind == kindUnmanaged && r.ovPaired {
314+ x.queueOverride(x.tab.lookup(r.xid), r)
315+ }
316+ }
317+}
A internal/adapters/wayland/xwm/configure_test.go +565 -0
@@ -0,0 +1,565 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "testing"
6+
7+ "bnema.dev/xturbo/xproto"
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/stretchr/testify/mock"
10+ "github.com/stretchr/testify/require"
11+)
12+
13+// newWin makes a ready managed window known to the XWM and the mocked host.
14+func newWin(t *testing.T, x *XWM, p *xpeer, h *mockhost, xid uint32) Slot {
15+ t.Helper()
16+ h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(xid)).Once()
17+ p.createNotify(xid, false)
18+ readUntil(t, x, func() bool { return x.tab.lookup(xid) != 0 })
19+ s := x.tab.lookup(xid)
20+ r := x.tab.rec(s)
21+ r.ready, r.w, r.h = true, 300, 200
22+ return s
23+}
24+
25+// settle flushes the XWM and waits until the peer has seen everything sent so
26+// far; it returns the number of requests received.
27+func settle(t *testing.T, x *XWM, p *xpeer) int {
28+ t.Helper()
29+ x.flush()
30+ n := len(p.requests())
31+ xproto.GetInputFocus(x.conn.Buf())
32+ require.NoError(t, x.conn.Flush())
33+ _, n = p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus })
34+ return n
35+}
36+
37+// since returns the requests of the given major opcode received after skip.
38+func since(p *xpeer, skip int, major uint8) []xpeerReq {
39+ var out []xpeerReq
40+ for _, r := range p.requests()[skip:] {
41+ if r.major == major {
42+ out = append(out, r)
43+ }
44+ }
45+ return out
46+}
47+
48+// sentEvents returns the 32-byte events of the SendEvent requests to dest.
49+func sentEvents(p *xpeer, skip int, dest uint32) [][]byte {
50+ var out [][]byte
51+ for _, r := range since(p, skip, opSendEvent) {
52+ if r.u32(4) == dest {
53+ out = append(out, r.raw[12:44])
54+ }
55+ }
56+ return out
57+}
58+
59+func propsTo(p *xpeer, skip int, win uint32, atom string) []xpeerReq {
60+ var out []xpeerReq
61+ a := p.atom(atom)
62+ for _, r := range since(p, skip, opChangeProperty) {
63+ if r.u32(4) == win && r.u32(8) == a {
64+ out = append(out, r)
65+ }
66+ }
67+ return out
68+}
69+
70+func (x *XWM) feed(t *testing.T, p *xpeer, send func()) {
71+ t.Helper()
72+ ev := x.stats.Events
73+ send()
74+ readUntil(t, x, func() bool { return x.stats.Events > ev })
75+}
76+
77+func TestConfigureCoalescesIntoOneRequest(t *testing.T) {
78+ x, p, h, _, runs := attached(t)
79+ const win = 0x500001
80+ s := newWin(t, x, p, h, win)
81+ n := settle(t, x, p)
82+
83+ x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480})
84+ x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true})
85+ x.Place(s, 10, 20)
86+ require.Len(t, *runs, 1, "one flush posted")
87+ n2 := settle(t, x, p)
88+
89+ cw := since(p, n, opConfigureWindow)
90+ require.Len(t, cw, 1)
91+ require.Equal(t, uint32(win), cw[0].u32(4))
92+ require.EqualValues(t, 0x1f, binary.LittleEndian.Uint16(cw[0].raw[8:]), "x y width height border")
93+ require.Equal(t, []uint32{10, 20, 800, 600, 0}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20), cw[0].u32(24), cw[0].u32(28)})
94+
95+ evs := sentEvents(p, n, win)
96+ require.Len(t, evs, 1, "one synthetic ConfigureNotify")
97+ require.EqualValues(t, 22, evs[0][0])
98+ require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][4:]))
99+ require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][8:]))
100+ require.EqualValues(t, 10, binary.LittleEndian.Uint16(evs[0][16:]))
101+ require.EqualValues(t, 20, binary.LittleEndian.Uint16(evs[0][18:]))
102+ require.EqualValues(t, 800, binary.LittleEndian.Uint16(evs[0][20:]))
103+ require.EqualValues(t, 600, binary.LittleEndian.Uint16(evs[0][22:]))
104+ require.Equal(t, xproto.EventMaskStructureNotify, xproto.EventMask(since(p, n, opSendEvent)[0].u32(8)))
105+
106+ // Unchanged: nothing is sent.
107+ x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true})
108+ x.Place(s, 10, 20)
109+ settle(t, x, p)
110+ require.Len(t, since(p, n2, opConfigureWindow), 0)
111+ require.Len(t, sentEvents(p, n2, win), 0)
112+}
113+
114+func TestConfigureWritesNetWMState(t *testing.T) {
115+ x, p, h, _, _ := attached(t)
116+ const win = 0x500001
117+ s := newWin(t, x, p, h, win)
118+ x.tab.rec(s).modal = true
119+ n := settle(t, x, p)
120+
121+ x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100})
122+ settle(t, x, p)
123+ require.Empty(t, propsTo(p, n, win, "_NET_WM_STATE"), "no state bit changed")
124+
125+ x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Fullscreen: true, Activated: true})
126+ settle(t, x, p)
127+ st, ok := p.prop(win, "_NET_WM_STATE")
128+ require.True(t, ok)
129+ require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FULLSCREEN"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st))
130+
131+ x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Activated: true})
132+ settle(t, x, p)
133+ st, _ = p.prop(win, "_NET_WM_STATE")
134+ require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st))
135+}
136+
137+func TestConfigureBadWindowIsAttributedAndIgnored(t *testing.T) {
138+ x, p, h, _, _ := attached(t)
139+ const win = 0x500001
140+ s := newWin(t, x, p, h, win)
141+ n := settle(t, x, p)
142+ x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480})
143+ x.flush()
144+ cw := []xpeerReq{}
145+ r, _ := p.waitReq(n, func(r xpeerReq) bool { return r.major == opConfigureWindow })
146+ cw = append(cw, r)
147+
148+ p.mu.Lock()
149+ e := make([]byte, 32)
150+ e[1] = xproto.BadWindow
151+ binary.LittleEndian.PutUint16(e[2:], cw[0].seq)
152+ binary.LittleEndian.PutUint32(e[4:], win)
153+ e[10] = opConfigureWindow
154+ p.write(e)
155+ p.mu.Unlock()
156+ readUntil(t, x, func() bool { return x.stats.Errors == 1 })
157+ require.Zero(t, x.stats.Unattributed)
158+ require.False(t, x.closed)
159+}
160+
161+func sendConfigureRequest(p *xpeer, win uint32, mask uint16, x, y int16, w, h uint16) {
162+ p.event(23, func(b *[32]byte) {
163+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
164+ binary.LittleEndian.PutUint32(b[8:], win)
165+ binary.LittleEndian.PutUint16(b[16:], uint16(x))
166+ binary.LittleEndian.PutUint16(b[18:], uint16(y))
167+ binary.LittleEndian.PutUint16(b[20:], w)
168+ binary.LittleEndian.PutUint16(b[22:], h)
169+ binary.LittleEndian.PutUint16(b[26:], mask)
170+ })
171+}
172+
173+func TestConfigureRequestTiledOnlyGetsNotify(t *testing.T) {
174+ x, p, h, _, _ := attached(t)
175+ const win = 0x500001
176+ newWin(t, x, p, h, win)
177+ n := settle(t, x, p)
178+
179+ x.feed(t, p, func() { sendConfigureRequest(p, win, 0xf, 5, 6, 1000, 900) })
180+ settle(t, x, p)
181+ require.Empty(t, since(p, n, opConfigureWindow), "a tiled window cannot resize itself")
182+ evs := sentEvents(p, n, win)
183+ require.Len(t, evs, 1)
184+ require.EqualValues(t, 22, evs[0][0])
185+ require.EqualValues(t, 300, binary.LittleEndian.Uint16(evs[0][20:]), "current geometry")
186+ require.EqualValues(t, 200, binary.LittleEndian.Uint16(evs[0][22:]))
187+}
188+
189+func TestConfigureRequestFloatingHonoursSize(t *testing.T) {
190+ x, p, h, _, _ := attached(t)
191+ const win = 0x500001
192+ s := newWin(t, x, p, h, win)
193+ x.tab.rec(s).floating = true
194+ n := settle(t, x, p)
195+
196+ x.feed(t, p, func() {
197+ sendConfigureRequest(p, win, uint16(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight), 0, 0, 70000&0xffff, 20000)
198+ })
199+ settle(t, x, p)
200+ cw := since(p, n, opConfigureWindow)
201+ require.Len(t, cw, 1)
202+ require.EqualValues(t, xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, binary.LittleEndian.Uint16(cw[0].raw[8:]))
203+ require.EqualValues(t, 4464, cw[0].u32(12))
204+ require.EqualValues(t, maxClientSize, cw[0].u32(16), "clamped")
205+ require.Len(t, sentEvents(p, n, win), 1)
206+}
207+
208+func TestConfigureRequestUnmanagedHonouredVerbatim(t *testing.T) {
209+ x, p, _, _, _ := attached(t)
210+ const win = 0x500009
211+ p.createNotify(win, true)
212+ readUntil(t, x, func() bool { return x.tab.lookup(win) != 0 })
213+ n := settle(t, x, p)
214+
215+ x.feed(t, p, func() {
216+ sendConfigureRequest(p, win, uint16(xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth), 30, 40, 50, 60)
217+ })
218+ settle(t, x, p)
219+ cw := since(p, n, opConfigureWindow)
220+ require.Len(t, cw, 1)
221+ require.EqualValues(t, 7, binary.LittleEndian.Uint16(cw[0].raw[8:]))
222+ require.Equal(t, []uint32{30, 40, 50}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20)})
223+ require.Empty(t, sentEvents(p, n, win))
224+}
225+
226+func TestConfigureStaleAndDuplicateDirtySlots(t *testing.T) {
227+ x, p, h, _, _ := attached(t)
228+ const a, b = 0x500001, 0x500002
229+ s := newWin(t, x, p, h, a)
230+ for i := 1; i <= 50; i++ {
231+ x.Configure(s, ports.ConfigureWindow{Width: 100 + i, Height: 100})
232+ }
233+ require.Len(t, x.dirty, 1, "queued once per false→true transition")
234+
235+ // The window dies with its entry queued; the slot is reused.
236+ h.EXPECT().Dissociate(s, true).Once()
237+ p.destroyNotify(a)
238+ readUntil(t, x, func() bool { return x.tab.lookup(a) == 0 })
239+ s2 := newWin(t, x, p, h, b)
240+ require.Equal(t, s, s2, "LIFO slot reuse")
241+ n := len(p.requests())
242+ x.Configure(s2, ports.ConfigureWindow{Width: 640, Height: 480})
243+ require.Len(t, x.dirty, 1)
244+ x.dirty = append(x.dirty, s, s) // stale and duplicate entries must be skipped
245+ settle(t, x, p)
246+ cw := since(p, n, opConfigureWindow)
247+ require.Len(t, cw, 1, "sent once despite duplicates")
248+ require.Equal(t, uint32(b), cw[0].u32(4))
249+ require.Empty(t, x.dirty)
250+ require.LessOrEqual(t, cap(x.dirty), len(x.tab.recs))
251+}
252+
253+func TestDirtyQueueNeverGrows(t *testing.T) {
254+ x, p, h, _, _ := attached(t)
255+ c0 := cap(x.dirty)
256+ const base = 0x600000
257+ for i := 0; i < 3*c0; i++ { // each round leaves a stale entry behind
258+ xid := uint32(base + i)
259+ h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(1)).Once()
260+ p.createNotify(xid, false)
261+ readUntil(t, x, func() bool { return x.tab.lookup(xid) != 0 })
262+ s := x.tab.lookup(xid)
263+ x.tab.rec(s).ready = true
264+ x.Configure(s, ports.ConfigureWindow{Width: 10, Height: 10})
265+ h.EXPECT().Dissociate(s, true).Once()
266+ p.destroyNotify(xid)
267+ readUntil(t, x, func() bool { return x.tab.lookup(xid) == 0 })
268+ }
269+ require.Equal(t, c0, cap(x.dirty))
270+ require.LessOrEqual(t, len(x.dirty), c0)
271+}
272+
273+func TestPublicCommandsIgnoredWhenClosed(t *testing.T) {
274+ x, p, h, src, _ := attached(t)
275+ const win = 0x500001
276+ s := newWin(t, x, p, h, win)
277+ h.EXPECT().Dissociate(s, true).Once()
278+ src.EXPECT().Remove().Once()
279+ x.Close()
280+ require.NotPanics(t, func() {
281+ x.Configure(s, ports.ConfigureWindow{Width: 1, Height: 1})
282+ x.Place(s, 1, 2)
283+ x.Focus(s)
284+ x.CloseWindow(s)
285+ })
286+ require.Empty(t, x.dirty)
287+ require.Zero(t, x.active)
288+}
289+
290+// Focus.
291+
292+type focusModel struct {
293+ name string
294+ input, take bool
295+ setFocus bool // SetInputFocus on the window
296+ takeFocus bool // WM_TAKE_FOCUS sent
297+}
298+
299+func TestFocusInputModels(t *testing.T) {
300+ for _, m := range []focusModel{
301+ {"passive", true, false, true, false},
302+ {"locally active", true, true, true, true},
303+ {"globally active", false, true, false, true},
304+ {"no input", false, false, false, false},
305+ } {
306+ t.Run(m.name, func(t *testing.T) {
307+ x, p, h, _, _ := attached(t)
308+ const win = 0x500001
309+ s := newWin(t, x, p, h, win)
310+ r := x.tab.rec(s)
311+ r.input, r.takeFocus = m.input, m.take
312+ n := settle(t, x, p)
313+
314+ x.Focus(s)
315+ settle(t, x, p)
316+ var focuses []xpeerReq
317+ for _, r := range since(p, n, opSetInputFocus) {
318+ if r.u32(4) == win {
319+ focuses = append(focuses, r)
320+ }
321+ }
322+ if m.setFocus {
323+ require.Len(t, focuses, 1)
324+ require.EqualValues(t, xproto.InputFocusNone, focuses[0].raw[1], "never PointerRoot")
325+ require.Zero(t, focuses[0].u32(8), "CurrentTime")
326+ } else {
327+ require.Empty(t, focuses)
328+ }
329+ var takes int
330+ for _, ev := range sentEvents(p, n, win) {
331+ if ev[0] == 33 && binary.LittleEndian.Uint32(ev[8:]) == p.atom("WM_PROTOCOLS") {
332+ require.EqualValues(t, 32, ev[1])
333+ require.Equal(t, p.atom("WM_TAKE_FOCUS"), binary.LittleEndian.Uint32(ev[12:]))
334+ require.Zero(t, binary.LittleEndian.Uint32(ev[16:]), "CurrentTime")
335+ takes++
336+ }
337+ }
338+ require.Equal(t, map[bool]int{true: 1, false: 0}[m.takeFocus], takes)
339+
340+ act, ok := p.prop(peerRoot, "_NET_ACTIVE_WINDOW")
341+ require.True(t, ok)
342+ require.Equal(t, []uint32{win}, atomsOf(act), "named even for No-input windows")
343+ st, ok := p.prop(win, "_NET_WM_STATE")
344+ require.True(t, ok)
345+ require.Contains(t, atomsOf(st), p.atom("_NET_WM_STATE_FOCUSED"))
346+ })
347+ }
348+}
349+
350+func TestFocusActiveWindowWrittenOnlyOnChange(t *testing.T) {
351+ x, p, h, _, _ := attached(t)
352+ const a, b = 0x500001, 0x500002
353+ sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b)
354+ n := settle(t, x, p)
355+
356+ x.Focus(sa)
357+ x.Focus(sa)
358+ settle(t, x, p)
359+ require.Len(t, propsTo(p, n, peerRoot, "_NET_ACTIVE_WINDOW"), 1)
360+
361+ x.Focus(sb)
362+ settle(t, x, p)
363+ act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW")
364+ require.Equal(t, []uint32{b}, atomsOf(act))
365+ sta, _ := p.prop(a, "_NET_WM_STATE")
366+ require.NotContains(t, atomsOf(sta), p.atom("_NET_WM_STATE_FOCUSED"), "old window loses FOCUSED")
367+ stb, _ := p.prop(b, "_NET_WM_STATE")
368+ require.Contains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED"))
369+
370+ n = settle(t, x, p)
371+ x.Focus(0)
372+ settle(t, x, p)
373+ none := since(p, n, opSetInputFocus)
374+ require.Len(t, none, 1)
375+ require.Zero(t, none[0].u32(4), "focus None")
376+ require.EqualValues(t, xproto.InputFocusNone, none[0].raw[1])
377+ act, _ = p.prop(peerRoot, "_NET_ACTIVE_WINDOW")
378+ require.Equal(t, []uint32{0}, atomsOf(act))
379+ stb, _ = p.prop(b, "_NET_WM_STATE")
380+ require.NotContains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED"))
381+ require.Zero(t, x.active)
382+}
383+
384+func TestFocusStackingLists(t *testing.T) {
385+ x, p, h, _, _ := attached(t)
386+ const a, b, c = 0x500001, 0x500002, 0x500003
387+ sa := newWin(t, x, p, h, a)
388+ newWin(t, x, p, h, b)
389+ newWin(t, x, p, h, c)
390+ for _, w := range []uint32{a, b, c} {
391+ x.addClient(w)
392+ }
393+ settle(t, x, p)
394+ list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST")
395+ require.Equal(t, []uint32{a, b, c}, atomsOf(list))
396+ n := settle(t, x, p)
397+
398+ x.Focus(sa)
399+ settle(t, x, p)
400+ stack, _ := p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING")
401+ require.Equal(t, []uint32{b, c, a}, atomsOf(stack), "bottom to top")
402+ list, _ = p.prop(peerRoot, "_NET_CLIENT_LIST")
403+ require.Equal(t, []uint32{a, b, c}, atomsOf(list), "map order")
404+ require.Empty(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), "clients list untouched by focus")
405+ var raised bool
406+ for _, r := range since(p, n, opConfigureWindow) {
407+ if r.u32(4) == a && binary.LittleEndian.Uint16(r.raw[8:]) == uint16(xproto.ConfigWindowStackMode) {
408+ require.EqualValues(t, xproto.StackModeAbove, r.u32(12))
409+ raised = true
410+ }
411+ }
412+ require.True(t, raised)
413+
414+ // A burst costs one write per list.
415+ n = settle(t, x, p)
416+ x.removeClient(b)
417+ x.removeClient(c)
418+ settle(t, x, p)
419+ require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), 1)
420+ require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST_STACKING"), 1)
421+ stack, _ = p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING")
422+ require.Equal(t, []uint32{a}, atomsOf(stack))
423+}
424+
425+func TestDestroyedActiveWindowClearsActive(t *testing.T) {
426+ x, p, h, _, _ := attached(t)
427+ const win = 0x500001
428+ s := newWin(t, x, p, h, win)
429+ x.addClient(win)
430+ x.Focus(s)
431+ settle(t, x, p)
432+ h.EXPECT().Dissociate(s, true).Once()
433+ p.destroyNotify(win)
434+ readUntil(t, x, func() bool { return x.tab.lookup(win) == 0 })
435+ settle(t, x, p)
436+ require.Zero(t, x.active)
437+ act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW")
438+ require.Equal(t, []uint32{0}, atomsOf(act))
439+ list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST")
440+ require.Empty(t, list.data)
441+}
442+
443+func TestFocusInDenied(t *testing.T) {
444+ x, p, h, _, _ := attached(t)
445+ const a, b = 0x500001, 0x500002
446+ sa := newWin(t, x, p, h, a)
447+ newWin(t, x, p, h, b)
448+ x.Focus(sa)
449+ n := settle(t, x, p)
450+
451+ focusIn := func(win uint32, detail byte) {
452+ p.event(9, func(e *[32]byte) {
453+ e[1] = detail
454+ binary.LittleEndian.PutUint32(e[4:], win)
455+ })
456+ }
457+ focusCount := func(skip int) int {
458+ c := 0
459+ for _, r := range since(p, skip, opSetInputFocus) {
460+ if r.u32(4) == a {
461+ c++
462+ }
463+ }
464+ return c
465+ }
466+ x.feed(t, p, func() { focusIn(a, 0) }) // the active window itself: fine
467+ x.feed(t, p, func() { focusIn(b, byte(xproto.NotifyDetailPointer)) })
468+ settle(t, x, p)
469+ require.Zero(t, focusCount(n))
470+
471+ x.feed(t, p, func() { focusIn(b, 0); focusIn(b, 0) })
472+ readUntil(t, x, func() bool { return x.stats.Events >= 5 })
473+ settle(t, x, p)
474+ require.Equal(t, 1, focusCount(n), "denied once per iteration")
475+ n = settle(t, x, p)
476+ settle(t, x, p)
477+ require.Zero(t, focusCount(n))
478+}
479+
480+func TestCloseWindow(t *testing.T) {
481+ x, p, h, _, _ := attached(t)
482+ const a, b = 0x500001, 0x500002
483+ sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b)
484+ x.tab.rec(sa).deleteWindow = true
485+ n := settle(t, x, p)
486+
487+ x.CloseWindow(sa)
488+ x.CloseWindow(sb)
489+ settle(t, x, p)
490+ evs := sentEvents(p, n, a)
491+ require.Len(t, evs, 1)
492+ require.EqualValues(t, 33, evs[0][0])
493+ require.EqualValues(t, 32, evs[0][1])
494+ require.Equal(t, p.atom("WM_PROTOCOLS"), binary.LittleEndian.Uint32(evs[0][8:]))
495+ require.Equal(t, p.atom("WM_DELETE_WINDOW"), binary.LittleEndian.Uint32(evs[0][12:]))
496+ require.Zero(t, binary.LittleEndian.Uint32(evs[0][16:]))
497+ require.Empty(t, sentEvents(p, n, b))
498+
499+ kills := since(p, n, opKillClient)
500+ require.Len(t, kills, 1)
501+ require.Equal(t, uint32(b), kills[0].u32(4))
502+}
503+
504+// Allocation guards. The peer sinks requests so the connection never blocks;
505+// its goroutine is idle in the sink loop before measuring.
506+
507+func sinkPeer(t *testing.T, x *XWM, p *xpeer) {
508+ t.Helper()
509+ p.sink.Store(true)
510+ n := len(p.requests())
511+ xproto.GetInputFocus(x.conn.Buf()) // served by the old loop iteration
512+ require.NoError(t, x.conn.Flush())
513+ p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus })
514+ // The peer records a request and answers it under mu: once the lock is
515+ // free again it is back in its sink loop, which does not allocate.
516+ p.mu.Lock()
517+ _ = p.seq
518+ p.mu.Unlock()
519+ x.src = nil // no source: write backpressure would not allocate either
520+ x.post = func(func()) {}
521+ x.warnedRing = true // the sink never answers: Expect slots stay used
522+}
523+
524+func TestConfigureFlushAllocations(t *testing.T) {
525+ x, p, h, _, _ := attached(t)
526+ s := newWin(t, x, p, h, 0x500001)
527+ sinkPeer(t, x, p)
528+ i := 0
529+ allocs := testing.AllocsPerRun(300, func() {
530+ i++
531+ x.Configure(s, ports.ConfigureWindow{Width: 100 + i%2, Height: 100, Fullscreen: i%3 == 0, Activated: i%2 == 0})
532+ x.Place(s, i%5, i%7)
533+ x.posted = false
534+ x.flush()
535+ })
536+ require.Zero(t, allocs)
537+ require.False(t, x.closed)
538+}
539+
540+func TestFocusAllocations(t *testing.T) {
541+ x, p, h, _, _ := attached(t)
542+ a, b := newWin(t, x, p, h, 0x500001), newWin(t, x, p, h, 0x500002)
543+ x.addClient(x.tab.rec(a).xid)
544+ x.addClient(x.tab.rec(b).xid)
545+ x.tab.rec(b).takeFocus = true
546+ x.tab.rec(a).input, x.tab.rec(b).input = true, true
547+ sinkPeer(t, x, p)
548+ i := 0
549+ allocs := testing.AllocsPerRun(150, func() {
550+ i++
551+ switch i % 3 {
552+ case 0:
553+ x.Focus(a)
554+ case 1:
555+ x.Focus(b)
556+ default:
557+ x.Focus(0)
558+ }
559+ x.refocus = i%5 == 0
560+ x.posted = false
561+ x.flush()
562+ })
563+ require.Zero(t, allocs)
564+ require.False(t, x.closed)
565+}
A internal/adapters/wayland/xwm/events.go +260 -0
@@ -0,0 +1,260 @@
1+package xwm
2+
3+import (
4+ "bnema.dev/xturbo"
5+ "bnema.dev/xturbo/composite"
6+ "bnema.dev/xturbo/res"
7+ "bnema.dev/xturbo/xfixes"
8+ "bnema.dev/xturbo/xproto"
9+)
10+
11+// Reply kinds registered with Expect. The first argument is the window tag
12+// (slot | generation<<16, see table.tag), the second a property index.
13+const (
14+ kindAttr uint16 = 16 + iota
15+ kindProp
16+ kindGeom
17+ kindClientIds
18+ kindConfigure
19+ kindFocus
20+ kindRootMask
21+ kindVersion
22+ kindTree
23+ kindOverrideType
24+)
25+
26+// onEvent dispatches an X event. Before Attach there is no host: only the
27+// table is maintained, and Attach replays the result.
28+func (x *XWM) onEvent(ev *xturbo.Event) {
29+ x.stats.Events++
30+ if x.closed {
31+ return
32+ }
33+ switch ev.Kind {
34+ case xproto.KindCreateNotify:
35+ x.onCreateNotify(ev)
36+ case xproto.KindDestroyNotify:
37+ x.onDestroyNotify(ev)
38+ case xproto.KindReparentNotify:
39+ x.onReparentNotify(ev)
40+ }
41+ if x.host == nil {
42+ x.onEventDetached(ev)
43+ return
44+ }
45+ switch ev.Kind {
46+ case xproto.KindMapRequest:
47+ x.onMapRequest(ev)
48+ case xproto.KindMapNotify:
49+ x.onMapNotify(ev)
50+ case xproto.KindUnmapNotify:
51+ x.onUnmapNotify(ev)
52+ case xproto.KindConfigureRequest:
53+ x.onConfigureRequest(ev)
54+ case xproto.KindConfigureNotify:
55+ x.onConfigureNotify(ev)
56+ case xproto.KindPropertyNotify:
57+ x.onPropertyNotify(ev)
58+ case xproto.KindClientMessage:
59+ x.onClientMessage(ev)
60+ case xproto.KindFocusIn:
61+ x.onFocusIn(ev)
62+ }
63+}
64+
65+// onEventDetached keeps what Attach replays: windows asked to map, and those
66+// withdrawn again, so Attach skips them; geometry requests are answered.
67+func (x *XWM) onEventDetached(ev *xturbo.Event) {
68+ switch ev.Kind {
69+ case xproto.KindMapRequest:
70+ if s := x.tab.lookup(uint32(xproto.MapRequestOf(ev).Window())); s != 0 {
71+ x.tab.rec(s).mapping = true
72+ }
73+ case xproto.KindUnmapNotify:
74+ if ev.SendEvent {
75+ return
76+ }
77+ if s := x.tab.lookup(uint32(xproto.UnmapNotifyOf(ev).Window())); s != 0 {
78+ r := x.tab.rec(s)
79+ r.mapped, r.mapping = false, false
80+ }
81+ case xproto.KindConfigureRequest:
82+ x.onConfigureRequest(ev)
83+ }
84+}
85+
86+func (x *XWM) onReply(r *xturbo.Reply) {
87+ x.stats.Replies++
88+ if x.closed {
89+ return
90+ }
91+ switch r.Kind {
92+ case kindVersion:
93+ x.onVersionReply(r)
94+ return
95+ case kindTree:
96+ x.onTreeReply(r)
97+ return
98+ }
99+ s, rec := x.tab.live(r.Args[0])
100+ if rec == nil {
101+ x.stats.Stale++
102+ return
103+ }
104+ switch r.Kind {
105+ case kindAttr:
106+ x.onAttrReply(rec, r)
107+ case kindProp:
108+ x.onPropReply(s, rec, r)
109+ case kindGeom:
110+ x.onGeomReply(s, rec, r)
111+ case kindClientIds:
112+ x.onClientIdsReply(s, rec, r)
113+ case kindOverrideType:
114+ x.onOverrideType(s, rec, r)
115+ }
116+}
117+
118+// onError never tears the XWM down: an error names a request, not the
119+// connection. Sticky Buf errors surface through Flush.
120+func (x *XWM) onError(e *xturbo.XError) {
121+ x.stats.Errors++
122+ if e.Kind == 0 {
123+ x.stats.Unattributed++
124+ x.log.Debug().Uint8("code", e.Code).Uint8("major", e.Major).Uint16("minor", e.Minor).
125+ Uint64("seq", uint64(e.Seq)).Msg("unattributed X error")
126+ return
127+ }
128+ switch e.Kind {
129+ case kindRootMask:
130+ if e.Code == xproto.BadAccess {
131+ x.bootErr = ErrWMExists
132+ }
133+ case kindProp, kindGeom, kindClientIds, kindConfigure, kindFocus, kindOverrideType:
134+ // Usually BadWindow: the window died between request and reply and
135+ // DestroyNotify follows. Whatever the code, the awaited reply will
136+ // never come, so the map must not wait for it.
137+ s, rec := x.tab.live(e.Args[0])
138+ if rec == nil {
139+ x.stats.Stale++
140+ return
141+ }
142+ switch e.Kind {
143+ case kindProp:
144+ rec.pending &^= 1 << e.Args[1]
145+ case kindGeom:
146+ rec.pending &^= propGeom
147+ case kindClientIds:
148+ rec.pending &^= propClientIDs
149+ case kindOverrideType:
150+ if e.Code != xproto.BadWindow { // typed unknown: the window still shows
151+ x.overrideReady(s)
152+ }
153+ return
154+ default:
155+ return
156+ }
157+ if e.Code != xproto.BadWindow && e.Code != xproto.BadDrawable { // a dead window is never announced
158+ x.checkReady(s)
159+ }
160+ }
161+}
162+
163+// onCreateNotify records a new top-level window. Override-redirect windows
164+// are kept (their geometry and serial matter later) but never reach the host.
165+func (x *XWM) onCreateNotify(ev *xturbo.Event) {
166+ e := xproto.CreateNotifyOf(ev)
167+ xid := uint32(e.Window())
168+ if xid == x.wmWin || x.tab.lookup(xid) != 0 {
169+ return
170+ }
171+ s := x.insert(xid)
172+ if s == 0 {
173+ return
174+ }
175+ r := x.tab.rec(s)
176+ override := e.OverrideRedirect()
177+ r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height())
178+ if override {
179+ r.kind = kindUnmanaged
180+ x.stats.UnmanagedSeen++
181+ return
182+ }
183+ if x.host != nil {
184+ x.adopt(s)
185+ }
186+}
187+
188+// adopt announces a managed window to the host and starts watching it.
189+func (x *XWM) adopt(s Slot) {
190+ r := x.tab.rec(s)
191+ r.id = x.host.Created(s, r.xid)
192+ var v xproto.ChangeWindowAttributesValues
193+ v.SetEventMask(xproto.EventMaskPropertyChange | xproto.EventMaskFocusChange)
194+ x.expect(xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(r.xid), &v), kindAttr, x.tab.tag(s), 0)
195+ x.log.Debug().Uint32("xid", r.xid).Uint32("slot", uint32(s)).Msg("x11 window created")
196+}
197+
198+func (x *XWM) onDestroyNotify(ev *xturbo.Event) {
199+ xid := uint32(xproto.DestroyNotifyOf(ev).Window())
200+ s := x.tab.lookup(xid)
201+ if s == 0 {
202+ return
203+ }
204+ x.forgetWindow(s)
205+}
206+
207+// onTreeReply (boot): record every existing child and ask for its attributes.
208+func (x *XWM) onTreeReply(r *xturbo.Reply) {
209+ q, ok := xproto.ParseQueryTreeReply(r.Raw)
210+ if !ok {
211+ return
212+ }
213+ children := q.Children()
214+ for i := 0; i < children.Len(); i++ {
215+ xid := uint32(children.At(i))
216+ if xid == x.wmWin || x.tab.lookup(xid) != 0 {
217+ continue
218+ }
219+ s := x.insert(xid)
220+ if s == 0 {
221+ return
222+ }
223+ x.expect(xproto.GetWindowAttributes(x.conn.Buf(), xproto.Window(xid)), kindAttr, x.tab.tag(s), 0)
224+ }
225+}
226+
227+// onAttrReply (boot): classify an existing window; viewable ones go through
228+// the map flow once the host is attached.
229+func (x *XWM) onAttrReply(rec *record, r *xturbo.Reply) {
230+ a, ok := xproto.ParseGetWindowAttributesReply(r.Raw)
231+ if !ok || x.host != nil {
232+ return
233+ }
234+ if a.OverrideRedirect() {
235+ rec.kind = kindUnmanaged
236+ rec.mapped = a.MapState() == xproto.MapStateViewable
237+ x.stats.UnmanagedSeen++
238+ return
239+ }
240+ if a.MapState() == xproto.MapStateViewable {
241+ rec.mapped, rec.mapping = true, true
242+ }
243+}
244+
245+func (x *XWM) onVersionReply(r *xturbo.Reply) {
246+ switch r.Args[0] {
247+ case versionComposite:
248+ if v, ok := composite.ParseQueryVersionReply(r.Raw); ok {
249+ x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("Composite")
250+ }
251+ case versionXFixes:
252+ if v, ok := xfixes.ParseQueryVersionReply(r.Raw); ok {
253+ x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("XFixes")
254+ }
255+ case versionRes:
256+ if v, ok := res.ParseQueryVersionReply(r.Raw); ok {
257+ x.log.Info().Uint16("major", v.ServerMajor()).Uint16("minor", v.ServerMinor()).Msg("X-Resource")
258+ }
259+ }
260+}
A internal/adapters/wayland/xwm/fdsource_mock_test.go +129 -0
@@ -0,0 +1,129 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package xwm
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMockfdSource creates a new instance of mockfdSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMockfdSource(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mockfdSource {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &mockfdSource{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// mockfdSource is an autogenerated mock type for the fdSource type
35+type mockfdSource struct {
36+ mock.Mock
37+}
38+
39+type mockfdSource_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *mockfdSource) EXPECT() *mockfdSource_Expecter {
44+ return &mockfdSource_Expecter{mock: &_m.Mock}
45+}
46+
47+// Remove provides a mock function for the type mockfdSource
48+func (_mock *mockfdSource) Remove() {
49+ _mock.Called()
50+ return
51+}
52+
53+// mockfdSource_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
54+type mockfdSource_Remove_Call struct {
55+ *mock.Call
56+}
57+
58+// Remove is a helper method to define mock.On call
59+func (_e *mockfdSource_Expecter) Remove() *mockfdSource_Remove_Call {
60+ return &mockfdSource_Remove_Call{Call: _e.mock.On("Remove")}
61+}
62+
63+func (_c *mockfdSource_Remove_Call) Run(run func()) *mockfdSource_Remove_Call {
64+ _c.Call.Run(func(args mock.Arguments) {
65+ run()
66+ })
67+ return _c
68+}
69+
70+func (_c *mockfdSource_Remove_Call) Return() *mockfdSource_Remove_Call {
71+ _c.Call.Return()
72+ return _c
73+}
74+
75+func (_c *mockfdSource_Remove_Call) RunAndReturn(run func()) *mockfdSource_Remove_Call {
76+ _c.Run(run)
77+ return _c
78+}
79+
80+// SetWritable provides a mock function for the type mockfdSource
81+func (_mock *mockfdSource) SetWritable(on bool) error {
82+ ret := _mock.Called(on)
83+
84+ if len(ret) == 0 {
85+ panic("no return value specified for SetWritable")
86+ }
87+
88+ var r0 error
89+ if returnFunc, ok := ret.Get(0).(func(bool) error); ok {
90+ r0 = returnFunc(on)
91+ } else {
92+ r0 = ret.Error(0)
93+ }
94+ return r0
95+}
96+
97+// mockfdSource_SetWritable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWritable'
98+type mockfdSource_SetWritable_Call struct {
99+ *mock.Call
100+}
101+
102+// SetWritable is a helper method to define mock.On call
103+// - on bool
104+func (_e *mockfdSource_Expecter) SetWritable(on any) *mockfdSource_SetWritable_Call {
105+ return &mockfdSource_SetWritable_Call{Call: _e.mock.On("SetWritable", on)}
106+}
107+
108+func (_c *mockfdSource_SetWritable_Call) Run(run func(on bool)) *mockfdSource_SetWritable_Call {
109+ _c.Call.Run(func(args mock.Arguments) {
110+ var arg0 bool
111+ if args[0] != nil {
112+ arg0 = args[0].(bool)
113+ }
114+ run(
115+ arg0,
116+ )
117+ })
118+ return _c
119+}
120+
121+func (_c *mockfdSource_SetWritable_Call) Return(err error) *mockfdSource_SetWritable_Call {
122+ _c.Call.Return(err)
123+ return _c
124+}
125+
126+func (_c *mockfdSource_SetWritable_Call) RunAndReturn(run func(on bool) error) *mockfdSource_SetWritable_Call {
127+ _c.Call.Return(run)
128+ return _c
129+}
A internal/adapters/wayland/xwm/fixes_test.go +230 -0
@@ -0,0 +1,230 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "testing"
6+
7+ "bnema.dev/xturbo"
8+ "bnema.dev/xturbo/xproto"
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/stretchr/testify/mock"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+func getProps(p *xpeer, skip int, win uint32) int {
15+ n := 0
16+ for _, r := range since(p, skip, opGetProperty) {
17+ if r.u32(4) == win {
18+ n++
19+ }
20+ }
21+ return n
22+}
23+
24+// Our own _NET_WM_STATE writes must not be read back once the window is ready.
25+func TestOwnNetWMStateWriteIsNotReadBack(t *testing.T) {
26+ hs := newHarness(t, nil)
27+ const win = 0x500001
28+ hs.classic(win)
29+ s := hs.mapAndWait(win)
30+
31+ hs.x.Configure(s, ports.ConfigureWindow{Width: 300, Height: 200, Fullscreen: true})
32+ n := settle(t, hs.x, hs.p)
33+ require.True(t, hs.x.tab.rec(s).fullscreen, "the written state is the known state")
34+ st, _ := hs.p.prop(win, "_NET_WM_STATE")
35+ require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FULLSCREEN")}, atomsOf(st))
36+
37+ ev := hs.x.Stats().Events
38+ hs.p.propertyNotify(win, hs.p.atom("_NET_WM_STATE"), false)
39+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Events > ev })
40+ settle(t, hs.x, hs.p)
41+ require.Zero(t, getProps(hs.p, n, win), "no GetProperty")
42+
43+}
44+
45+// A pre-map XMoveResize is honoured, floating or not.
46+func TestConfigureRequestBeforeReadyIsHonoured(t *testing.T) {
47+ hs := newHarness(t, nil)
48+ const win = 0x500001
49+ hs.classic(win)
50+ hs.create(win)
51+ n := settle(t, hs.x, hs.p)
52+
53+ mask := uint16(xproto.ConfigWindowX | xproto.ConfigWindowY | xproto.ConfigWindowWidth | xproto.ConfigWindowHeight)
54+ hs.x.feed(t, hs.p, func() { sendConfigureRequest(hs.p, win, mask, 11, 22, 640, 70000&0xffff) })
55+ settle(t, hs.x, hs.p)
56+ cw := since(hs.p, n, opConfigureWindow)
57+ require.Len(t, cw, 1)
58+ require.Equal(t, []uint32{11, 22, 640, 4464}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20), cw[0].u32(24)})
59+ require.Len(t, sentEvents(hs.p, n, win), 1)
60+
61+ hs.expectReady(win)
62+ hs.p.mapRequest(win)
63+ readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
64+ require.Equal(t, 640, hs.ready[win].Width)
65+ require.Equal(t, 4464, hs.ready[win].Height)
66+ require.False(t, hs.ready[win].Floating)
67+}
68+
69+// After a remap the state is rewritten from scratch, FOCUSED included.
70+func TestRemapRewritesStateWithFocused(t *testing.T) {
71+ hs := newHarness(t, nil)
72+ const win = 0x500001
73+ hs.classic(win)
74+ s := hs.mapAndWait(win)
75+ hs.x.Focus(s)
76+ settle(t, hs.x, hs.p)
77+ st, _ := hs.p.prop(win, "_NET_WM_STATE")
78+ require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st))
79+
80+ hs.h.EXPECT().Dissociate(s, false).Once()
81+ hs.p.unmapNotify(win, false)
82+ readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).mapping })
83+ delete(hs.ready, win)
84+ hs.expectReady(win)
85+ hs.p.mapRequest(win)
86+ readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
87+
88+ hs.x.Focus(s)
89+ settle(t, hs.x, hs.p)
90+ st, _ = hs.p.prop(win, "_NET_WM_STATE")
91+ require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st))
92+}
93+
94+// Replies of a previous map never satisfy the next one (xturbo usually retires
95+// their slots first; the generation guards the rest).
96+func TestStaleRepliesOfPreviousMapAreDropped(t *testing.T) {
97+ hs := newHarness(t, nil)
98+ const win = 0x500001
99+ hs.classic(win)
100+ s := hs.create(win)
101+ hs.p.holdReplies() // the second map is never answered
102+ hs.p.mapRequest(win)
103+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapping })
104+ oldTag := hs.x.tab.tag(s)
105+ hs.h.EXPECT().Dissociate(s, false).Once()
106+ hs.p.unmapNotify(win, false)
107+ readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).mapping })
108+ hs.p.mapRequest(win)
109+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapping })
110+ require.NotEqual(t, oldTag, hs.x.tab.tag(s))
111+
112+ rep := make([]byte, 32)
113+ rep[0] = 1
114+ for idx := 0; idx < propCount; idx++ {
115+ hs.x.onReply(&xturbo.Reply{Kind: kindProp, Args: [4]uint32{oldTag, uint32(idx)}, Raw: rep})
116+ }
117+ hs.x.onReply(&xturbo.Reply{Kind: kindGeom, Args: [4]uint32{oldTag}, Raw: rep})
118+ require.EqualValues(t, propCount+1, hs.x.Stats().Stale)
119+ require.False(t, hs.x.tab.rec(s).ready)
120+ require.Equal(t, propMaskAll&^propClientIDs, hs.x.tab.rec(s).pending, "the new map still awaits its own replies")
121+}
122+
123+func TestGetGeometryBadDrawableIsNotReady(t *testing.T) {
124+ hs := newHarness(t, nil)
125+ const win = 0x500001
126+ hs.classic(win)
127+ s := hs.create(win)
128+ hs.p.set(func(p *xpeer) { p.bad[win] = true })
129+ hs.p.mapRequest(win)
130+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Errors >= uint64(propCount)+1 })
131+ require.Zero(t, hs.x.Stats().Unattributed)
132+ require.False(t, hs.x.tab.rec(s).ready)
133+ hs.h.EXPECT().Dissociate(s, true).Once()
134+ hs.p.destroyNotify(win)
135+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 })
136+}
137+
138+// A host that refocuses the window while dissociating it must not leave a
139+// dangling active slot.
140+func TestForgetWindowClearsActiveSetByHost(t *testing.T) {
141+ x, p, h, _, _ := attached(t)
142+ const win = 0x500001
143+ s := newWin(t, x, p, h, win)
144+ h.EXPECT().Dissociate(s, true).Run(func(sl Slot, _ bool) { x.Focus(sl) }).Once()
145+ p.destroyNotify(win)
146+ readUntil(t, x, func() bool { return x.tab.lookup(win) == 0 })
147+ require.Zero(t, x.active)
148+ require.False(t, x.refocus)
149+ settle(t, x, p)
150+ act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW")
151+ require.Equal(t, []uint32{0}, atomsOf(act))
152+}
153+
154+func TestTitleAndAppIDAreValidUTF8(t *testing.T) {
155+ hs := newHarness(t, nil)
156+ const win = 0x500001
157+ hs.classic(win)
158+ hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) })
159+ hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("caf\xe9"))
160+ hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("i\x00C\xe9\x00"))
161+ s := hs.mapAndWait(win)
162+ require.Equal(t, "caf\uFFFD", hs.ready[win].Title)
163+ require.Equal(t, "C\uFFFD", hs.ready[win].AppID)
164+
165+ // Unchanged after sanitising: no host call.
166+ hs.notifyWMName(win)
167+ // A changed truncated multibyte title reaches the host sanitised.
168+ hs.h.EXPECT().Title(s, "\u00e9\uFFFD").Once()
169+ hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("\xc3\xa9\xe2\x82"))
170+ hs.notifyWMName(win)
171+}
172+
173+func TestSyntheticConfigureNotifyClampsCoordinates(t *testing.T) {
174+ x, p, h, _, _ := attached(t)
175+ const win = 0x500001
176+ s := newWin(t, x, p, h, win)
177+ n := settle(t, x, p)
178+ r := x.tab.rec(s)
179+ r.x, r.y, r.w, r.h = 70000, -70000, 70000, 5
180+ x.sendConfigureNotify(r)
181+ settle(t, x, p)
182+ ev := sentEvents(p, n, win)
183+ require.Len(t, ev, 1)
184+ require.EqualValues(t, 32767, int16(binary.LittleEndian.Uint16(ev[0][16:])))
185+ require.EqualValues(t, -32768, int16(binary.LittleEndian.Uint16(ev[0][18:])))
186+ require.EqualValues(t, 65535, binary.LittleEndian.Uint16(ev[0][20:]))
187+ require.EqualValues(t, 5, binary.LittleEndian.Uint16(ev[0][22:]))
188+}
189+
190+// A window withdrawn before Attach is not mapped by Attach; a pre-attach
191+// ConfigureRequest is honoured.
192+func TestUnmapAndConfigureBeforeAttach(t *testing.T) {
193+ x, p := bootPeer(t, nil)
194+ const gone, sized = 0x500001, 0x500002
195+ for _, w := range []uint32{gone, sized} {
196+ p.setProp(w, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("a\x00B\x00"))
197+ p.geoms[w] = xpeerGeom{w: 300, h: 200}
198+ p.createNotify(w, false)
199+ p.mapRequest(w)
200+ }
201+ p.unmapNotify(gone, false)
202+ mask := uint16(xproto.ConfigWindowWidth | xproto.ConfigWindowHeight)
203+ sendConfigureRequest(p, sized, mask, 0, 0, 500, 400)
204+ readUntil(t, x, func() bool { return x.Stats().Events >= 6 })
205+ n := len(p.requests())
206+
207+ h := newMockhost(t)
208+ src := newMockfdSource(t)
209+ src.EXPECT().Remove().Maybe()
210+ post, _ := newPost()
211+ var got WindowInfo
212+ done := false
213+ h.EXPECT().Created(mock.Anything, mock.Anything).Return(ports.WindowID(1)).Twice()
214+ h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, i WindowInfo) { got, done = i, true }).Once()
215+ x.attach(src, h, post)
216+ readUntil(t, x, func() bool { return done })
217+ settle(t, x, p)
218+ for _, r := range since(p, n, opMapWindow) {
219+ require.NotEqual(t, uint32(gone), r.u32(4), "withdrawn window must not be mapped")
220+ }
221+ require.Equal(t, 500, got.Width)
222+ require.Equal(t, 400, got.Height)
223+}
224+
225+func (hs *harness) notifyWMName(win uint32) {
226+ hs.t.Helper()
227+ before := hs.x.Stats().Replies
228+ hs.p.propertyNotify(win, uint32(xproto.AtomWmName), false)
229+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before })
230+}
A internal/adapters/wayland/xwm/focus.go +218 -0
@@ -0,0 +1,218 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "slices"
6+
7+ "bnema.dev/xturbo"
8+ "bnema.dev/xturbo/xproto"
9+)
10+
11+// Bits of XWM.listsDirty: the client lists to rewrite at flush.
12+const (
13+ listClients uint8 = 1 << iota
14+ listStacking
15+)
16+
17+// onFocusIn denies a focus change the XWM did not make: Wayland is the truth.
18+// The active window's focus is re-issued once at the next flush.
19+func (x *XWM) onFocusIn(ev *xturbo.Event) {
20+ e := xproto.FocusInOf(ev)
21+ if e.Detail() == xproto.NotifyDetailPointer {
22+ return
23+ }
24+ // Keyboard grabs are how menus steal focus legitimately (wlroots does
25+ // the same): fighting them would close every Steam menu.
26+ if e.Mode() == xproto.NotifyModeGrab || e.Mode() == xproto.NotifyModeUngrab {
27+ return
28+ }
29+ s := x.tab.lookup(uint32(e.Event()))
30+ if s == 0 || s == x.active {
31+ return
32+ }
33+ if r := x.tab.rec(s); r.kind == kindUnmanaged {
34+ return
35+ }
36+ x.refocus = true
37+ x.wake()
38+}
39+
40+// Focus gives keyboard focus to s; 0 means a native Wayland window or none.
41+// Focus never uses PointerRoot: without an X window to focus it is None.
42+func (x *XWM) Focus(s Slot) {
43+ if x.closed {
44+ return
45+ }
46+ r := x.managedRec(s)
47+ if r == nil {
48+ s = 0
49+ }
50+ if s == x.active {
51+ return
52+ }
53+ if old := x.managedRec(x.active); old != nil {
54+ old.pend.activated = false
55+ x.markDirty(x.active, old)
56+ }
57+ x.active = s
58+ if r == nil {
59+ x.issueFocus(0, nil)
60+ x.setActive(0)
61+ x.wake()
62+ return
63+ }
64+ r.pend.activated = true
65+ x.markDirty(s, r)
66+ x.issueFocus(s, r)
67+ x.setActive(r.xid)
68+ x.raise(r)
69+ x.wake()
70+}
71+
72+// issueFocus applies the ICCCM input model of r: Passive and Locally active
73+// windows get SetInputFocus, Locally and Globally active ones WM_TAKE_FOCUS,
74+// No-input windows nothing. A nil r clears the X focus.
75+func (x *XWM) issueFocus(s Slot, r *record) {
76+ b := x.conn.Buf()
77+ if r == nil {
78+ xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.WindowNone, 0)
79+ return
80+ }
81+ if r.input {
82+ x.expect(xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.Window(r.xid), 0), kindFocus, x.tab.tag(s), 0)
83+ }
84+ if r.takeFocus {
85+ x.sendProtocol(r.xid, atomWMTakeFocus)
86+ }
87+}
88+
89+// sendProtocol sends a WM_PROTOCOLS client message [proto, CurrentTime].
90+func (x *XWM) sendProtocol(xid uint32, proto int) {
91+ var raw [32]byte
92+ raw[0], raw[1] = 33, 32 // ClientMessage, format 32
93+ binary.LittleEndian.PutUint32(raw[4:], xid)
94+ binary.LittleEndian.PutUint32(raw[8:], x.atoms[atomWMProtocols])
95+ binary.LittleEndian.PutUint32(raw[12:], x.atoms[proto])
96+ xproto.SendEvent(x.conn.Buf(), false, xproto.Window(xid), xproto.EventMaskNoEvent, &raw)
97+}
98+
99+// setActive writes _NET_ACTIVE_WINDOW on the root.
100+func (x *XWM) setActive(xid uint32) {
101+ v := [1]uint32{xid}
102+ x.setProp32(x.root, atomNetActiveWindow, xproto.AtomWindow, v[:])
103+}
104+
105+// raise restacks r above its siblings and at the top of the stacking list.
106+func (x *XWM) raise(r *record) {
107+ var v xproto.ConfigureWindowValues
108+ v.SetStackMode(xproto.StackModeAbove)
109+ xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v)
110+ if i := slices.Index(x.stacking, r.xid); i >= 0 {
111+ x.stacking = append(x.stacking[:i], x.stacking[i+1:]...)
112+ x.stacking = append(x.stacking, r.xid)
113+ x.listsDirty |= listStacking
114+ }
115+}
116+
117+// CloseWindow asks s to close (WM_DELETE_WINDOW), or kills its client.
118+func (x *XWM) CloseWindow(s Slot) {
119+ r := x.managedRec(s)
120+ if r == nil {
121+ return
122+ }
123+ if r.deleteWindow {
124+ x.sendProtocol(r.xid, atomWMDeleteWindow)
125+ } else {
126+ xproto.KillClient(x.conn.Buf(), r.xid)
127+ }
128+ x.wake()
129+}
130+
131+// addClient puts a ready window at the end of both client lists.
132+func (x *XWM) addClient(xid uint32) {
133+ if !slices.Contains(x.clients, xid) {
134+ x.clients = append(x.clients, xid)
135+ x.listsDirty |= listClients
136+ }
137+ if !slices.Contains(x.stacking, xid) {
138+ x.stacking = append(x.stacking, xid)
139+ x.listsDirty |= listStacking
140+ }
141+ x.wake()
142+}
143+
144+// removeClient drops an unmapped or destroyed window from both client lists.
145+func (x *XWM) removeClient(xid uint32) {
146+ if i := slices.Index(x.clients, xid); i >= 0 {
147+ x.clients = slices.Delete(x.clients, i, i+1)
148+ x.listsDirty |= listClients
149+ }
150+ if i := slices.Index(x.stacking, xid); i >= 0 {
151+ x.stacking = slices.Delete(x.stacking, i, i+1)
152+ x.listsDirty |= listStacking
153+ }
154+ x.wake()
155+}
156+
157+// dropWindow takes s out of the client lists, the focus and the configure
158+// queue when it stops being mapped (unmap, destroy). It is idempotent.
159+func (x *XWM) dropWindow(s Slot) {
160+ r := x.tab.rec(s)
161+ if r != nil {
162+ r.ovQueued = false // a reused slot must not inherit a queued report
163+ }
164+ if r == nil || r.kind != kindManaged {
165+ return
166+ }
167+ x.removeClient(r.xid)
168+ r.pend.dirty = false // the queued slot is skipped at flush
169+ // A remap starts from a clean _NET_WM_STATE, so Focus writes FOCUSED again.
170+ r.pend.activated, r.sent.activated = false, false
171+ r.sent.fullscreen, r.stateSent = false, 0
172+ if x.active == s {
173+ x.active = 0
174+ x.refocus = false
175+ x.setActive(0)
176+ }
177+}
178+
179+// forgetWindow ends the life of s (destroy, reparent away): it is dropped,
180+// the host is told if it knew the window, and the record is freed.
181+func (x *XWM) forgetWindow(s Slot) {
182+ r := x.tab.rec(s)
183+ if r == nil || r.kind == kindFree {
184+ return
185+ }
186+ x.dropWindow(s)
187+ if r.kind == kindUnmanaged && r.ovPaired && x.host != nil {
188+ r.ovPaired = false
189+ x.host.OverrideGone(s)
190+ }
191+ if r.kind == kindManaged && r.id != 0 && x.host != nil {
192+ r.id = 0
193+ x.host.Dissociate(s, true)
194+ if x.active == s { // the host re-focused the window while dissociating it
195+ x.active, x.refocus = 0, false
196+ x.issueFocus(0, nil)
197+ x.setActive(0)
198+ x.wake()
199+ }
200+ }
201+ x.tab.remove(s)
202+}
203+
204+// flushFocus rewrites the client lists that changed and re-issues a denied
205+// focus, once per display iteration.
206+func (x *XWM) flushFocus() {
207+ if x.listsDirty&listClients != 0 {
208+ x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, x.clients)
209+ }
210+ if x.listsDirty&listStacking != 0 {
211+ x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, x.stacking)
212+ }
213+ x.listsDirty = 0
214+ if x.refocus {
215+ x.refocus = false
216+ x.issueFocus(x.active, x.managedRec(x.active))
217+ }
218+}
A internal/adapters/wayland/xwm/host.go +57 -0
@@ -0,0 +1,57 @@
1+package xwm
2+
3+import "github.com/bnema/neferwl/internal/ports"
4+
5+// Slot is the XWM's window slot: table index + 1, stable until the window is
6+// destroyed. 0 means none.
7+type Slot uint32
8+
9+// WindowInfo is what the XWM learned about a managed window by the time it is
10+// ready to be mapped. The host copies it.
11+type WindowInfo struct {
12+ PID int
13+ Floating bool // dialog/utility/splash/fixed size
14+ Width, Height int // requested size, logical (scale 1)
15+ Parent Slot
16+ Modal bool // _NET_WM_STATE_MODAL
17+ Fullscreen bool // _NET_WM_STATE_FULLSCREEN at map
18+ Title, AppID string
19+}
20+
21+// host is the wayland server as the XWM sees it. Display goroutine only.
22+// All strings passed are owned by the XWM record and already deduplicated
23+// (only passed when changed).
24+type host interface {
25+ // Created: a managed (non-override-redirect) X window is known; the host
26+ // allocates a ports.WindowID and returns it. Not yet mapped.
27+ Created(slot Slot, xid uint32) ports.WindowID
28+ // Ready: properties read and MapWindow issued; the surface may still be
29+ // missing. Info is copied by the host.
30+ Ready(slot Slot, info WindowInfo)
31+ // Associate links the X window to the wl_surface with identity surfaceID.
32+ // The host maps the window on the surface's first buffer commit.
33+ Associate(slot Slot, surfaceID uint64)
34+ // Dissociate: UnmapNotify/DestroyNotify; the host emits WindowUnmapped if
35+ // mapped and drops the surface link. destroyed also frees the WindowID.
36+ Dissociate(slot Slot, destroyed bool)
37+ Title(slot Slot, title string) // changed only
38+ AppID(slot Slot, appID string) // changed only (WM_CLASS class)
39+ Parent(slot Slot, parent Slot) // 0 = none; after a WM_TRANSIENT_FOR change
40+ // Fullscreen forwards a fullscreen request (_NET_WM_STATE client message
41+ // or initial state); the host emits WindowFullscreenRequest.
42+ Fullscreen(slot Slot, on bool)
43+
44+ // OverrideMapped reports an override-redirect window that is mapped,
45+ // typed and paired with its wl_surface. Rect is in layout coordinates.
46+ OverrideMapped(slot Slot, xid uint32, surfaceID uint64, x, y, w, h int, noInput bool)
47+ // OverrideMoved reports a changed layout rect of a mapped override window.
48+ OverrideMoved(slot Slot, x, y, w, h int)
49+ // OverrideGone ends a mapped override window: unmapped or destroyed.
50+ OverrideGone(slot Slot)
51+}
52+
53+// fdSource is the display's event source for the X socket (*server.Source).
54+type fdSource interface {
55+ SetWritable(on bool) error
56+ Remove()
57+}
A internal/adapters/wayland/xwm/host_mock_test.go +611 -0
@@ -0,0 +1,611 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package xwm
6+
7+import (
8+ "github.com/bnema/neferwl/internal/ports"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// newMockhost creates a new instance of mockhost. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
13+// The first argument is typically a *testing.T value.
14+func newMockhost(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *mockhost {
18+ if helper, ok := t.(interface{ Helper() }); ok {
19+ helper.Helper()
20+ }
21+
22+ mock := &mockhost{}
23+ mock.Mock.Test(t)
24+
25+ t.Cleanup(func() {
26+ if helper, ok := t.(interface{ Helper() }); ok {
27+ helper.Helper()
28+ }
29+ mock.AssertExpectations(t)
30+ })
31+
32+ return mock
33+}
34+
35+// mockhost is an autogenerated mock type for the host type
36+type mockhost struct {
37+ mock.Mock
38+}
39+
40+type mockhost_Expecter struct {
41+ mock *mock.Mock
42+}
43+
44+func (_m *mockhost) EXPECT() *mockhost_Expecter {
45+ return &mockhost_Expecter{mock: &_m.Mock}
46+}
47+
48+// AppID provides a mock function for the type mockhost
49+func (_mock *mockhost) AppID(slot Slot, appID string) {
50+ _mock.Called(slot, appID)
51+ return
52+}
53+
54+// mockhost_AppID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppID'
55+type mockhost_AppID_Call struct {
56+ *mock.Call
57+}
58+
59+// AppID is a helper method to define mock.On call
60+// - slot Slot
61+// - appID string
62+func (_e *mockhost_Expecter) AppID(slot any, appID any) *mockhost_AppID_Call {
63+ return &mockhost_AppID_Call{Call: _e.mock.On("AppID", slot, appID)}
64+}
65+
66+func (_c *mockhost_AppID_Call) Run(run func(slot Slot, appID string)) *mockhost_AppID_Call {
67+ _c.Call.Run(func(args mock.Arguments) {
68+ var arg0 Slot
69+ if args[0] != nil {
70+ arg0 = args[0].(Slot)
71+ }
72+ var arg1 string
73+ if args[1] != nil {
74+ arg1 = args[1].(string)
75+ }
76+ run(
77+ arg0,
78+ arg1,
79+ )
80+ })
81+ return _c
82+}
83+
84+func (_c *mockhost_AppID_Call) Return() *mockhost_AppID_Call {
85+ _c.Call.Return()
86+ return _c
87+}
88+
89+func (_c *mockhost_AppID_Call) RunAndReturn(run func(slot Slot, appID string)) *mockhost_AppID_Call {
90+ _c.Run(run)
91+ return _c
92+}
93+
94+// Associate provides a mock function for the type mockhost
95+func (_mock *mockhost) Associate(slot Slot, surfaceID uint64) {
96+ _mock.Called(slot, surfaceID)
97+ return
98+}
99+
100+// mockhost_Associate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Associate'
101+type mockhost_Associate_Call struct {
102+ *mock.Call
103+}
104+
105+// Associate is a helper method to define mock.On call
106+// - slot Slot
107+// - surfaceID uint64
108+func (_e *mockhost_Expecter) Associate(slot any, surfaceID any) *mockhost_Associate_Call {
109+ return &mockhost_Associate_Call{Call: _e.mock.On("Associate", slot, surfaceID)}
110+}
111+
112+func (_c *mockhost_Associate_Call) Run(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call {
113+ _c.Call.Run(func(args mock.Arguments) {
114+ var arg0 Slot
115+ if args[0] != nil {
116+ arg0 = args[0].(Slot)
117+ }
118+ var arg1 uint64
119+ if args[1] != nil {
120+ arg1 = args[1].(uint64)
121+ }
122+ run(
123+ arg0,
124+ arg1,
125+ )
126+ })
127+ return _c
128+}
129+
130+func (_c *mockhost_Associate_Call) Return() *mockhost_Associate_Call {
131+ _c.Call.Return()
132+ return _c
133+}
134+
135+func (_c *mockhost_Associate_Call) RunAndReturn(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call {
136+ _c.Run(run)
137+ return _c
138+}
139+
140+// Created provides a mock function for the type mockhost
141+func (_mock *mockhost) Created(slot Slot, xid uint32) ports.WindowID {
142+ ret := _mock.Called(slot, xid)
143+
144+ if len(ret) == 0 {
145+ panic("no return value specified for Created")
146+ }
147+
148+ var r0 ports.WindowID
149+ if returnFunc, ok := ret.Get(0).(func(Slot, uint32) ports.WindowID); ok {
150+ r0 = returnFunc(slot, xid)
151+ } else {
152+ r0 = ret.Get(0).(ports.WindowID)
153+ }
154+ return r0
155+}
156+
157+// mockhost_Created_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Created'
158+type mockhost_Created_Call struct {
159+ *mock.Call
160+}
161+
162+// Created is a helper method to define mock.On call
163+// - slot Slot
164+// - xid uint32
165+func (_e *mockhost_Expecter) Created(slot any, xid any) *mockhost_Created_Call {
166+ return &mockhost_Created_Call{Call: _e.mock.On("Created", slot, xid)}
167+}
168+
169+func (_c *mockhost_Created_Call) Run(run func(slot Slot, xid uint32)) *mockhost_Created_Call {
170+ _c.Call.Run(func(args mock.Arguments) {
171+ var arg0 Slot
172+ if args[0] != nil {
173+ arg0 = args[0].(Slot)
174+ }
175+ var arg1 uint32
176+ if args[1] != nil {
177+ arg1 = args[1].(uint32)
178+ }
179+ run(
180+ arg0,
181+ arg1,
182+ )
183+ })
184+ return _c
185+}
186+
187+func (_c *mockhost_Created_Call) Return(windowID ports.WindowID) *mockhost_Created_Call {
188+ _c.Call.Return(windowID)
189+ return _c
190+}
191+
192+func (_c *mockhost_Created_Call) RunAndReturn(run func(slot Slot, xid uint32) ports.WindowID) *mockhost_Created_Call {
193+ _c.Call.Return(run)
194+ return _c
195+}
196+
197+// Dissociate provides a mock function for the type mockhost
198+func (_mock *mockhost) Dissociate(slot Slot, destroyed bool) {
199+ _mock.Called(slot, destroyed)
200+ return
201+}
202+
203+// mockhost_Dissociate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Dissociate'
204+type mockhost_Dissociate_Call struct {
205+ *mock.Call
206+}
207+
208+// Dissociate is a helper method to define mock.On call
209+// - slot Slot
210+// - destroyed bool
211+func (_e *mockhost_Expecter) Dissociate(slot any, destroyed any) *mockhost_Dissociate_Call {
212+ return &mockhost_Dissociate_Call{Call: _e.mock.On("Dissociate", slot, destroyed)}
213+}
214+
215+func (_c *mockhost_Dissociate_Call) Run(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call {
216+ _c.Call.Run(func(args mock.Arguments) {
217+ var arg0 Slot
218+ if args[0] != nil {
219+ arg0 = args[0].(Slot)
220+ }
221+ var arg1 bool
222+ if args[1] != nil {
223+ arg1 = args[1].(bool)
224+ }
225+ run(
226+ arg0,
227+ arg1,
228+ )
229+ })
230+ return _c
231+}
232+
233+func (_c *mockhost_Dissociate_Call) Return() *mockhost_Dissociate_Call {
234+ _c.Call.Return()
235+ return _c
236+}
237+
238+func (_c *mockhost_Dissociate_Call) RunAndReturn(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call {
239+ _c.Run(run)
240+ return _c
241+}
242+
243+// Fullscreen provides a mock function for the type mockhost
244+func (_mock *mockhost) Fullscreen(slot Slot, on bool) {
245+ _mock.Called(slot, on)
246+ return
247+}
248+
249+// mockhost_Fullscreen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fullscreen'
250+type mockhost_Fullscreen_Call struct {
251+ *mock.Call
252+}
253+
254+// Fullscreen is a helper method to define mock.On call
255+// - slot Slot
256+// - on bool
257+func (_e *mockhost_Expecter) Fullscreen(slot any, on any) *mockhost_Fullscreen_Call {
258+ return &mockhost_Fullscreen_Call{Call: _e.mock.On("Fullscreen", slot, on)}
259+}
260+
261+func (_c *mockhost_Fullscreen_Call) Run(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call {
262+ _c.Call.Run(func(args mock.Arguments) {
263+ var arg0 Slot
264+ if args[0] != nil {
265+ arg0 = args[0].(Slot)
266+ }
267+ var arg1 bool
268+ if args[1] != nil {
269+ arg1 = args[1].(bool)
270+ }
271+ run(
272+ arg0,
273+ arg1,
274+ )
275+ })
276+ return _c
277+}
278+
279+func (_c *mockhost_Fullscreen_Call) Return() *mockhost_Fullscreen_Call {
280+ _c.Call.Return()
281+ return _c
282+}
283+
284+func (_c *mockhost_Fullscreen_Call) RunAndReturn(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call {
285+ _c.Run(run)
286+ return _c
287+}
288+
289+// OverrideGone provides a mock function for the type mockhost
290+func (_mock *mockhost) OverrideGone(slot Slot) {
291+ _mock.Called(slot)
292+ return
293+}
294+
295+// mockhost_OverrideGone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideGone'
296+type mockhost_OverrideGone_Call struct {
297+ *mock.Call
298+}
299+
300+// OverrideGone is a helper method to define mock.On call
301+// - slot Slot
302+func (_e *mockhost_Expecter) OverrideGone(slot any) *mockhost_OverrideGone_Call {
303+ return &mockhost_OverrideGone_Call{Call: _e.mock.On("OverrideGone", slot)}
304+}
305+
306+func (_c *mockhost_OverrideGone_Call) Run(run func(slot Slot)) *mockhost_OverrideGone_Call {
307+ _c.Call.Run(func(args mock.Arguments) {
308+ var arg0 Slot
309+ if args[0] != nil {
310+ arg0 = args[0].(Slot)
311+ }
312+ run(
313+ arg0,
314+ )
315+ })
316+ return _c
317+}
318+
319+func (_c *mockhost_OverrideGone_Call) Return() *mockhost_OverrideGone_Call {
320+ _c.Call.Return()
321+ return _c
322+}
323+
324+func (_c *mockhost_OverrideGone_Call) RunAndReturn(run func(slot Slot)) *mockhost_OverrideGone_Call {
325+ _c.Run(run)
326+ return _c
327+}
328+
329+// OverrideMapped provides a mock function for the type mockhost
330+func (_mock *mockhost) OverrideMapped(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool) {
331+ _mock.Called(slot, xid, surfaceID, x, y, w, h, noInput)
332+ return
333+}
334+
335+// mockhost_OverrideMapped_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideMapped'
336+type mockhost_OverrideMapped_Call struct {
337+ *mock.Call
338+}
339+
340+// OverrideMapped is a helper method to define mock.On call
341+// - slot Slot
342+// - xid uint32
343+// - surfaceID uint64
344+// - x int
345+// - y int
346+// - w int
347+// - h int
348+// - noInput bool
349+func (_e *mockhost_Expecter) OverrideMapped(slot any, xid any, surfaceID any, x any, y any, w any, h any, noInput any) *mockhost_OverrideMapped_Call {
350+ return &mockhost_OverrideMapped_Call{Call: _e.mock.On("OverrideMapped", slot, xid, surfaceID, x, y, w, h, noInput)}
351+}
352+
353+func (_c *mockhost_OverrideMapped_Call) Run(run func(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool)) *mockhost_OverrideMapped_Call {
354+ _c.Call.Run(func(args mock.Arguments) {
355+ var arg0 Slot
356+ if args[0] != nil {
357+ arg0 = args[0].(Slot)
358+ }
359+ var arg1 uint32
360+ if args[1] != nil {
361+ arg1 = args[1].(uint32)
362+ }
363+ var arg2 uint64
364+ if args[2] != nil {
365+ arg2 = args[2].(uint64)
366+ }
367+ var arg3 int
368+ if args[3] != nil {
369+ arg3 = args[3].(int)
370+ }
371+ var arg4 int
372+ if args[4] != nil {
373+ arg4 = args[4].(int)
374+ }
375+ var arg5 int
376+ if args[5] != nil {
377+ arg5 = args[5].(int)
378+ }
379+ var arg6 int
380+ if args[6] != nil {
381+ arg6 = args[6].(int)
382+ }
383+ var arg7 bool
384+ if args[7] != nil {
385+ arg7 = args[7].(bool)
386+ }
387+ run(
388+ arg0,
389+ arg1,
390+ arg2,
391+ arg3,
392+ arg4,
393+ arg5,
394+ arg6,
395+ arg7,
396+ )
397+ })
398+ return _c
399+}
400+
401+func (_c *mockhost_OverrideMapped_Call) Return() *mockhost_OverrideMapped_Call {
402+ _c.Call.Return()
403+ return _c
404+}
405+
406+func (_c *mockhost_OverrideMapped_Call) RunAndReturn(run func(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool)) *mockhost_OverrideMapped_Call {
407+ _c.Run(run)
408+ return _c
409+}
410+
411+// OverrideMoved provides a mock function for the type mockhost
412+func (_mock *mockhost) OverrideMoved(slot Slot, x int, y int, w int, h int) {
413+ _mock.Called(slot, x, y, w, h)
414+ return
415+}
416+
417+// mockhost_OverrideMoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideMoved'
418+type mockhost_OverrideMoved_Call struct {
419+ *mock.Call
420+}
421+
422+// OverrideMoved is a helper method to define mock.On call
423+// - slot Slot
424+// - x int
425+// - y int
426+// - w int
427+// - h int
428+func (_e *mockhost_Expecter) OverrideMoved(slot any, x any, y any, w any, h any) *mockhost_OverrideMoved_Call {
429+ return &mockhost_OverrideMoved_Call{Call: _e.mock.On("OverrideMoved", slot, x, y, w, h)}
430+}
431+
432+func (_c *mockhost_OverrideMoved_Call) Run(run func(slot Slot, x int, y int, w int, h int)) *mockhost_OverrideMoved_Call {
433+ _c.Call.Run(func(args mock.Arguments) {
434+ var arg0 Slot
435+ if args[0] != nil {
436+ arg0 = args[0].(Slot)
437+ }
438+ var arg1 int
439+ if args[1] != nil {
440+ arg1 = args[1].(int)
441+ }
442+ var arg2 int
443+ if args[2] != nil {
444+ arg2 = args[2].(int)
445+ }
446+ var arg3 int
447+ if args[3] != nil {
448+ arg3 = args[3].(int)
449+ }
450+ var arg4 int
451+ if args[4] != nil {
452+ arg4 = args[4].(int)
453+ }
454+ run(
455+ arg0,
456+ arg1,
457+ arg2,
458+ arg3,
459+ arg4,
460+ )
461+ })
462+ return _c
463+}
464+
465+func (_c *mockhost_OverrideMoved_Call) Return() *mockhost_OverrideMoved_Call {
466+ _c.Call.Return()
467+ return _c
468+}
469+
470+func (_c *mockhost_OverrideMoved_Call) RunAndReturn(run func(slot Slot, x int, y int, w int, h int)) *mockhost_OverrideMoved_Call {
471+ _c.Run(run)
472+ return _c
473+}
474+
475+// Parent provides a mock function for the type mockhost
476+func (_mock *mockhost) Parent(slot Slot, parent Slot) {
477+ _mock.Called(slot, parent)
478+ return
479+}
480+
481+// mockhost_Parent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Parent'
482+type mockhost_Parent_Call struct {
483+ *mock.Call
484+}
485+
486+// Parent is a helper method to define mock.On call
487+// - slot Slot
488+// - parent Slot
489+func (_e *mockhost_Expecter) Parent(slot any, parent any) *mockhost_Parent_Call {
490+ return &mockhost_Parent_Call{Call: _e.mock.On("Parent", slot, parent)}
491+}
492+
493+func (_c *mockhost_Parent_Call) Run(run func(slot Slot, parent Slot)) *mockhost_Parent_Call {
494+ _c.Call.Run(func(args mock.Arguments) {
495+ var arg0 Slot
496+ if args[0] != nil {
497+ arg0 = args[0].(Slot)
498+ }
499+ var arg1 Slot
500+ if args[1] != nil {
501+ arg1 = args[1].(Slot)
502+ }
503+ run(
504+ arg0,
505+ arg1,
506+ )
507+ })
508+ return _c
509+}
510+
511+func (_c *mockhost_Parent_Call) Return() *mockhost_Parent_Call {
512+ _c.Call.Return()
513+ return _c
514+}
515+
516+func (_c *mockhost_Parent_Call) RunAndReturn(run func(slot Slot, parent Slot)) *mockhost_Parent_Call {
517+ _c.Run(run)
518+ return _c
519+}
520+
521+// Ready provides a mock function for the type mockhost
522+func (_mock *mockhost) Ready(slot Slot, info WindowInfo) {
523+ _mock.Called(slot, info)
524+ return
525+}
526+
527+// mockhost_Ready_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ready'
528+type mockhost_Ready_Call struct {
529+ *mock.Call
530+}
531+
532+// Ready is a helper method to define mock.On call
533+// - slot Slot
534+// - info WindowInfo
535+func (_e *mockhost_Expecter) Ready(slot any, info any) *mockhost_Ready_Call {
536+ return &mockhost_Ready_Call{Call: _e.mock.On("Ready", slot, info)}
537+}
538+
539+func (_c *mockhost_Ready_Call) Run(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call {
540+ _c.Call.Run(func(args mock.Arguments) {
541+ var arg0 Slot
542+ if args[0] != nil {
543+ arg0 = args[0].(Slot)
544+ }
545+ var arg1 WindowInfo
546+ if args[1] != nil {
547+ arg1 = args[1].(WindowInfo)
548+ }
549+ run(
550+ arg0,
551+ arg1,
552+ )
553+ })
554+ return _c
555+}
556+
557+func (_c *mockhost_Ready_Call) Return() *mockhost_Ready_Call {
558+ _c.Call.Return()
559+ return _c
560+}
561+
562+func (_c *mockhost_Ready_Call) RunAndReturn(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call {
563+ _c.Run(run)
564+ return _c
565+}
566+
567+// Title provides a mock function for the type mockhost
568+func (_mock *mockhost) Title(slot Slot, title string) {
569+ _mock.Called(slot, title)
570+ return
571+}
572+
573+// mockhost_Title_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Title'
574+type mockhost_Title_Call struct {
575+ *mock.Call
576+}
577+
578+// Title is a helper method to define mock.On call
579+// - slot Slot
580+// - title string
581+func (_e *mockhost_Expecter) Title(slot any, title any) *mockhost_Title_Call {
582+ return &mockhost_Title_Call{Call: _e.mock.On("Title", slot, title)}
583+}
584+
585+func (_c *mockhost_Title_Call) Run(run func(slot Slot, title string)) *mockhost_Title_Call {
586+ _c.Call.Run(func(args mock.Arguments) {
587+ var arg0 Slot
588+ if args[0] != nil {
589+ arg0 = args[0].(Slot)
590+ }
591+ var arg1 string
592+ if args[1] != nil {
593+ arg1 = args[1].(string)
594+ }
595+ run(
596+ arg0,
597+ arg1,
598+ )
599+ })
600+ return _c
601+}
602+
603+func (_c *mockhost_Title_Call) Return() *mockhost_Title_Call {
604+ _c.Call.Return()
605+ return _c
606+}
607+
608+func (_c *mockhost_Title_Call) RunAndReturn(run func(slot Slot, title string)) *mockhost_Title_Call {
609+ _c.Run(run)
610+ return _c
611+}
A internal/adapters/wayland/xwm/mapflow.go +355 -0
@@ -0,0 +1,355 @@
1+package xwm
2+
3+import (
4+ "bnema.dev/xturbo"
5+ "bnema.dev/xturbo/xproto"
6+)
7+
8+// ICCCM WM_STATE values.
9+const (
10+ wmStateWithdrawn uint32 = 0
11+ wmStateNormal uint32 = 1
12+)
13+
14+// _NET_WM_STATE client message actions.
15+const (
16+ netStateRemove = 0
17+ netStateAdd = 1
18+ netStateToggle = 2
19+)
20+
21+// beginMap starts the map flow of a managed window: every property, the
22+// geometry and MapWindow go out in one batch (wlroots order). Also called by
23+// Attach for windows mapped before it.
24+func (x *XWM) beginMap(s Slot) {
25+ rec := x.tab.rec(s)
26+ if rec.kind != kindManaged || rec.mapping {
27+ return
28+ }
29+ rec.mapping, rec.ready = true, false
30+ rec.hasNetName = false
31+ rec.pending = propGeom
32+ xid := rec.xid
33+ for idx := 0; idx < propCount; idx++ {
34+ x.requestProp(s, idx)
35+ }
36+ b := x.conn.Buf()
37+ x.expect(xproto.GetGeometry(b, xproto.Drawable(xid)), kindGeom, x.tab.tag(s), 0)
38+ xproto.MapWindow(b, xproto.Window(xid))
39+}
40+
41+func (x *XWM) onMapRequest(ev *xturbo.Event) {
42+ if s := x.tab.lookup(uint32(xproto.MapRequestOf(ev).Window())); s != 0 {
43+ x.beginMap(s)
44+ }
45+}
46+
47+func (x *XWM) onMapNotify(ev *xturbo.Event) {
48+ s := x.tab.lookup(uint32(xproto.MapNotifyOf(ev).Window()))
49+ if s == 0 {
50+ return
51+ }
52+ rec := x.tab.rec(s)
53+ again := rec.mapped
54+ rec.mapped = true
55+ if rec.kind == kindUnmanaged && !again {
56+ x.requestOverrideType(s)
57+ }
58+}
59+
60+// onUnmapNotify ends the mapped life of a managed window; a later MapRequest
61+// starts over and re-reads the properties. Synthetic events (ICCCM unmap
62+// requests) are not an unmap.
63+func (x *XWM) onUnmapNotify(ev *xturbo.Event) {
64+ if ev.SendEvent {
65+ return
66+ }
67+ s := x.tab.lookup(uint32(xproto.UnmapNotifyOf(ev).Window()))
68+ if s == 0 {
69+ return
70+ }
71+ rec := x.tab.rec(s)
72+ rec.mapped = false
73+ if rec.kind == kindUnmanaged {
74+ x.overrideUnmapped(s, rec)
75+ return
76+ }
77+ if rec.kind != kindManaged {
78+ return
79+ }
80+ wasMapping := rec.mapping
81+ rec.ready, rec.mapping, rec.pending, rec.sized = false, false, 0, false
82+ rec.gen++ // replies still in flight for this map name a dead tag
83+ if rec.gen == 0 {
84+ rec.gen = 1
85+ }
86+ rec.pend.dirty = false
87+ rec.serial, rec.surface = 0, 0
88+ if wasMapping {
89+ xid := rec.xid
90+ x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateWithdrawn, 0})
91+ xproto.DeleteProperty(x.conn.Buf(), xproto.Window(xid), xproto.Atom(x.atoms[atomNetWMState]))
92+ }
93+ x.dropWindow(s)
94+ if rec.id != 0 {
95+ x.host.Dissociate(s, false)
96+ }
97+}
98+
99+// setProp32Typed is setProp32 with an explicit property type atom.
100+func (x *XWM) setProp32Typed(win uint32, prop int, typ uint32, vals []uint32) {
101+ x.setProp32(win, prop, xproto.Atom(typ), vals)
102+}
103+
104+// checkReady fires Ready once every awaited reply is in.
105+func (x *XWM) checkReady(s Slot) {
106+ rec := x.tab.rec(s)
107+ if rec == nil || rec.pending != 0 || rec.ready || !rec.mapping || rec.kind != kindManaged || x.host == nil {
108+ return
109+ }
110+ rec.ready = true
111+ x.classify(rec)
112+ info := x.info(s)
113+ xid := rec.xid
114+
115+ var state [2]uint32
116+ n := 0
117+ if rec.modal {
118+ state[n] = x.atoms[atomNetWMStateModal]
119+ n++
120+ }
121+ if rec.fullscreen {
122+ state[n] = x.atoms[atomNetWMStateFullscreen]
123+ n++
124+ }
125+ x.setProp32(xid, atomNetWMState, xproto.AtomAtom, state[:n])
126+ x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateNormal, 0})
127+ x.addClient(xid)
128+
129+ x.host.Ready(s, info)
130+ x.tryPair(s)
131+}
132+
133+// info is what the host learns when a window is ready.
134+func (x *XWM) info(s Slot) WindowInfo {
135+ rec := x.tab.rec(s)
136+ parent := x.managedSlot(rec.transient)
137+ info := WindowInfo{
138+ PID: int(rec.pid),
139+ Floating: rec.floating,
140+ Parent: parent,
141+ Modal: rec.modal,
142+ Fullscreen: rec.fullscreen,
143+ Title: rec.title,
144+ AppID: rec.class,
145+ }
146+ if info.Floating || rec.sized {
147+ info.Width, info.Height = int(rec.w), int(rec.h)
148+ }
149+ return info
150+}
151+
152+func (x *XWM) onGeomReply(s Slot, rec *record, r *xturbo.Reply) {
153+ if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok {
154+ rec.x, rec.y, rec.w, rec.h = int32(g.X()), int32(g.Y()), uint32(g.Width()), uint32(g.Height())
155+ }
156+ rec.pending &^= propGeom
157+ x.checkReady(s)
158+}
159+
160+// onClientMessage handles WL_SURFACE_SERIAL (pairing), the legacy
161+// WL_SURFACE_ID (counted, ignored) and _NET_WM_STATE requests.
162+func (x *XWM) onClientMessage(ev *xturbo.Event) {
163+ e := xproto.ClientMessageOf(ev)
164+ if e.Format() != 32 {
165+ return
166+ }
167+ typ := uint32(e.Type())
168+ switch typ {
169+ case x.atoms[atomWLSurfaceSerial]:
170+ s := x.tab.lookup(uint32(e.Window()))
171+ if s == 0 {
172+ return
173+ }
174+ rec := x.tab.rec(s)
175+ if rec.serial != 0 {
176+ x.log.Debug().Uint32("xid", rec.xid).Msg("duplicate WL_SURFACE_SERIAL ignored")
177+ return
178+ }
179+ d := e.Data().Data32()
180+ rec.serial = uint64(d[1])<<32 | uint64(d[0])
181+ x.tryPair(s)
182+ case x.atoms[atomWLSurfaceID]:
183+ x.stats.LegacyID++
184+ if x.stats.LegacyID == 1 {
185+ x.log.Warn().Msg("WL_SURFACE_ID received: Xwayland did not bind xwayland_shell_v1, window ignored")
186+ }
187+ case x.atoms[atomNetWMState]:
188+ x.onNetWMState(e)
189+ case x.atoms[atomWMChangeState]:
190+ // Iconify requests are ignored: windows stay mapped.
191+ }
192+}
193+
194+// onNetWMState forwards fullscreen requests to the host and records them; the
195+// property itself is written when core answers with a configure.
196+func (x *XWM) onNetWMState(e xproto.ClientMessageEvent) {
197+ s := x.tab.lookup(uint32(e.Window()))
198+ if s == 0 {
199+ return
200+ }
201+ rec := x.tab.rec(s)
202+ if rec.kind != kindManaged {
203+ return
204+ }
205+ d := e.Data().Data32()
206+ apply := func(cur bool) bool {
207+ switch d[0] {
208+ case netStateRemove:
209+ return false
210+ case netStateAdd:
211+ return true
212+ case netStateToggle:
213+ return !cur
214+ }
215+ return cur
216+ }
217+ for _, a := range d[1:3] {
218+ switch {
219+ case a == 0:
220+ case a == x.atoms[atomNetWMStateModal]:
221+ rec.modal = apply(rec.modal)
222+ case a == x.atoms[atomNetWMStateFullscreen]:
223+ on := apply(rec.fullscreen)
224+ if on == rec.fullscreen {
225+ continue
226+ }
227+ rec.fullscreen = on
228+ if rec.ready {
229+ x.host.Fullscreen(s, on)
230+ rec = x.tab.rec(s)
231+ }
232+ }
233+ }
234+}
235+
236+// onReparentNotify keeps the table to root's direct children: a window moved
237+// away is forgotten, one moved into root is treated as newly created.
238+func (x *XWM) onReparentNotify(ev *xturbo.Event) {
239+ e := xproto.ReparentNotifyOf(ev)
240+ xid := uint32(e.Window())
241+ if xid == x.wmWin {
242+ return
243+ }
244+ s := x.tab.lookup(xid)
245+ if uint32(e.Parent()) != x.root {
246+ if s != 0 {
247+ x.forgetWindow(s)
248+ }
249+ return
250+ }
251+ if s != 0 {
252+ return
253+ }
254+ s = x.insert(xid)
255+ if s == 0 {
256+ return
257+ }
258+ rec := x.tab.rec(s)
259+ rec.x, rec.y = int32(e.X()), int32(e.Y())
260+ if e.OverrideRedirect() {
261+ rec.kind = kindUnmanaged
262+ x.stats.UnmanagedSeen++
263+ return
264+ }
265+ if x.host != nil {
266+ x.adopt(s)
267+ }
268+}
269+
270+// tryPair links the window to the wl_surface carrying its serial, once both
271+// are known and the window is ready (the host maps on the surface's first
272+// buffer, so it must know the window's info by then). Unmanaged windows keep
273+// the surface but are never associated.
274+func (x *XWM) tryPair(s Slot) {
275+ rec := x.tab.rec(s)
276+ if rec.serial == 0 || rec.surface != 0 {
277+ return
278+ }
279+ if rec.kind == kindManaged && !rec.ready {
280+ return
281+ }
282+ for i := 0; i < x.unpairedN; i++ {
283+ if x.unpaired[i].serial != rec.serial {
284+ continue
285+ }
286+ rec.surface = x.unpaired[i].surface
287+ x.unpairedN--
288+ x.unpaired[i] = x.unpaired[x.unpairedN]
289+ if rec.kind == kindManaged {
290+ x.log.Debug().Uint32("xid", rec.xid).Uint64("surface", rec.surface).Msg("x11 window paired")
291+ x.host.Associate(s, rec.surface)
292+ } else {
293+ x.overrideMapped(s, rec)
294+ }
295+ return
296+ }
297+}
298+
299+// SetSerial records the serial of a wl_surface (xwayland_surface_v1.set_serial)
300+// and pairs it with the window announcing the same serial.
301+func (x *XWM) SetSerial(surfaceID uint64, serial uint64) {
302+ if serial == 0 || x.closed {
303+ return
304+ }
305+ for i := 0; i < x.unpairedN; i++ {
306+ if x.unpaired[i].serial == serial {
307+ x.unpaired[i].surface = surfaceID
308+ x.pairSerial(serial)
309+ return
310+ }
311+ }
312+ if x.unpairedN == unpairedCap {
313+ copy(x.unpaired[:], x.unpaired[1:])
314+ x.unpairedN--
315+ if !x.warnedUnpaired {
316+ x.warnedUnpaired = true
317+ x.log.Warn().Msg("too many unpaired surfaces, dropping the oldest")
318+ }
319+ }
320+ x.unpaired[x.unpairedN] = struct{ serial, surface uint64 }{serial, surfaceID}
321+ x.unpairedN++
322+ x.pairSerial(serial)
323+}
324+
325+// pairSerial offers a newly known surface serial to the windows.
326+func (x *XWM) pairSerial(serial uint64) {
327+ for i := range x.tab.recs {
328+ if r := &x.tab.recs[i]; r.kind != kindFree && r.serial == serial && r.surface == 0 {
329+ x.tryPair(Slot(i + 1))
330+ break
331+ }
332+ }
333+ x.wake()
334+}
335+
336+// SurfaceGone forgets a destroyed wl_surface.
337+func (x *XWM) SurfaceGone(surfaceID uint64) {
338+ for i := 0; i < x.unpairedN; i++ {
339+ if x.unpaired[i].surface == surfaceID {
340+ x.unpairedN--
341+ x.unpaired[i] = x.unpaired[x.unpairedN]
342+ break
343+ }
344+ }
345+ for i := range x.tab.recs {
346+ if r := &x.tab.recs[i]; r.kind != kindFree && r.surface == surfaceID {
347+ r.surface = 0
348+ if r.kind == kindUnmanaged && r.ovPaired {
349+ r.ovPaired = false
350+ x.host.OverrideGone(Slot(i + 1))
351+ }
352+ }
353+ }
354+ x.wake()
355+}
A internal/adapters/wayland/xwm/mapflow_test.go +779 -0
@@ -0,0 +1,779 @@
1+package xwm
2+
3+import (
4+ "context"
5+ "encoding/binary"
6+ "testing"
7+
8+ "bnema.dev/xturbo"
9+ "bnema.dev/xturbo/xproto"
10+ "github.com/bnema/neferwl/internal/ports"
11+ "github.com/stretchr/testify/mock"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+// harness bundles an attached XWM, its peer and the host mock.
16+type harness struct {
17+ t *testing.T
18+ x *XWM
19+ p *xpeer
20+ h *mockhost
21+
22+ ready map[uint32]WindowInfo // by xid, filled by Ready
23+ next ports.WindowID
24+}
25+
26+func newHarness(t *testing.T, mutate func(p *xpeer)) *harness {
27+ t.Helper()
28+ x, p := bootPeer(t, mutate)
29+ hs := &harness{t: t, x: x, p: p, h: newMockhost(t), ready: map[uint32]WindowInfo{}}
30+ src := newMockfdSource(t)
31+ src.EXPECT().Remove().Maybe()
32+ src.EXPECT().SetWritable(mock.Anything).Return(nil).Maybe()
33+ post, _ := newPost()
34+ x.attach(src, hs.h, post)
35+ return hs
36+}
37+
38+func (hs *harness) expectCreated(win uint32) {
39+ hs.next++
40+ hs.h.EXPECT().Created(mock.Anything, win).Return(hs.next).Once()
41+}
42+
43+func (hs *harness) expectReady(win uint32) {
44+ hs.h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, info WindowInfo) {
45+ hs.ready[win] = info
46+ }).Once()
47+}
48+
49+func (hs *harness) create(win uint32) Slot {
50+ hs.t.Helper()
51+ hs.expectCreated(win)
52+ hs.p.createNotify(win, false)
53+ readUntil(hs.t, hs.x, func() bool { return hs.x.tab.lookup(win) != 0 })
54+ return hs.x.tab.lookup(win)
55+}
56+
57+// mapAndWait creates and maps win and waits for Ready.
58+func (hs *harness) mapAndWait(win uint32) Slot {
59+ hs.t.Helper()
60+ s := hs.create(win)
61+ hs.expectReady(win)
62+ hs.p.mapRequest(win)
63+ readUntil(hs.t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
64+ return s
65+}
66+
67+func (hs *harness) str(win uint32, atom string, typ uint32, s string) {
68+ hs.p.setProp(win, hs.p.atom(atom), typ, 8, []byte(s))
69+}
70+
71+func (hs *harness) words(win uint32, atom string, typ uint32, w ...uint32) {
72+ hs.p.setProp(win, hs.p.atom(atom), typ, 32, le32(w...))
73+}
74+
75+func (hs *harness) atoms(win uint32, atom string, names ...string) {
76+ var w []uint32
77+ for _, n := range names {
78+ w = append(w, hs.p.atom(n))
79+ }
80+ hs.words(win, atom, uint32(xproto.AtomAtom), w...)
81+}
82+
83+func (hs *harness) classic(win uint32) {
84+ hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("inst\x00Class\x00"))
85+ hs.str(win, "_NET_WM_NAME", hs.p.atom("UTF8_STRING"), "net title")
86+ hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("old title"))
87+ hs.words(win, "_NET_WM_PID", uint32(xproto.AtomCardinal), 4242)
88+ hs.p.set(func(p *xpeer) { p.geoms[win] = xpeerGeom{w: 300, h: 200} })
89+}
90+
91+func (hs *harness) count(match func(r xpeerReq) bool) int {
92+ n := 0
93+ for _, r := range hs.p.requests() {
94+ if match(r) {
95+ n++
96+ }
97+ }
98+ return n
99+}
100+
101+func (hs *harness) waitMap(win uint32) {
102+ hs.t.Helper()
103+ readUntil(hs.t, hs.x, func() bool {
104+ return hs.count(func(r xpeerReq) bool { return r.major == opMapWindow && r.u32(4) == win }) > 0
105+ })
106+}
107+
108+func TestMapFlowBatchesRequestsAndReadyFiresOnce(t *testing.T) {
109+ hs := newHarness(t, nil)
110+ const win = 0x500001
111+ hs.classic(win)
112+ hs.create(win)
113+ base := len(hs.p.requests())
114+
115+ hs.p.holdReplies()
116+ hs.p.mapRequest(win)
117+ hs.expectReady(win)
118+ hs.waitMap(win)
119+
120+ reqs := hs.p.requests()[base:]
121+ want := []uint32{
122+ uint32(xproto.AtomWmClass), uint32(xproto.AtomWmName), hs.p.atom("_NET_WM_NAME"), uint32(xproto.AtomWmHints),
123+ uint32(xproto.AtomWmNormalHints), hs.p.atom("WM_PROTOCOLS"), uint32(xproto.AtomWmTransientFor),
124+ hs.p.atom("_NET_WM_WINDOW_TYPE"), hs.p.atom("_NET_WM_STATE"), hs.p.atom("_NET_WM_PID"),
125+ }
126+ require.GreaterOrEqual(t, len(reqs), len(want)+2)
127+ for i, a := range want {
128+ require.EqualValues(t, opGetProperty, reqs[i].major, i)
129+ require.Equal(t, uint32(win), reqs[i].u32(4))
130+ require.Equal(t, a, reqs[i].u32(8), i)
131+ require.EqualValues(t, 1024, reqs[i].u32(20), "long length: one 4 KiB read")
132+ }
133+ require.EqualValues(t, opGetGeometry, reqs[len(want)].major)
134+ require.EqualValues(t, opMapWindow, reqs[len(want)+1].major, "MapWindow in the same batch")
135+
136+ hs.p.release() // every reply arrives after MapWindow went out
137+ readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
138+ info := hs.ready[win]
139+ require.Equal(t, WindowInfo{PID: 4242, Title: "net title", AppID: "Class"}, info, "_NET_WM_NAME wins, class is the second string")
140+
141+ state, ok := hs.p.prop(win, "WM_STATE")
142+ require.True(t, ok)
143+ require.Equal(t, []uint32{1, 0}, atomsOf(state))
144+ list, ok := hs.p.prop(peerRoot, "_NET_CLIENT_LIST")
145+ require.True(t, ok)
146+ require.Equal(t, []uint32{win}, atomsOf(list))
147+}
148+
149+func TestMapFlowFloatingAndModal(t *testing.T) {
150+ hint := func(minw, minh, maxw, maxh uint32) []byte {
151+ return le32(hintMinSize|hintMaxSize, 0, 0, 0, 0, minw, minh, maxw, maxh, 0, 0, 0, 0, 0, 0, 0, 0, 0)
152+ }
153+ cases := []struct {
154+ name string
155+ setup func(hs *harness, win uint32)
156+ want func(info WindowInfo)
157+ }{
158+ {"dialog type", func(hs *harness, w uint32) {
159+ hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_DIALOG")
160+ }, func(i WindowInfo) {
161+ require.True(t, i.Floating)
162+ require.Equal(t, 300, i.Width)
163+ require.Equal(t, 200, i.Height)
164+ }},
165+ {"utility type", func(hs *harness, w uint32) {
166+ hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_UTILITY")
167+ }, func(i WindowInfo) { require.True(t, i.Floating) }},
168+ {"splash type", func(hs *harness, w uint32) {
169+ hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_SPLASH")
170+ }, func(i WindowInfo) { require.True(t, i.Floating) }},
171+ {"normal type", func(hs *harness, w uint32) {
172+ hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_NORMAL")
173+ }, func(i WindowInfo) { require.False(t, i.Floating); require.Zero(t, i.Width) }},
174+ {"fixed size", func(hs *harness, w uint32) {
175+ hs.p.setProp(w, uint32(xproto.AtomWmNormalHints), uint32(xproto.AtomWmSizeHints), 32, hint(200, 100, 200, 100))
176+ }, func(i WindowInfo) { require.True(t, i.Floating) }},
177+ {"resizable", func(hs *harness, w uint32) {
178+ hs.p.setProp(w, uint32(xproto.AtomWmNormalHints), uint32(xproto.AtomWmSizeHints), 32, hint(100, 100, 800, 600))
179+ }, func(i WindowInfo) { require.False(t, i.Floating) }},
180+ {"modal", func(hs *harness, w uint32) {
181+ hs.atoms(w, "_NET_WM_STATE", "_NET_WM_STATE_MODAL")
182+ }, func(i WindowInfo) { require.True(t, i.Modal); require.False(t, i.Fullscreen) }},
183+ {"fullscreen at map", func(hs *harness, w uint32) {
184+ hs.atoms(w, "_NET_WM_STATE", "_NET_WM_STATE_FULLSCREEN")
185+ }, func(i WindowInfo) { require.True(t, i.Fullscreen) }},
186+ }
187+ for _, c := range cases {
188+ t.Run(c.name, func(t *testing.T) {
189+ hs := newHarness(t, nil)
190+ const win = 0x500001
191+ hs.classic(win)
192+ c.setup(hs, win)
193+ hs.mapAndWait(win)
194+ c.want(hs.ready[win])
195+ })
196+ }
197+}
198+
199+func TestMapFlowTransientForManagedWindowFloats(t *testing.T) {
200+ hs := newHarness(t, nil)
201+ const parent, child = 0x500001, 0x500002
202+ hs.classic(parent)
203+ ps := hs.mapAndWait(parent)
204+ hs.classic(child)
205+ hs.p.setProp(child, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(parent))
206+ hs.mapAndWait(child)
207+ require.True(t, hs.ready[child].Floating)
208+ require.Equal(t, ps, hs.ready[child].Parent)
209+}
210+
211+func TestMapFlowTransientForUnknownWindowIsNotAParent(t *testing.T) {
212+ hs := newHarness(t, nil)
213+ const win = 0x500001
214+ hs.classic(win)
215+ hs.p.setProp(win, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(0x777777))
216+ hs.mapAndWait(win)
217+ require.Zero(t, hs.ready[win].Parent)
218+ require.False(t, hs.ready[win].Floating)
219+}
220+
221+func TestMapFlowClassFallsBackToInstance(t *testing.T) {
222+ hs := newHarness(t, nil)
223+ const win = 0x500001
224+ hs.classic(win)
225+ hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("onlyinstance\x00"))
226+ hs.mapAndWait(win)
227+ require.Equal(t, "onlyinstance", hs.ready[win].AppID)
228+}
229+
230+func TestMapFlowTitleFallsBackToWMName(t *testing.T) {
231+ hs := newHarness(t, nil)
232+ const win = 0x500001
233+ hs.classic(win)
234+ hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) })
235+ hs.mapAndWait(win)
236+ require.Equal(t, "old title", hs.ready[win].Title)
237+}
238+
239+func TestMapFlowPIDFromXRes(t *testing.T) {
240+ hs := newHarness(t, func(p *xpeer) { p.pids = map[uint32]uint32{0x500001: 777} })
241+ const win = 0x500001
242+ hs.classic(win)
243+ hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) })
244+ hs.create(win)
245+ hs.expectReady(win)
246+ hs.p.mapRequest(win)
247+ readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
248+ require.Equal(t, 777, hs.ready[win].PID, "Ready waited for the QueryClientIds reply")
249+ require.Equal(t, 1, hs.count(func(r xpeerReq) bool { return r.major == 144 && r.minor == 4 }))
250+ require.Zero(t, hs.x.tab.rec(hs.x.tab.lookup(win)).pending)
251+}
252+
253+func TestMapFlowPIDUnknownStillReady(t *testing.T) {
254+ hs := newHarness(t, nil)
255+ const win = 0x500001
256+ hs.classic(win)
257+ hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) })
258+ hs.mapAndWait(win)
259+ require.Zero(t, hs.ready[win].PID)
260+}
261+
262+func TestMapFlowNoXResAsksNothing(t *testing.T) {
263+ exts := fullPeerExts()
264+ delete(exts, "X-Resource")
265+ fd, p := startXPeer(t, exts)
266+ x, err := Boot(context.Background(), fd, testLog())
267+ require.NoError(t, err)
268+ t.Cleanup(x.CloseConn)
269+ h := newMockhost(t)
270+ src := newMockfdSource(t)
271+ src.EXPECT().Remove().Maybe()
272+ post, _ := newPost()
273+ x.attach(src, h, post)
274+ hs := &harness{t: t, x: x, p: p, h: h, ready: map[uint32]WindowInfo{}}
275+ const win = 0x500001
276+ hs.classic(win)
277+ p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) })
278+ hs.mapAndWait(win)
279+ require.Zero(t, hs.count(func(r xpeerReq) bool { return r.major == 144 }))
280+}
281+
282+func TestMapFlowBadWindowOnPropertiesNeverReady(t *testing.T) {
283+ hs := newHarness(t, nil)
284+ const win = 0x500001
285+ hs.classic(win)
286+ hs.p.set(func(p *xpeer) { p.bad[win] = true })
287+ s := hs.create(win)
288+ hs.p.mapRequest(win)
289+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Errors >= uint64(propCount)+1 })
290+ require.Zero(t, hs.x.tab.rec(s).pending, "failed replies are no longer awaited")
291+ require.False(t, hs.x.tab.rec(s).ready)
292+ require.Zero(t, hs.x.Stats().Unattributed)
293+
294+ hs.h.EXPECT().Dissociate(s, true).Once()
295+ hs.p.destroyNotify(win)
296+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 })
297+}
298+
299+func TestMapRequestTwiceBeforeReadyAsksOnce(t *testing.T) {
300+ hs := newHarness(t, nil)
301+ const win = 0x500001
302+ hs.classic(win)
303+ hs.create(win)
304+ hs.p.holdReplies()
305+ hs.p.mapRequest(win)
306+ hs.p.mapRequest(win)
307+ hs.expectReady(win)
308+ hs.waitMap(win)
309+ hs.p.release()
310+ readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
311+ require.Equal(t, 1, hs.count(func(r xpeerReq) bool { return r.major == opMapWindow && r.u32(4) == win }))
312+}
313+
314+func TestMapNotifyMarksMapped(t *testing.T) {
315+ hs := newHarness(t, nil)
316+ const win = 0x500001
317+ hs.classic(win)
318+ s := hs.mapAndWait(win)
319+ hs.p.mapNotify(win)
320+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapped })
321+}
322+
323+// Property changes after Ready.
324+
325+func (hs *harness) notify(win uint32, atom string, deleted bool) {
326+ hs.t.Helper()
327+ before := hs.x.Stats().Replies
328+ hs.p.propertyNotify(win, hs.p.atom(atom), deleted)
329+ if deleted {
330+ hs.p.propertyNotify(win, hs.p.atom("WM_PROTOCOLS"), false) // sentinel: one reply follows
331+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before })
332+ return
333+ }
334+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before })
335+}
336+
337+func TestPropertyChangedTitleReachesHostOnce(t *testing.T) {
338+ hs := newHarness(t, nil)
339+ const win = 0x500001
340+ hs.classic(win)
341+ s := hs.mapAndWait(win)
342+
343+ hs.h.EXPECT().Title(s, "renamed").Once()
344+ hs.str(win, "_NET_WM_NAME", hs.p.atom("UTF8_STRING"), "renamed")
345+ hs.notify(win, "_NET_WM_NAME", false)
346+ require.Equal(t, "renamed", hs.x.tab.rec(s).title)
347+
348+ // Same value again: no host call (the mock fails on an unexpected one).
349+ hs.notify(win, "_NET_WM_NAME", false)
350+
351+ // WM_NAME is ignored while _NET_WM_NAME is set.
352+ hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("legacy"))
353+ before := hs.x.Stats().Events
354+ hs.p.propertyNotify(win, uint32(xproto.AtomWmName), false)
355+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Events > before })
356+ require.Equal(t, "renamed", hs.x.tab.rec(s).title)
357+}
358+
359+func TestPropertyNetNameDeletedFallsBackToWMName(t *testing.T) {
360+ hs := newHarness(t, nil)
361+ const win = 0x500001
362+ hs.classic(win)
363+ s := hs.mapAndWait(win)
364+ hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) })
365+ hs.h.EXPECT().Title(s, "old title").Once()
366+ hs.p.propertyNotify(win, hs.p.atom("_NET_WM_NAME"), true)
367+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).title == "old title" })
368+}
369+
370+func TestPropertyClassChangeReachesHost(t *testing.T) {
371+ hs := newHarness(t, nil)
372+ const win = 0x500001
373+ hs.classic(win)
374+ s := hs.mapAndWait(win)
375+ hs.h.EXPECT().AppID(s, "Other").Once()
376+ hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("i\x00Other\x00"))
377+ hs.p.propertyNotify(win, uint32(xproto.AtomWmClass), false)
378+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).class == "Other" })
379+}
380+
381+func TestPropertyTransientChangeReachesHost(t *testing.T) {
382+ hs := newHarness(t, nil)
383+ const a, b = 0x500001, 0x500002
384+ hs.classic(a)
385+ as := hs.mapAndWait(a)
386+ hs.classic(b)
387+ bs := hs.mapAndWait(b)
388+ hs.h.EXPECT().Parent(bs, as).Once()
389+ hs.p.setProp(b, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(a))
390+ hs.p.propertyNotify(b, uint32(xproto.AtomWmTransientFor), false)
391+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(bs).transient == a })
392+}
393+
394+func TestPropertyInputProtocolsAndHints(t *testing.T) {
395+ hs := newHarness(t, nil)
396+ const win = 0x500001
397+ hs.classic(win)
398+ hs.atoms(win, "WM_PROTOCOLS", "WM_DELETE_WINDOW", "WM_TAKE_FOCUS")
399+ hs.p.setProp(win, uint32(xproto.AtomWmHints), uint32(xproto.AtomWmHints), 32, le32(hintInput, 0, 0, 0, 0, 0, 0, 0, 0))
400+ s := hs.mapAndWait(win)
401+ r := hs.x.tab.rec(s)
402+ require.True(t, r.deleteWindow)
403+ require.True(t, r.takeFocus)
404+ require.False(t, r.input, "WM_HINTS input = false")
405+
406+ // No WM_HINTS: input defaults to true.
407+ const other = 0x500002
408+ hs.classic(other)
409+ o := hs.mapAndWait(other)
410+ require.True(t, hs.x.tab.rec(o).input)
411+ require.False(t, hs.x.tab.rec(o).deleteWindow)
412+}
413+
414+func TestPropertyUnwatchedAtomAsksNothing(t *testing.T) {
415+ hs := newHarness(t, nil)
416+ const win = 0x500001
417+ hs.classic(win)
418+ hs.mapAndWait(win)
419+ n := len(hs.p.requests())
420+ hs.p.propertyNotify(win, hs.p.atom("_MOTIF_WM_HINTS"), false)
421+ hs.p.propertyNotify(0x999999, hs.p.atom("_NET_WM_NAME"), false) // unknown window
422+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 })
423+ require.Len(t, hs.p.requests(), n)
424+}
425+
426+// Pairing.
427+
428+func (hs *harness) serialMsg(win uint32, serial uint64) {
429+ hs.p.clientMessage(win, hs.p.atom("WL_SURFACE_SERIAL"), uint32(serial), uint32(serial>>32))
430+ hs.t.Helper()
431+ before := hs.x.Stats().Events
432+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Events > before })
433+}
434+
435+func TestPairingSerialBeforeSetSerial(t *testing.T) {
436+ hs := newHarness(t, nil)
437+ const win = 0x500001
438+ hs.classic(win)
439+ s := hs.mapAndWait(win)
440+ hs.serialMsg(win, 0x1_0000_0002)
441+ require.EqualValues(t, 0x1_0000_0002, hs.x.tab.rec(s).serial, "high word in data32[1]")
442+ hs.h.EXPECT().Associate(s, uint64(99)).Once()
443+ hs.x.SetSerial(99, 0x1_0000_0002)
444+ require.EqualValues(t, 99, hs.x.tab.rec(s).surface)
445+ require.Zero(t, hs.x.unpairedN)
446+}
447+
448+func TestPairingSetSerialBeforeSerial(t *testing.T) {
449+ hs := newHarness(t, nil)
450+ const win = 0x500001
451+ hs.classic(win)
452+ s := hs.mapAndWait(win)
453+ hs.x.SetSerial(99, 7)
454+ require.Equal(t, 1, hs.x.unpairedN)
455+ hs.h.EXPECT().Associate(s, uint64(99)).Once()
456+ hs.serialMsg(win, 7)
457+ require.Zero(t, hs.x.unpairedN)
458+}
459+
460+func TestPairingWaitsForReady(t *testing.T) {
461+ hs := newHarness(t, nil)
462+ const win = 0x500001
463+ hs.classic(win)
464+ s := hs.create(win)
465+ hs.x.SetSerial(5, 3)
466+ hs.serialMsg(win, 3) // not mapped yet: no Associate (the mock fails on one)
467+ require.Zero(t, hs.x.tab.rec(s).surface)
468+
469+ hs.h.EXPECT().Associate(s, uint64(5)).Once()
470+ hs.expectReady(win)
471+ hs.p.mapRequest(win)
472+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).surface != 0 })
473+}
474+
475+func TestPairingSerialForUnknownWindowIgnored(t *testing.T) {
476+ hs := newHarness(t, nil)
477+ hs.x.SetSerial(5, 3)
478+ hs.serialMsg(0x888888, 3)
479+ require.Equal(t, 1, hs.x.unpairedN, "stays unpaired")
480+}
481+
482+func TestPairingSurfaceGoneBeforePair(t *testing.T) {
483+ hs := newHarness(t, nil)
484+ const win = 0x500001
485+ hs.classic(win)
486+ s := hs.mapAndWait(win)
487+ hs.x.SetSerial(5, 3)
488+ hs.x.SurfaceGone(5)
489+ require.Zero(t, hs.x.unpairedN)
490+ hs.serialMsg(win, 3) // the surface is gone: nothing to pair with
491+ require.Zero(t, hs.x.tab.rec(s).surface)
492+}
493+
494+func TestPairingSurfaceGoneAfterPairClearsLink(t *testing.T) {
495+ hs := newHarness(t, nil)
496+ const win = 0x500001
497+ hs.classic(win)
498+ s := hs.mapAndWait(win)
499+ hs.serialMsg(win, 3)
500+ hs.h.EXPECT().Associate(s, uint64(5)).Once()
501+ hs.x.SetSerial(5, 3)
502+ hs.x.SurfaceGone(5)
503+ require.Zero(t, hs.x.tab.rec(s).surface)
504+}
505+
506+func TestPairingDuplicateSerialIgnored(t *testing.T) {
507+ hs := newHarness(t, nil)
508+ const win = 0x500001
509+ hs.classic(win)
510+ s := hs.mapAndWait(win)
511+ hs.serialMsg(win, 3)
512+ hs.serialMsg(win, 4)
513+ require.EqualValues(t, 3, hs.x.tab.rec(s).serial)
514+ hs.x.SetSerial(8, 4) // nobody announces 4: stays unpaired
515+ require.Equal(t, 1, hs.x.unpairedN)
516+}
517+
518+func TestPairingUnmanagedKeepsSurfaceWithoutAssociate(t *testing.T) {
519+ hs := newHarness(t, nil)
520+ hs.p.createNotify(0x500009, true)
521+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 })
522+ hs.serialMsg(0x500009, 3)
523+ hs.x.SetSerial(5, 3) // the mock fails on an Associate
524+ require.EqualValues(t, 5, hs.x.tab.rec(hs.x.tab.lookup(0x500009)).surface)
525+}
526+
527+func TestPairingTableOverflowDropsOldest(t *testing.T) {
528+ hs := newHarness(t, nil)
529+ for i := uint64(1); i <= unpairedCap+1; i++ {
530+ hs.x.SetSerial(100+i, i)
531+ }
532+ require.Equal(t, unpairedCap, hs.x.unpairedN)
533+ for i := 0; i < hs.x.unpairedN; i++ {
534+ require.NotEqual(t, uint64(1), hs.x.unpaired[i].serial, "oldest dropped")
535+ }
536+}
537+
538+func TestPairingZeroSerialIgnored(t *testing.T) {
539+ hs := newHarness(t, nil)
540+ hs.x.SetSerial(5, 0)
541+ require.Zero(t, hs.x.unpairedN)
542+}
543+
544+func TestLegacySurfaceIDCountedAndIgnored(t *testing.T) {
545+ hs := newHarness(t, nil)
546+ const win = 0x500001
547+ hs.classic(win)
548+ s := hs.mapAndWait(win)
549+ hs.p.clientMessage(win, hs.p.atom("WL_SURFACE_ID"), 12)
550+ readUntil(t, hs.x, func() bool { return hs.x.Stats().LegacyID == 1 })
551+ require.Zero(t, hs.x.tab.rec(s).serial)
552+}
553+
554+func TestNetWMStateClientMessageForwardsFullscreen(t *testing.T) {
555+ hs := newHarness(t, nil)
556+ const win = 0x500001
557+ hs.classic(win)
558+ s := hs.mapAndWait(win)
559+ hs.h.EXPECT().Fullscreen(s, true).Once()
560+ hs.p.clientMessage(win, hs.p.atom("_NET_WM_STATE"), 1, hs.p.atom("_NET_WM_STATE_FULLSCREEN"))
561+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).fullscreen })
562+ hs.h.EXPECT().Fullscreen(s, false).Once() // toggle
563+ hs.p.clientMessage(win, hs.p.atom("_NET_WM_STATE"), 2, hs.p.atom("_NET_WM_STATE_FULLSCREEN"))
564+ readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).fullscreen })
565+ // Not written back by the message itself.
566+ st, ok := hs.p.prop(win, "_NET_WM_STATE")
567+ require.True(t, ok)
568+ require.Empty(t, st.data)
569+}
570+
571+// Unmap, destroy, remap.
572+
573+func TestUnmapDissociatesAndRemapRereadsProperties(t *testing.T) {
574+ hs := newHarness(t, nil)
575+ const win = 0x500001
576+ hs.classic(win)
577+ s := hs.mapAndWait(win)
578+
579+ // A synthetic UnmapNotify (ICCCM withdraw request) is not an unmap.
580+ hs.p.unmapNotify(win, true)
581+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 })
582+ require.True(t, hs.x.tab.rec(s).ready)
583+
584+ hs.h.EXPECT().Dissociate(s, false).Once()
585+ hs.p.unmapNotify(win, false)
586+ readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).ready })
587+ r := hs.x.tab.rec(s)
588+ require.False(t, r.mapping)
589+ require.Zero(t, r.serial)
590+ hs.p.waitReq(0, func(q xpeerReq) bool {
591+ return q.major == opSetInputFocus || q.major == opChangeProperty && q.u32(4) == win
592+ })
593+ readUntil(t, hs.x, func() bool {
594+ v, ok := hs.p.prop(win, "WM_STATE")
595+ return ok && len(v.data) == 8 && binary.LittleEndian.Uint32(v.data) == 0
596+ })
597+ _, ok := hs.p.prop(win, "_NET_WM_STATE")
598+ require.False(t, ok, "_NET_WM_STATE deleted")
599+ list, _ := hs.p.prop(peerRoot, "_NET_CLIENT_LIST")
600+ require.Empty(t, list.data)
601+
602+ // Remap starts over.
603+ delete(hs.ready, win)
604+ hs.expectReady(win)
605+ n := hs.count(func(q xpeerReq) bool { return q.major == opGetProperty && q.u32(4) == win })
606+ hs.p.mapRequest(win)
607+ readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
608+ require.Equal(t, n+propCount, hs.count(func(q xpeerReq) bool { return q.major == opGetProperty && q.u32(4) == win }))
609+ list, _ = hs.p.prop(peerRoot, "_NET_CLIENT_LIST")
610+ require.Equal(t, []uint32{win}, atomsOf(list))
611+}
612+
613+func TestDestroyWhileMappingNeverReady(t *testing.T) {
614+ hs := newHarness(t, nil)
615+ const win = 0x500001
616+ hs.classic(win)
617+ s := hs.create(win)
618+ hs.p.holdReplies()
619+ hs.p.mapRequest(win)
620+ hs.waitMap(win)
621+ hs.h.EXPECT().Dissociate(s, true).Once()
622+ hs.p.destroyNotify(win)
623+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 })
624+ replies := hs.x.Stats().Replies
625+ hs.p.release() // replies for a window that is gone: xturbo retired their slots
626+ readUntil(t, hs.x, func() bool { return hs.x.conn.Stats().DroppedReplies >= uint64(propCount) })
627+ require.Equal(t, replies, hs.x.Stats().Replies, "none reaches the XWM")
628+}
629+
630+func TestStaleRepliesAfterSlotReuseAreIgnored(t *testing.T) {
631+ hs := newHarness(t, nil)
632+ const old, fresh = 0x500001, 0x500002
633+ hs.classic(old)
634+ hs.classic(fresh)
635+ s := hs.create(old)
636+ hs.p.holdReplies()
637+ hs.p.mapRequest(old)
638+ hs.waitMap(old)
639+ hs.h.EXPECT().Dissociate(s, true).Once()
640+ hs.p.destroyNotify(old)
641+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(old) == 0 })
642+
643+ hs.expectCreated(fresh)
644+ hs.p.createNotify(fresh, false)
645+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(fresh) != 0 })
646+ require.Equal(t, s, hs.x.tab.lookup(fresh), "the freed slot is reused")
647+
648+ hs.p.release()
649+ readUntil(t, hs.x, func() bool { return hs.x.conn.Stats().DroppedReplies >= uint64(propCount) })
650+
651+ // A reply that does get through with the old tag (slot reused, generation
652+ // bumped) is ignored.
653+ oldTag := s | Slot(hs.x.tab.rec(s).gen-1)<<16
654+ rep := make([]byte, 32)
655+ rep[0] = 1
656+ hs.x.onReply(&xturbo.Reply{Kind: kindProp, Args: [4]uint32{uint32(oldTag), propClass}, Raw: rep})
657+ hs.x.onReply(&xturbo.Reply{Kind: kindGeom, Args: [4]uint32{uint32(oldTag)}, Raw: rep})
658+ require.EqualValues(t, 2, hs.x.Stats().Stale)
659+ require.False(t, hs.x.tab.rec(s).ready, "stale replies must not complete the new window")
660+ require.Zero(t, hs.x.tab.rec(s).pending)
661+}
662+
663+// Reparenting.
664+
665+func TestReparentAwayFromRootRemovesManagedWindow(t *testing.T) {
666+ hs := newHarness(t, nil)
667+ const win = 0x500001
668+ hs.classic(win)
669+ s := hs.mapAndWait(win)
670+ hs.h.EXPECT().Dissociate(s, true).Once()
671+ hs.p.reparentNotify(win, 0x600000)
672+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 })
673+ list, _ := hs.p.prop(peerRoot, "_NET_CLIENT_LIST")
674+ require.Empty(t, list.data)
675+ require.Zero(t, hs.x.tab.n)
676+}
677+
678+func TestReparentAwayOfUnmanagedWindowRemovesIt(t *testing.T) {
679+ hs := newHarness(t, nil)
680+ hs.p.createNotify(0x500009, true)
681+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 })
682+ hs.p.reparentNotify(0x500009, 0x600000)
683+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) == 0 })
684+}
685+
686+func TestReparentIntoRootAdoptsWindow(t *testing.T) {
687+ hs := newHarness(t, nil)
688+ const win = 0x500001
689+ hs.expectCreated(win)
690+ hs.p.reparentNotify(win, peerRoot)
691+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) != 0 })
692+ require.Equal(t, kindManaged, hs.x.tab.rec(hs.x.tab.lookup(win)).kind)
693+
694+ // Already known: no second Created.
695+ hs.p.reparentNotify(win, peerRoot)
696+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 })
697+}
698+
699+func TestReparentIntoRootOfOverrideRedirectWindowStaysUnmanaged(t *testing.T) {
700+ hs := newHarness(t, nil)
701+ hs.p.event(21, func(b *[32]byte) {
702+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
703+ binary.LittleEndian.PutUint32(b[8:], 0x500009)
704+ binary.LittleEndian.PutUint32(b[12:], peerRoot)
705+ b[20] = 1
706+ })
707+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 })
708+ require.Equal(t, kindUnmanaged, hs.x.tab.rec(hs.x.tab.lookup(0x500009)).kind)
709+}
710+
711+func TestReparentOfWMWindowIgnored(t *testing.T) {
712+ hs := newHarness(t, nil)
713+ hs.p.reparentNotify(hs.x.wmWin, peerRoot)
714+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 1 })
715+ require.Zero(t, hs.x.tab.lookup(hs.x.wmWin))
716+}
717+
718+// Window known only before Attach: Attach replays the map flow.
719+func TestAttachMapsWindowsRequestedBeforeAttach(t *testing.T) {
720+ x, p := bootPeer(t, nil)
721+ const win = 0x500001
722+ p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("a\x00B\x00"))
723+ p.createNotify(win, false)
724+ p.mapRequest(win)
725+ readUntil(t, x, func() bool { return x.Stats().Events >= 2 })
726+ h := newMockhost(t)
727+ src := newMockfdSource(t)
728+ src.EXPECT().Remove().Maybe()
729+ post, _ := newPost()
730+ var got WindowInfo
731+ done := false
732+ h.EXPECT().Created(mock.Anything, uint32(win)).Return(ports.WindowID(1)).Once()
733+ h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, i WindowInfo) { got, done = i, true }).Once()
734+ x.attach(src, h, post)
735+ readUntil(t, x, func() bool { return done })
736+ require.Equal(t, "B", got.AppID)
737+}
738+
739+// Replies complete a map in any order (a server may answer the geometry
740+// before the properties it was asked for first): Ready fires on the last one.
741+func TestMapFlowReadyFiresOnLastReplyInAnyOrder(t *testing.T) {
742+ hs := newHarness(t, nil)
743+ const win = 0x500001
744+ hs.classic(win)
745+ s := hs.create(win)
746+ r := hs.x.tab.rec(s)
747+ r.mapping, r.pending = true, propMaskAll&^propClientIDs
748+ hs.expectReady(win)
749+ order := []int{propCount, propPID, propClass, propNetName, propName, propHints, propNormalHints, propProtocols, propTransientFor, propWindowType, propState}
750+ for i, bit := range order {
751+ require.False(t, hs.x.tab.rec(s).ready, i)
752+ if bit == propCount { // the geometry
753+ hs.x.tab.rec(s).w, hs.x.tab.rec(s).h = 300, 200
754+ hs.x.tab.rec(s).pending &^= propGeom
755+ hs.x.checkReady(s)
756+ continue
757+ }
758+ var val []byte
759+ var format uint8 = 8
760+ switch bit {
761+ case propClass:
762+ val = []byte("a\x00B\x00")
763+ case propNetName:
764+ val = []byte("t")
765+ case propPID:
766+ val, format = le32(5), 32
767+ default:
768+ format = 32
769+ }
770+ x := hs.x
771+ x.applyProp(s, bit, format, val)
772+ x.tab.rec(s).pending &^= 1 << bit
773+ x.checkReady(s)
774+ }
775+ require.True(t, hs.x.tab.rec(s).ready)
776+ require.Equal(t, "B", hs.ready[win].AppID)
777+ require.Equal(t, "t", hs.ready[win].Title)
778+ require.Equal(t, 5, hs.ready[win].PID)
779+}
A internal/adapters/wayland/xwm/props.go +356 -0
@@ -0,0 +1,356 @@
1+package xwm
2+
3+import (
4+ "bytes"
5+ "encoding/binary"
6+ "strings"
7+
8+ "bnema.dev/xturbo"
9+ "bnema.dev/xturbo/res"
10+ "bnema.dev/xturbo/xproto"
11+)
12+
13+// Property indices (second Expect argument of kindProp) and their bits in
14+// record.pending. propGeom is the GetGeometry bit; propClientIDs is raised
15+// while a QueryClientIds reply stands in for a missing _NET_WM_PID.
16+const (
17+ propClass = iota
18+ propName
19+ propNetName
20+ propHints
21+ propNormalHints
22+ propProtocols
23+ propTransientFor
24+ propWindowType
25+ propState
26+ propPID
27+ propCount
28+)
29+
30+const (
31+ propGeom uint16 = 1 << propCount
32+ propClientIDs uint16 = propGeom << 1
33+ // propMaskAll is what a map waits for: every property and the geometry.
34+ propMaskAll uint16 = propGeom<<1 - 1
35+)
36+
37+// WM_NORMAL_HINTS flags and WM_HINTS flags.
38+const (
39+ hintInput = 1 << 0
40+ hintMinSize = 1 << 4
41+ hintMaxSize = 1 << 5
42+)
43+
44+// maxPropWords bounds one GetProperty (4 KiB), so titles are truncated there.
45+const maxPropWords = 1024
46+
47+// propIndex maps a property atom to its index, or -1 when it is not watched.
48+func (x *XWM) propIndex(a uint32) int {
49+ switch a {
50+ case uint32(xproto.AtomWmClass):
51+ return propClass
52+ case uint32(xproto.AtomWmName):
53+ return propName
54+ case uint32(xproto.AtomWmHints):
55+ return propHints
56+ case uint32(xproto.AtomWmNormalHints):
57+ return propNormalHints
58+ case uint32(xproto.AtomWmTransientFor):
59+ return propTransientFor
60+ case x.atoms[atomNetWMName]:
61+ return propNetName
62+ case x.atoms[atomWMProtocols]:
63+ return propProtocols
64+ case x.atoms[atomNetWMWindowType]:
65+ return propWindowType
66+ case x.atoms[atomNetWMState]:
67+ return propState
68+ case x.atoms[atomNetWMPID]:
69+ return propPID
70+ }
71+ return -1
72+}
73+
74+func (x *XWM) propAtom(idx int) xproto.Atom {
75+ switch idx {
76+ case propClass:
77+ return xproto.AtomWmClass
78+ case propName:
79+ return xproto.AtomWmName
80+ case propHints:
81+ return xproto.AtomWmHints
82+ case propNormalHints:
83+ return xproto.AtomWmNormalHints
84+ case propTransientFor:
85+ return xproto.AtomWmTransientFor
86+ case propNetName:
87+ return xproto.Atom(x.atoms[atomNetWMName])
88+ case propProtocols:
89+ return xproto.Atom(x.atoms[atomWMProtocols])
90+ case propWindowType:
91+ return xproto.Atom(x.atoms[atomNetWMWindowType])
92+ case propState:
93+ return xproto.Atom(x.atoms[atomNetWMState])
94+ default:
95+ return xproto.Atom(x.atoms[atomNetWMPID])
96+ }
97+}
98+
99+// requestProp queues a GetProperty for property idx of s.
100+func (x *XWM) requestProp(s Slot, idx int) {
101+ rec := x.tab.rec(s)
102+ seq := xproto.GetProperty(x.conn.Buf(), false, xproto.Window(rec.xid), x.propAtom(idx), xproto.AtomAny, 0, maxPropWords)
103+ rec.pending |= 1 << idx
104+ x.expect(seq, kindProp, x.tab.tag(s), uint32(idx))
105+}
106+
107+// onPropertyNotify handles a changed property of a managed window that is
108+// being mapped or is mapped; unwatched atoms cost nothing.
109+func (x *XWM) onPropertyNotify(ev *xturbo.Event) {
110+ e := xproto.PropertyNotifyOf(ev)
111+ s := x.tab.lookup(uint32(e.Window()))
112+ if s == 0 {
113+ return
114+ }
115+ if rec := x.tab.rec(s); rec.kind != kindManaged || !rec.mapping {
116+ return
117+ } else if rec.ready && x.propIndex(uint32(e.Atom())) == propState {
118+ // The XWM owns _NET_WM_STATE of a mapped window: our own writes
119+ // would feed back, and clients change it with ClientMessages.
120+ return
121+ }
122+ idx := x.propIndex(uint32(e.Atom()))
123+ if idx < 0 {
124+ return
125+ }
126+ if e.State() == xproto.PropertyDelete {
127+ x.applyProp(s, idx, 0, nil)
128+ return
129+ }
130+ x.requestProp(s, idx)
131+}
132+
133+// onPropReply decodes one GetProperty reply into the record. An absent
134+// property (format 0) clears its fields.
135+func (x *XWM) onPropReply(s Slot, _ *record, r *xturbo.Reply) {
136+ idx := int(r.Args[1])
137+ if idx < 0 || idx >= propCount {
138+ return
139+ }
140+ var format uint8
141+ var val []byte
142+ if rep, ok := xproto.ParseGetPropertyReply(r.Raw); ok {
143+ format, val = rep.Format(), rep.Value()
144+ }
145+ x.applyProp(s, idx, format, val)
146+ x.tab.rec(s).pending &^= 1 << idx
147+ x.checkReady(s)
148+}
149+
150+func word(b []byte, i int) uint32 { return binary.LittleEndian.Uint32(b[4*i:]) }
151+
152+// applyProp stores one decoded property. It does not allocate unless a string
153+// changed. Host calls come last: the record is not touched after them.
154+func (x *XWM) applyProp(s Slot, idx int, format uint8, val []byte) {
155+ rec := x.tab.rec(s)
156+ if idx == propClass || idx == propName || idx == propNetName {
157+ if format != 8 {
158+ val = nil
159+ }
160+ } else if format != 32 {
161+ val = nil
162+ }
163+ switch idx {
164+ case propClass:
165+ inst, class := val, []byte(nil)
166+ if i := bytes.IndexByte(val, 0); i >= 0 {
167+ inst, class = val[:i], val[i+1:]
168+ if j := bytes.IndexByte(class, 0); j >= 0 {
169+ class = class[:j]
170+ }
171+ }
172+ app := class
173+ if len(app) == 0 {
174+ app = inst
175+ }
176+ if rec.class == string(app) {
177+ return
178+ }
179+ cls := strings.ToValidUTF8(string(app), "\uFFFD")
180+ if rec.class == cls {
181+ return
182+ }
183+ rec.class = cls
184+ if rec.ready {
185+ x.host.AppID(s, rec.class)
186+ }
187+ case propName:
188+ if !rec.hasNetName {
189+ x.setTitle(s, val)
190+ }
191+ case propNetName:
192+ had := rec.hasNetName
193+ rec.hasNetName = len(val) > 0
194+ if rec.hasNetName {
195+ x.setTitle(s, val)
196+ } else if had && rec.mapping {
197+ x.requestProp(s, propName)
198+ }
199+ case propHints:
200+ rec.input = true
201+ if len(val) >= 8 && word(val, 0)&hintInput != 0 {
202+ rec.input = word(val, 1) != 0
203+ }
204+ case propNormalHints:
205+ rec.minW, rec.minH, rec.maxW, rec.maxH = 0, 0, 0, 0
206+ if len(val) >= 36 {
207+ fl := word(val, 0)
208+ if fl&hintMinSize != 0 {
209+ rec.minW, rec.minH = int32(word(val, 5)), int32(word(val, 6))
210+ }
211+ if fl&hintMaxSize != 0 {
212+ rec.maxW, rec.maxH = int32(word(val, 7)), int32(word(val, 8))
213+ }
214+ }
215+ x.classify(rec)
216+ case propProtocols:
217+ rec.deleteWindow, rec.takeFocus = false, false
218+ for i := 0; i+4 <= len(val); i += 4 {
219+ switch binary.LittleEndian.Uint32(val[i:]) {
220+ case x.atoms[atomWMDeleteWindow]:
221+ rec.deleteWindow = true
222+ case x.atoms[atomWMTakeFocus]:
223+ rec.takeFocus = true
224+ }
225+ }
226+ case propTransientFor:
227+ var t uint32
228+ if len(val) >= 4 {
229+ t = word(val, 0)
230+ }
231+ if t == rec.transient {
232+ return
233+ }
234+ rec.transient = t
235+ x.classify(rec)
236+ if rec.ready {
237+ x.host.Parent(s, x.managedSlot(t))
238+ }
239+ case propWindowType:
240+ rec.wtype = 0
241+ for i := 0; i+4 <= len(val); i += 4 {
242+ if t := x.windowType(binary.LittleEndian.Uint32(val[i:])); t != 0 {
243+ rec.wtype = t
244+ break
245+ }
246+ }
247+ x.classify(rec)
248+ case propState:
249+ var modal, fs bool
250+ for i := 0; i+4 <= len(val); i += 4 {
251+ switch binary.LittleEndian.Uint32(val[i:]) {
252+ case x.atoms[atomNetWMStateModal]:
253+ modal = true
254+ case x.atoms[atomNetWMStateFullscreen]:
255+ fs = true
256+ }
257+ }
258+ changed := fs != rec.fullscreen
259+ rec.modal, rec.fullscreen = modal, fs
260+ if changed && rec.ready {
261+ x.host.Fullscreen(s, fs)
262+ }
263+ case propPID:
264+ if len(val) >= 4 {
265+ rec.pid = int32(word(val, 0))
266+ return
267+ }
268+ rec.pid = 0
269+ if !rec.ready && rec.pending&propClientIDs == 0 && x.conn.Ext().Info(xturbo.ExtRes).Present {
270+ x.queryClientIDs(s)
271+ }
272+ }
273+}
274+
275+// setTitle stores a title when it differs; only a change allocates.
276+func (x *XWM) setTitle(s Slot, val []byte) {
277+ rec := x.tab.rec(s)
278+ if rec.title == string(val) {
279+ return
280+ }
281+ t := strings.ToValidUTF8(string(val), "\uFFFD")
282+ if rec.title == t {
283+ return
284+ }
285+ rec.title = t
286+ if rec.ready {
287+ x.host.Title(s, rec.title)
288+ }
289+}
290+
291+// windowType returns the atom enum of a recognised _NET_WM_WINDOW_TYPE atom,
292+// or 0.
293+func (x *XWM) windowType(a uint32) uint8 {
294+ for t := atomNetWMWindowTypeNormal; t <= atomNetWMWindowTypeDND; t++ {
295+ if x.atoms[t] == a {
296+ return uint8(t)
297+ }
298+ }
299+ return 0
300+}
301+
302+// floatingType reports whether a window type floats by itself.
303+func floatingType(t uint8) bool {
304+ switch int(t) {
305+ case atomNetWMWindowTypeDialog, atomNetWMWindowTypeUtility, atomNetWMWindowTypeSplash, atomNetWMWindowTypeToolbar:
306+ return true
307+ }
308+ return false
309+}
310+
311+// classify sets rec.floating, the single source of truth: a dialog-like
312+// window type, a transient for a managed window, or a fixed size.
313+func (x *XWM) classify(rec *record) {
314+ fixed := rec.minW > 0 && rec.minH > 0 && rec.minW == rec.maxW && rec.minH == rec.maxH
315+ rec.floating = floatingType(rec.wtype) || x.managedSlot(rec.transient) != 0 || fixed
316+}
317+
318+// managedSlot returns the slot of the managed window xid, or 0.
319+func (x *XWM) managedSlot(xid uint32) Slot {
320+ if xid == 0 {
321+ return 0
322+ }
323+ if s := x.tab.lookup(xid); s != 0 && x.tab.rec(s).kind == kindManaged {
324+ return s
325+ }
326+ return 0
327+}
328+
329+// queryClientIDs asks XRes for the PID of the client owning s, which stands
330+// in for a missing _NET_WM_PID: Ready waits for it (propClientIDs).
331+func (x *XWM) queryClientIDs(s Slot) {
332+ rec := x.tab.rec(s)
333+ specs := [1]res.ClientIdSpec{{Client: rec.xid, Mask: res.ClientIdMaskLocalClientPID}}
334+ seq := res.QueryClientIds(x.conn.Buf(), specs[:])
335+ if seq == 0 {
336+ return
337+ }
338+ rec.pending |= propClientIDs
339+ x.expect(seq, kindClientIds, x.tab.tag(s), 0)
340+}
341+
342+// onClientIdsReply stores the PID XRes found and releases the map.
343+func (x *XWM) onClientIdsReply(s Slot, rec *record, r *xturbo.Reply) {
344+ if q, ok := res.ParseQueryClientIdsReply(r.Raw); ok {
345+ it := q.Ids()
346+ for it.Next() {
347+ v := it.Value()
348+ if v.Spec().Mask == res.ClientIdMaskLocalClientPID && v.Value().Len() > 0 {
349+ rec.pid = int32(v.Value().At(0))
350+ break
351+ }
352+ }
353+ }
354+ rec.pending &^= propClientIDs
355+ x.checkReady(s)
356+}
A internal/adapters/wayland/xwm/space.go +135 -0
@@ -0,0 +1,135 @@
1+package xwm
2+
3+import "github.com/bnema/neferwl/internal/ports"
4+
5+const maxRegions = 16
6+
7+// Region is one output's part of the layout and where it is in X space.
8+// X = layout - Origin.
9+type Region struct {
10+ Layout, X ports.Rect // logical layout rect; X rect in X pixels
11+}
12+
13+// Space maps layout coordinates (which may be negative) to X coordinates
14+// (non-negative: the layout top-left is X (0,0)). Fixed storage, no allocs.
15+type Space struct {
16+ regions [maxRegions]Region
17+ n int
18+ dropped int // outputs beyond maxRegions in the last Set
19+ ox, oy int // layout point that is X (0,0)
20+ w, h int // X screen size: the extents
21+}
22+
23+// Origin is the layout point that is X (0,0): the top-left of the layout
24+// extents.
25+func (s *Space) Origin() (ox, oy int) { return s.ox, s.oy }
26+
27+// Dropped is how many outputs of the last Set did not fit.
28+func (s *Space) Dropped() int { return s.dropped }
29+
30+// Set recomputes the mapping and reports whether it changed.
31+func (s *Space) Set(l ports.Layout) bool {
32+ var next Space
33+ n := len(l)
34+ if n > maxRegions {
35+ next.dropped = n - maxRegions
36+ n = maxRegions
37+ }
38+ next.n = n
39+ var maxX, maxY int
40+ for i := 0; i < n; i++ {
41+ o := l[i]
42+ if i == 0 || o.X < next.ox {
43+ next.ox = o.X
44+ }
45+ if i == 0 || o.Y < next.oy {
46+ next.oy = o.Y
47+ }
48+ if i == 0 || o.X+o.Width > maxX {
49+ maxX = o.X + o.Width
50+ }
51+ if i == 0 || o.Y+o.Height > maxY {
52+ maxY = o.Y + o.Height
53+ }
54+ }
55+ for i := 0; i < n; i++ {
56+ o := l[i]
57+ next.regions[i] = Region{
58+ Layout: ports.Rect{X: o.X, Y: o.Y, W: o.Width, H: o.Height},
59+ X: ports.Rect{X: o.X - next.ox, Y: o.Y - next.oy, W: o.Width, H: o.Height},
60+ }
61+ }
62+ next.w, next.h = maxX-next.ox, maxY-next.oy
63+ if next.regions == s.regions && next.n == s.n && next.ox == s.ox && next.oy == s.oy &&
64+ next.w == s.w && next.h == s.h {
65+ s.dropped = next.dropped
66+ return false
67+ }
68+ *s = next
69+ return true
70+}
71+
72+// Size is the X screen size.
73+func (s *Space) Size() (w, h int) { return s.w, s.h }
74+
75+// nearest returns the index of the region containing the layout point, or
76+// the closest one by squared distance when it is in a hole. -1 when empty.
77+func (s *Space) nearest(lx, ly int) int {
78+ best, bestD := -1, 0
79+ for i := 0; i < s.n; i++ {
80+ r := s.regions[i].Layout
81+ dx := axisGap(lx, r.X, r.X+r.W)
82+ dy := axisGap(ly, r.Y, r.Y+r.H)
83+ d := dx*dx + dy*dy
84+ if best < 0 || d < bestD {
85+ best, bestD = i, d
86+ }
87+ }
88+ return best
89+}
90+
91+// nearestX is nearest in X space.
92+func (s *Space) nearestX(x, y int) int {
93+ best, bestD := -1, 0
94+ for i := 0; i < s.n; i++ {
95+ r := s.regions[i].X
96+ dx := axisGap(x, r.X, r.X+r.W)
97+ dy := axisGap(y, r.Y, r.Y+r.H)
98+ d := dx*dx + dy*dy
99+ if best < 0 || d < bestD {
100+ best, bestD = i, d
101+ }
102+ }
103+ return best
104+}
105+
106+// axisGap is the distance from v to [lo, hi): 0 inside.
107+func axisGap(v, lo, hi int) int {
108+ if v < lo {
109+ return lo - v
110+ }
111+ if v >= hi {
112+ return v - hi + 1
113+ }
114+ return 0
115+}
116+
117+// ToX converts a layout point to X, through the nearest region.
118+func (s *Space) ToX(lx, ly int) (x, y int) {
119+ i := s.nearest(lx, ly)
120+ if i < 0 {
121+ return lx, ly
122+ }
123+ r := &s.regions[i]
124+ return r.X.X + lx - r.Layout.X, r.X.Y + ly - r.Layout.Y
125+}
126+
127+// ToLayout converts an X point to layout coordinates.
128+func (s *Space) ToLayout(x, y int) (lx, ly int) {
129+ i := s.nearestX(x, y)
130+ if i < 0 {
131+ return x, y
132+ }
133+ r := &s.regions[i]
134+ return r.Layout.X + x - r.X.X, r.Layout.Y + y - r.X.Y
135+}
A internal/adapters/wayland/xwm/space_test.go +94 -0
@@ -0,0 +1,94 @@
1+package xwm
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+func out(x, y, w, h int) ports.OutputPlacement {
10+ return ports.OutputPlacement{X: x, Y: y, Width: w, Height: h, Scale: 1}
11+}
12+
13+func TestSpaceLayouts(t *testing.T) {
14+ tests := []struct {
15+ name string
16+ l ports.Layout
17+ w, h int
18+ lx, ly, x, y int
19+ }{
20+ {"single", ports.Layout{out(0, 0, 1920, 1080)}, 1920, 1080, 10, 20, 10, 20},
21+ {"side by side", ports.Layout{out(0, 0, 1920, 1080), out(1920, 0, 1280, 1024)}, 3200, 1080, 2000, 5, 2000, 5},
22+ {"left-of negative origin", ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)}, 2560, 1080, -630, 20, 10, 20},
23+ {"above", ports.Layout{out(0, -500, 800, 500), out(0, 0, 800, 600)}, 800, 1100, 3, -400, 3, 100},
24+ {"below", ports.Layout{out(0, 0, 800, 600), out(0, 600, 800, 400)}, 800, 1000, 3, 700, 3, 700},
25+ {"hole nearest", ports.Layout{out(0, 0, 100, 100), out(300, 0, 100, 100)}, 400, 100, 120, 10, 120, 10},
26+ }
27+ for _, tc := range tests {
28+ t.Run(tc.name, func(t *testing.T) {
29+ var s Space
30+ if !s.Set(tc.l) {
31+ t.Fatal("first Set not changed")
32+ }
33+ if w, h := s.Size(); w != tc.w || h != tc.h {
34+ t.Fatalf("size %dx%d, want %dx%d", w, h, tc.w, tc.h)
35+ }
36+ x, y := s.ToX(tc.lx, tc.ly)
37+ if x != tc.x || y != tc.y {
38+ t.Fatalf("ToX = %d,%d want %d,%d", x, y, tc.x, tc.y)
39+ }
40+ if lx, ly := s.ToLayout(x, y); lx != tc.lx || ly != tc.ly {
41+ t.Fatalf("round trip = %d,%d want %d,%d", lx, ly, tc.lx, tc.ly)
42+ }
43+ if s.Set(tc.l) {
44+ t.Fatal("identical Set reported changed")
45+ }
46+ })
47+ }
48+}
49+
50+func TestSpaceEmptyAndOrigin(t *testing.T) {
51+ var s Space
52+ if x, y := s.ToX(5, 6); x != 5 || y != 6 {
53+ t.Fatalf("empty ToX = %d,%d", x, y)
54+ }
55+ s.Set(ports.Layout{out(-640, -20, 640, 480), out(0, 0, 1920, 1080)})
56+ if ox, oy := s.Origin(); ox != -640 || oy != -20 {
57+ t.Fatalf("origin = %d,%d", ox, oy)
58+ }
59+}
60+
61+func TestSpaceSeventeenOutputs(t *testing.T) {
62+ l := make(ports.Layout, 17)
63+ for i := range l {
64+ l[i] = out(i*100, 0, 100, 100)
65+ }
66+ var s Space
67+ s.Set(l)
68+ if s.Dropped() != 1 {
69+ t.Fatalf("dropped = %d", s.Dropped())
70+ }
71+ if w, _ := s.Size(); w != 1600 {
72+ t.Fatalf("width = %d", w)
73+ }
74+ if x, _ := s.ToX(1550, 5); x != 1550 {
75+ t.Fatalf("ToX = %d", x)
76+ }
77+}
78+
79+func TestSpaceAllocations(t *testing.T) {
80+ l := ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)}
81+ var s Space
82+ s.Set(l)
83+ n := testing.AllocsPerRun(100, func() {
84+ s.Set(l)
85+ x, y := s.ToX(-600, 10)
86+ x, y = s.ToLayout(x, y)
87+ _, _ = x, y
88+ _, _ = s.Origin()
89+ _, _ = s.Size()
90+ })
91+ if n != 0 {
92+ t.Fatalf("allocs = %v", n)
93+ }
94+}
A internal/adapters/wayland/xwm/streams_test.go +183 -0
@@ -0,0 +1,183 @@
1+package xwm
2+
3+import (
4+ "encoding/hex"
5+ "os"
6+ "path/filepath"
7+ "runtime"
8+ "strings"
9+ "testing"
10+ "time"
11+
12+ "bnema.dev/xturbo/xproto"
13+ "github.com/stretchr/testify/require"
14+)
15+
16+// The recorded streams are the bytes an X server wrote to the XWM during a
17+// steady-state session of one mapped window, captured from the scripted xpeer
18+// (real encoded X packets: events, GetProperty replies). The scenario is
19+// deterministic, so a replay meets the request sequence numbers it recorded.
20+// They are not xtrace captures of a real Xwayland: xpeer speaks the same wire
21+// protocol and keeps the fixture reproducible. Regenerate with
22+//
23+// XWM_UPDATE_STREAMS=1 go test ./internal/adapters/wayland/xwm -run TestRecordedStreams
24+const (
25+ streamCycles = 400
26+ streamWarm = 20 // cycles replayed before measuring
27+)
28+
29+const streamWin = 0x500001
30+
31+type streamKind struct {
32+ name string
33+ // perCycle is Events+Replies the XWM sees per cycle.
34+ perCycle int
35+ cycle func(hs *harness)
36+}
37+
38+var streamKinds = []streamKind{
39+ // A title notification the XWM answers with one GetProperty whose reply
40+ // equals the stored title.
41+ {"property_unchanged", 2, func(hs *harness) {
42+ before := hs.x.Stats().Replies
43+ hs.p.propertyNotify(streamWin, hs.p.atom("_NET_WM_NAME"), false)
44+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before })
45+ }},
46+ // The same plus ConfigureNotify, FocusIn and MapNotify.
47+ {"steady", 5, func(hs *harness) {
48+ before := hs.x.Stats().Replies
49+ hs.p.propertyNotify(streamWin, hs.p.atom("_NET_WM_NAME"), false)
50+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before })
51+ events := hs.x.Stats().Events
52+ hs.p.configureNotify(streamWin)
53+ hs.p.focusIn(streamWin)
54+ hs.p.mapNotify(streamWin)
55+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Events >= events+3 })
56+ }},
57+}
58+
59+// streamScenario boots, maps one window and parks the peer behind a poke so
60+// nothing but the test writes to the XWM afterwards (an allocation count is
61+// process-wide). The poke is part of the scenario, recorded or replayed.
62+func streamScenario(t *testing.T) *harness {
63+ t.Helper()
64+ hs := newHarness(t, nil)
65+ hs.classic(streamWin)
66+ hs.mapAndWait(streamWin)
67+ // The steady state is a focused window: a FocusIn on it is not denied,
68+ // so no request depends on how events are batched into flushes. The
69+ // poke makes sure the focus requests are out before the peer pauses.
70+ hs.x.Focus(hs.x.tab.lookup(streamWin))
71+ poke(hs)
72+ return hs
73+}
74+
75+func poke(hs *harness) {
76+ hs.t.Helper()
77+ n := len(hs.p.requests())
78+ xproto.GetInputFocus(hs.x.conn.Buf())
79+ hs.x.flush()
80+ hs.p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus })
81+}
82+
83+func recordStream(t *testing.T, k streamKind) []byte {
84+ hs := streamScenario(t)
85+ poke(hs)
86+ hs.p.mu.Lock()
87+ hs.p.record = true
88+ hs.p.mu.Unlock()
89+ for i := 0; i < streamCycles; i++ {
90+ k.cycle(hs)
91+ }
92+ hs.p.mu.Lock()
93+ defer hs.p.mu.Unlock()
94+ hs.p.record = false
95+ return append([]byte(nil), hs.p.tap...)
96+}
97+
98+func streamPath(k streamKind) string {
99+ return filepath.Join("testdata", "streams", k.name+".hex")
100+}
101+
102+func loadStream(t *testing.T, k streamKind) []byte {
103+ t.Helper()
104+ text, err := os.ReadFile(streamPath(k))
105+ require.NoError(t, err)
106+ b, err := hex.DecodeString(strings.Join(strings.Fields(string(text)), ""))
107+ require.NoError(t, err)
108+ return b
109+}
110+
111+func encodeHex(b []byte) string {
112+ var sb strings.Builder
113+ for len(b) > 0 {
114+ n := min(32, len(b))
115+ sb.WriteString(hex.EncodeToString(b[:n]))
116+ sb.WriteByte('\n')
117+ b = b[n:]
118+ }
119+ return sb.String()
120+}
121+
122+func TestRecordedStreams(t *testing.T) {
123+ for _, k := range streamKinds {
124+ t.Run(k.name, func(t *testing.T) {
125+ got := recordStream(t, k)
126+ if os.Getenv("XWM_UPDATE_STREAMS") != "" {
127+ require.NoError(t, os.MkdirAll(filepath.Dir(streamPath(k)), 0o755))
128+ require.NoError(t, os.WriteFile(streamPath(k), []byte(encodeHex(got)), 0o644))
129+ }
130+ require.Equal(t, loadStream(t, k), got, "the committed stream no longer matches the protocol the XWM speaks")
131+ })
132+ }
133+}
134+
135+// replay feeds the recorded stream and reports the mallocs of the part after
136+// the warm-up cycles.
137+func replay(t *testing.T, k streamKind) (mallocs uint64, hs *harness) {
138+ t.Helper()
139+ stream := loadStream(t, k)
140+ hs = streamScenario(t)
141+ hs.p.pause()
142+ poke(hs)
143+ _, err := hs.p.f.Write(stream)
144+ require.NoError(t, err)
145+
146+ x := hs.x
147+ base := x.Stats().Events + x.Stats().Replies
148+ drive := func(until uint64) {
149+ deadline := time.Now().Add(10 * time.Second)
150+ for x.Stats().Events+x.Stats().Replies < until {
151+ if time.Now().After(deadline) { // no formatting arguments: boxing them would count as mallocs
152+ t.Fatal("stream stalled")
153+ }
154+ before := x.Stats().Events + x.Stats().Replies
155+ x.onReadable()
156+ if x.Stats().Events+x.Stats().Replies == before {
157+ runtime.Gosched()
158+ }
159+ }
160+ }
161+ drive(base + uint64(streamWarm*k.perCycle))
162+ target := base + uint64(streamCycles*k.perCycle)
163+
164+ var m0, m1 runtime.MemStats
165+ runtime.GC()
166+ runtime.ReadMemStats(&m0)
167+ drive(target)
168+ runtime.ReadMemStats(&m1)
169+ require.Equal(t, target, x.Stats().Events+x.Stats().Replies)
170+ return m1.Mallocs - m0.Mallocs, hs
171+}
172+
173+func TestPropertyUnchangedAllocations(t *testing.T) {
174+ m, hs := replay(t, streamKinds[0])
175+ require.Zero(t, m, "unchanged property cycles must not allocate")
176+ require.Equal(t, "net title", hs.x.tab.rec(hs.x.tab.lookup(streamWin)).title)
177+}
178+
179+func TestDispatchAllocations(t *testing.T) {
180+ m, hs := replay(t, streamKinds[1])
181+ require.Zero(t, m, "steady-state event dispatch must not allocate")
182+ require.True(t, hs.x.tab.rec(hs.x.tab.lookup(streamWin)).mapped)
183+}
A internal/adapters/wayland/xwm/table.go +241 -0
@@ -0,0 +1,241 @@
1+package xwm
2+
3+import (
4+ "math/bits"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+// Record kinds.
10+const (
11+ kindFree uint8 = iota
12+ kindManaged
13+ kindUnmanaged
14+)
15+
16+const (
17+ initialSlots = 256
18+ // maxSlots bounds the table: a Slot travels in the low 16 bits of an
19+ // Expect argument.
20+ maxSlots = 1<<16 - 1
21+ indexGone = 0xFFFFFFFF // tombstone in table.index
22+)
23+
24+// pending is the configure state not yet (or last) sent to the X server.
25+type pending struct {
26+ x, y int32
27+ w, h uint32
28+ fullscreen, activated, dirty bool
29+}
30+
31+// ovRect is a layout rectangle.
32+type ovRect struct{ x, y, w, h int32 }
33+
34+// record is one X window known to the XWM. Strings are the only per-record
35+// heap data.
36+type record struct {
37+ xid uint32
38+ id ports.WindowID
39+ kind uint8
40+ gen uint16 // bumped on insert; stale Expect replies are told apart by it
41+
42+ // X state
43+ mapped, mapping, ready bool
44+ x, y int32
45+ w, h uint32
46+
47+ // properties
48+ pending uint16 // propMask bits still awaited
49+ input, deleteWindow, takeFocus bool
50+ wtype uint8
51+ modal, fullscreen bool
52+ transient uint32 // xid
53+ pid int32
54+ minW, minH, maxW, maxH int32
55+ title, class string // owned, replaced only on change
56+ hasNetName bool
57+ floating bool // dialog-like type; set with the properties
58+ sized bool // the client asked for its size before Ready
59+
60+ // pairing
61+ serial uint64
62+ surface uint64
63+
64+ // layout position of a managed window, from Place
65+ lx, ly int32
66+ placed bool
67+
68+ // unmanaged (override-redirect) windows
69+ noInput bool // tooltip, DND, notification: never takes the pointer
70+ ovPaired bool // OverrideMapped sent, OverrideGone owed
71+ ovQueued bool // slot is in XWM.ovDirty
72+ ov ovRect // layout rect last sent to the host
73+
74+ // configure
75+ pend, sent pending
76+ stateSent uint8 // _NET_WM_STATE bits last written
77+}
78+
79+// table is a slab of records with an open-addressing index by X id. No Go map.
80+type table struct {
81+ recs []record // slab, len == cap, power of two
82+ free []uint32 // free slot indices (LIFO)
83+ index []uint32 // slot+1, 0 empty, indexGone tombstone; len == 2*len(recs)
84+ shift uint8
85+ n int
86+ tombs int
87+}
88+
89+func newTable(size int) *table {
90+ t := &table{}
91+ t.resize(size)
92+ return t
93+}
94+
95+// resize reallocates the slab, free list and index for size records and
96+// reindexes the live ones.
97+func (t *table) resize(size int) {
98+ old := t.recs
99+ t.recs = make([]record, size)
100+ copy(t.recs, old)
101+ t.free = make([]uint32, 0, size)
102+ for i := min(size, maxSlots) - 1; i >= 0; i-- {
103+ if i >= len(old) || old[i].kind == kindFree {
104+ t.free = append(t.free, uint32(i))
105+ }
106+ }
107+ t.index = make([]uint32, 2*size)
108+ t.shift = uint8(32 - bits.TrailingZeros(uint(len(t.index))))
109+ t.reindex()
110+}
111+
112+// reindex rebuilds the index from the live records, dropping tombstones.
113+// It does not allocate.
114+func (t *table) reindex() {
115+ clear(t.index)
116+ t.tombs = 0
117+ for i := range t.recs {
118+ if t.recs[i].kind != kindFree {
119+ t.indexAdd(t.recs[i].xid, uint32(i)+1)
120+ }
121+ }
122+}
123+
124+func (t *table) hash(xid uint32) uint32 { return (xid * 0x9E3779B1) >> t.shift }
125+
126+// indexAdd places slot for xid, which must be absent, in the first empty or
127+// tombstone position of its probe sequence.
128+func (t *table) indexAdd(xid, slot uint32) {
129+ mask := uint32(len(t.index) - 1)
130+ for i := t.hash(xid); ; i = (i + 1) & mask {
131+ switch t.index[i] {
132+ case 0:
133+ t.index[i] = slot
134+ return
135+ case indexGone:
136+ t.index[i] = slot
137+ t.tombs--
138+ return
139+ }
140+ }
141+}
142+
143+// grow doubles the table. It reports false at maxSlots.
144+func (t *table) grow() bool {
145+ if len(t.recs) >= 1<<16 {
146+ return false
147+ }
148+ t.resize(2 * len(t.recs))
149+ return len(t.free) > 0
150+}
151+
152+// insert returns the slot of xid, creating a managed record (generation
153+// bumped) when absent. It returns 0 when the table is full. Growth is the
154+// only allocation.
155+func (t *table) insert(xid uint32) Slot {
156+ if s := t.lookup(xid); s != 0 {
157+ return s
158+ }
159+ if len(t.free) == 0 && !t.grow() {
160+ return 0
161+ }
162+ i := t.free[len(t.free)-1]
163+ t.free = t.free[:len(t.free)-1]
164+ if t.n+t.tombs+1 > len(t.index)/4*3 { // keep an empty index cell for lookup to stop at
165+ t.reindex()
166+ }
167+ r := &t.recs[i]
168+ gen := r.gen + 1
169+ if gen == 0 {
170+ gen = 1
171+ }
172+ *r = record{xid: xid, kind: kindManaged, gen: gen, input: true}
173+ t.indexAdd(xid, i+1)
174+ t.n++
175+ return Slot(i + 1)
176+}
177+
178+// remove frees s. The record is zeroed except its generation.
179+func (t *table) remove(s Slot) {
180+ r := t.rec(s)
181+ if r == nil || r.kind == kindFree {
182+ return
183+ }
184+ mask := uint32(len(t.index) - 1)
185+ for i := t.hash(r.xid); ; i = (i + 1) & mask {
186+ v := t.index[i]
187+ if v == 0 {
188+ break
189+ }
190+ if v == uint32(s) {
191+ t.index[i] = indexGone
192+ t.tombs++
193+ break
194+ }
195+ }
196+ *r = record{gen: r.gen}
197+ t.free = append(t.free, uint32(s)-1)
198+ t.n--
199+ if t.n+t.tombs > len(t.index)/4*3 {
200+ t.reindex()
201+ }
202+}
203+
204+// lookup returns the slot of xid, or 0.
205+func (t *table) lookup(xid uint32) Slot {
206+ mask := uint32(len(t.index) - 1)
207+ for i := t.hash(xid); ; i = (i + 1) & mask {
208+ v := t.index[i]
209+ if v == 0 {
210+ return 0
211+ }
212+ if v != indexGone && t.recs[v-1].xid == xid {
213+ return Slot(v)
214+ }
215+ }
216+}
217+
218+// rec returns the record of s, or nil when s is out of range. The pointer is
219+// valid only until the next insert, host call or anything else that can grow
220+// the table or free s: never hold it across those, fetch it again with rec(s).
221+func (t *table) rec(s Slot) *record {
222+ if s == 0 || int(s) > len(t.recs) {
223+ return nil
224+ }
225+ return &t.recs[s-1]
226+}
227+
228+// tag is the Expect argument naming s: slot in the low 16 bits, generation
229+// in the high 16.
230+func (t *table) tag(s Slot) uint32 { return uint32(s) | uint32(t.recs[s-1].gen)<<16 }
231+
232+// live resolves an Expect tag. It returns nil when the slot was freed or
233+// reused since the request was sent.
234+func (t *table) live(tag uint32) (Slot, *record) {
235+ s := Slot(tag & 0xFFFF)
236+ r := t.rec(s)
237+ if r == nil || r.kind == kindFree || r.gen != uint16(tag>>16) {
238+ return 0, nil
239+ }
240+ return s, r
241+}
A internal/adapters/wayland/xwm/table_test.go +138 -0
@@ -0,0 +1,138 @@
1+package xwm
2+
3+import (
4+ "math/rand/v2"
5+ "testing"
6+
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestTableAgainstMap(t *testing.T) {
11+ rng := rand.New(rand.NewPCG(1, 2))
12+ tab := newTable(initialSlots)
13+ model := map[uint32]Slot{}
14+ var xids []uint32
15+ for i := 0; i < 10000; i++ {
16+ switch op := rng.IntN(3); {
17+ case op < 2 || len(xids) == 0:
18+ xid := rng.Uint32()&0x1fffff | 0x200000
19+ if _, ok := model[xid]; ok {
20+ continue
21+ }
22+ s := tab.insert(xid)
23+ require.NotZero(t, s)
24+ model[xid] = s
25+ xids = append(xids, xid)
26+ default:
27+ k := rng.IntN(len(xids))
28+ xid := xids[k]
29+ xids[k] = xids[len(xids)-1]
30+ xids = xids[:len(xids)-1]
31+ tab.remove(model[xid])
32+ delete(model, xid)
33+ }
34+ if i%500 == 0 {
35+ for xid, s := range model {
36+ require.Equal(t, s, tab.lookup(xid))
37+ }
38+ require.Zero(t, tab.lookup(rng.Uint32()|1<<31))
39+ }
40+ }
41+ require.Equal(t, len(model), tab.n)
42+ for xid, s := range model {
43+ require.Equal(t, s, tab.lookup(xid))
44+ require.Equal(t, xid, tab.rec(s).xid)
45+ }
46+}
47+
48+func TestTableGrowth(t *testing.T) {
49+ tab := newTable(initialSlots)
50+ slots := make([]Slot, 0, initialSlots+1)
51+ for i := 0; i < initialSlots; i++ {
52+ slots = append(slots, tab.insert(uint32(0x200000+i)))
53+ }
54+ require.Len(t, tab.recs, initialSlots)
55+ tab.rec(slots[7]).title = "kept"
56+ slots = append(slots, tab.insert(0x300000))
57+ require.Len(t, tab.recs, 2*initialSlots)
58+ require.Len(t, tab.index, 4*initialSlots)
59+ for i, s := range slots[:initialSlots] {
60+ require.Equal(t, s, tab.lookup(uint32(0x200000+i)))
61+ }
62+ require.Equal(t, "kept", tab.rec(slots[7]).title, "growth keeps records")
63+ require.Equal(t, slots[initialSlots], tab.lookup(0x300000))
64+}
65+
66+func TestTableTombstoneReuse(t *testing.T) {
67+ tab := newTable(initialSlots)
68+ for i := 0; i < 5000; i++ { // far more churn than slots: tombstones must be recycled
69+ s := tab.insert(uint32(0x200000 + i))
70+ require.NotZero(t, s)
71+ require.Equal(t, s, tab.lookup(uint32(0x200000+i)))
72+ tab.remove(s)
73+ require.Zero(t, tab.lookup(uint32(0x200000+i)))
74+ }
75+ require.Len(t, tab.recs, initialSlots)
76+ require.Zero(t, tab.n)
77+}
78+
79+func TestTableGenerationAndTag(t *testing.T) {
80+ tab := newTable(initialSlots)
81+ s := tab.insert(0x200001)
82+ tag := tab.tag(s)
83+ got, rec := tab.live(tag)
84+ require.Equal(t, s, got)
85+ require.NotNil(t, rec)
86+ tab.remove(s)
87+ _, rec = tab.live(tag)
88+ require.Nil(t, rec, "freed slot")
89+ s2 := tab.insert(0x200002)
90+ require.Equal(t, s, s2, "LIFO free list reuses the slot")
91+ _, rec = tab.live(tag)
92+ require.Nil(t, rec, "stale tag after slot reuse")
93+ _, rec = tab.live(tab.tag(s2))
94+ require.NotNil(t, rec)
95+}
96+
97+func TestTableFull(t *testing.T) {
98+ tab := newTable(initialSlots)
99+ for i := 0; i < maxSlots; i++ {
100+ require.NotZero(t, tab.insert(uint32(i+1)), i)
101+ }
102+ require.Zero(t, tab.insert(0xFFFFFF), "table is bounded by the 16-bit slot in Expect tags")
103+ require.Equal(t, Slot(maxSlots), tab.lookup(maxSlots))
104+}
105+
106+func TestTableInsertExisting(t *testing.T) {
107+ tab := newTable(initialSlots)
108+ s := tab.insert(5)
109+ require.Equal(t, s, tab.insert(5))
110+ require.Equal(t, 1, tab.n)
111+}
112+
113+func TestTableLookupAllocations(t *testing.T) {
114+ tab := newTable(initialSlots)
115+ for i := 0; i < 200; i++ {
116+ tab.insert(uint32(0x200000 + i))
117+ }
118+ var sink Slot
119+ n := testing.AllocsPerRun(1000, func() {
120+ sink += tab.lookup(0x200010)
121+ sink += tab.lookup(0x7fffffff) // absent
122+ _, r := tab.live(tab.tag(3))
123+ if r != nil {
124+ sink++
125+ }
126+ })
127+ require.Zero(t, n)
128+ require.NotZero(t, sink)
129+}
130+
131+func TestTableInsertRemoveAllocations(t *testing.T) {
132+ tab := newTable(initialSlots)
133+ n := testing.AllocsPerRun(1000, func() {
134+ s := tab.insert(0x200001)
135+ tab.remove(s)
136+ })
137+ require.Zero(t, n)
138+}
A internal/adapters/wayland/xwm/testdata/streams/property_unchanged.hex +950 -0
@@ -0,0 +1,950 @@
1+1c00650001005000f30300000000000000000000000000000000000000000000
2+0108660003000000ef0300000000000009000000000000000000000000000000
3+6e6574207469746c650000001c00660001005000f30300000000000000000000
4+0000000000000000000000000108670003000000ef0300000000000009000000
5+0000000000000000000000006e6574207469746c650000001c00670001005000
6+f303000000000000000000000000000000000000000000000108680003000000
7+ef03000000000000090000000000000000000000000000006e6574207469746c
8+650000001c00680001005000f303000000000000000000000000000000000000
9+000000000108690003000000ef03000000000000090000000000000000000000
10+000000006e6574207469746c650000001c00690001005000f303000000000000
11+0000000000000000000000000000000001086a0003000000ef03000000000000
12+090000000000000000000000000000006e6574207469746c650000001c006a00
13+01005000f3030000000000000000000000000000000000000000000001086b00
14+03000000ef03000000000000090000000000000000000000000000006e657420
15+7469746c650000001c006b0001005000f3030000000000000000000000000000
16+000000000000000001086c0003000000ef030000000000000900000000000000
17+00000000000000006e6574207469746c650000001c006c0001005000f3030000
18+000000000000000000000000000000000000000001086d0003000000ef030000
19+00000000090000000000000000000000000000006e6574207469746c65000000
20+1c006d0001005000f30300000000000000000000000000000000000000000000
21+01086e0003000000ef0300000000000009000000000000000000000000000000
22+6e6574207469746c650000001c006e0001005000f30300000000000000000000
23+00000000000000000000000001086f0003000000ef0300000000000009000000
24+0000000000000000000000006e6574207469746c650000001c006f0001005000
25+f303000000000000000000000000000000000000000000000108700003000000
26+ef03000000000000090000000000000000000000000000006e6574207469746c
27+650000001c00700001005000f303000000000000000000000000000000000000
28+000000000108710003000000ef03000000000000090000000000000000000000
29+000000006e6574207469746c650000001c00710001005000f303000000000000
30+000000000000000000000000000000000108720003000000ef03000000000000
31+090000000000000000000000000000006e6574207469746c650000001c007200
32+01005000f3030000000000000000000000000000000000000000000001087300
33+03000000ef03000000000000090000000000000000000000000000006e657420
34+7469746c650000001c00730001005000f3030000000000000000000000000000
35+00000000000000000108740003000000ef030000000000000900000000000000
36+00000000000000006e6574207469746c650000001c00740001005000f3030000
37+00000000000000000000000000000000000000000108750003000000ef030000
38+00000000090000000000000000000000000000006e6574207469746c65000000
39+1c00750001005000f30300000000000000000000000000000000000000000000
40+0108760003000000ef0300000000000009000000000000000000000000000000
41+6e6574207469746c650000001c00760001005000f30300000000000000000000
42+0000000000000000000000000108770003000000ef0300000000000009000000
43+0000000000000000000000006e6574207469746c650000001c00770001005000
44+f303000000000000000000000000000000000000000000000108780003000000
45+ef03000000000000090000000000000000000000000000006e6574207469746c
46+650000001c00780001005000f303000000000000000000000000000000000000
47+000000000108790003000000ef03000000000000090000000000000000000000
48+000000006e6574207469746c650000001c00790001005000f303000000000000
49+0000000000000000000000000000000001087a0003000000ef03000000000000
50+090000000000000000000000000000006e6574207469746c650000001c007a00
51+01005000f3030000000000000000000000000000000000000000000001087b00
52+03000000ef03000000000000090000000000000000000000000000006e657420
53+7469746c650000001c007b0001005000f3030000000000000000000000000000
54+000000000000000001087c0003000000ef030000000000000900000000000000
55+00000000000000006e6574207469746c650000001c007c0001005000f3030000
56+000000000000000000000000000000000000000001087d0003000000ef030000
57+00000000090000000000000000000000000000006e6574207469746c65000000
58+1c007d0001005000f30300000000000000000000000000000000000000000000
59+01087e0003000000ef0300000000000009000000000000000000000000000000
60+6e6574207469746c650000001c007e0001005000f30300000000000000000000
61+00000000000000000000000001087f0003000000ef0300000000000009000000
62+0000000000000000000000006e6574207469746c650000001c007f0001005000
63+f303000000000000000000000000000000000000000000000108800003000000
64+ef03000000000000090000000000000000000000000000006e6574207469746c
65+650000001c00800001005000f303000000000000000000000000000000000000
66+000000000108810003000000ef03000000000000090000000000000000000000
67+000000006e6574207469746c650000001c00810001005000f303000000000000
68+000000000000000000000000000000000108820003000000ef03000000000000
69+090000000000000000000000000000006e6574207469746c650000001c008200
70+01005000f3030000000000000000000000000000000000000000000001088300
71+03000000ef03000000000000090000000000000000000000000000006e657420
72+7469746c650000001c00830001005000f3030000000000000000000000000000
73+00000000000000000108840003000000ef030000000000000900000000000000
74+00000000000000006e6574207469746c650000001c00840001005000f3030000
75+00000000000000000000000000000000000000000108850003000000ef030000
76+00000000090000000000000000000000000000006e6574207469746c65000000
77+1c00850001005000f30300000000000000000000000000000000000000000000
78+0108860003000000ef0300000000000009000000000000000000000000000000
79+6e6574207469746c650000001c00860001005000f30300000000000000000000
80+0000000000000000000000000108870003000000ef0300000000000009000000
81+0000000000000000000000006e6574207469746c650000001c00870001005000
82+f303000000000000000000000000000000000000000000000108880003000000
83+ef03000000000000090000000000000000000000000000006e6574207469746c
84+650000001c00880001005000f303000000000000000000000000000000000000
85+000000000108890003000000ef03000000000000090000000000000000000000
86+000000006e6574207469746c650000001c00890001005000f303000000000000
87+0000000000000000000000000000000001088a0003000000ef03000000000000
88+090000000000000000000000000000006e6574207469746c650000001c008a00
89+01005000f3030000000000000000000000000000000000000000000001088b00
90+03000000ef03000000000000090000000000000000000000000000006e657420
91+7469746c650000001c008b0001005000f3030000000000000000000000000000
92+000000000000000001088c0003000000ef030000000000000900000000000000
93+00000000000000006e6574207469746c650000001c008c0001005000f3030000
94+000000000000000000000000000000000000000001088d0003000000ef030000
95+00000000090000000000000000000000000000006e6574207469746c65000000
96+1c008d0001005000f30300000000000000000000000000000000000000000000
97+01088e0003000000ef0300000000000009000000000000000000000000000000
98+6e6574207469746c650000001c008e0001005000f30300000000000000000000
99+00000000000000000000000001088f0003000000ef0300000000000009000000
100+0000000000000000000000006e6574207469746c650000001c008f0001005000
101+f303000000000000000000000000000000000000000000000108900003000000
102+ef03000000000000090000000000000000000000000000006e6574207469746c
103+650000001c00900001005000f303000000000000000000000000000000000000
104+000000000108910003000000ef03000000000000090000000000000000000000
105+000000006e6574207469746c650000001c00910001005000f303000000000000
106+000000000000000000000000000000000108920003000000ef03000000000000
107+090000000000000000000000000000006e6574207469746c650000001c009200
108+01005000f3030000000000000000000000000000000000000000000001089300
109+03000000ef03000000000000090000000000000000000000000000006e657420
110+7469746c650000001c00930001005000f3030000000000000000000000000000
111+00000000000000000108940003000000ef030000000000000900000000000000
112+00000000000000006e6574207469746c650000001c00940001005000f3030000
113+00000000000000000000000000000000000000000108950003000000ef030000
114+00000000090000000000000000000000000000006e6574207469746c65000000
115+1c00950001005000f30300000000000000000000000000000000000000000000
116+0108960003000000ef0300000000000009000000000000000000000000000000
117+6e6574207469746c650000001c00960001005000f30300000000000000000000
118+0000000000000000000000000108970003000000ef0300000000000009000000
119+0000000000000000000000006e6574207469746c650000001c00970001005000
120+f303000000000000000000000000000000000000000000000108980003000000
121+ef03000000000000090000000000000000000000000000006e6574207469746c
122+650000001c00980001005000f303000000000000000000000000000000000000
123+000000000108990003000000ef03000000000000090000000000000000000000
124+000000006e6574207469746c650000001c00990001005000f303000000000000
125+0000000000000000000000000000000001089a0003000000ef03000000000000
126+090000000000000000000000000000006e6574207469746c650000001c009a00
127+01005000f3030000000000000000000000000000000000000000000001089b00
128+03000000ef03000000000000090000000000000000000000000000006e657420
129+7469746c650000001c009b0001005000f3030000000000000000000000000000
130+000000000000000001089c0003000000ef030000000000000900000000000000
131+00000000000000006e6574207469746c650000001c009c0001005000f3030000
132+000000000000000000000000000000000000000001089d0003000000ef030000
133+00000000090000000000000000000000000000006e6574207469746c65000000
134+1c009d0001005000f30300000000000000000000000000000000000000000000
135+01089e0003000000ef0300000000000009000000000000000000000000000000
136+6e6574207469746c650000001c009e0001005000f30300000000000000000000
137+00000000000000000000000001089f0003000000ef0300000000000009000000
138+0000000000000000000000006e6574207469746c650000001c009f0001005000
139+f303000000000000000000000000000000000000000000000108a00003000000
140+ef03000000000000090000000000000000000000000000006e6574207469746c
141+650000001c00a00001005000f303000000000000000000000000000000000000
142+000000000108a10003000000ef03000000000000090000000000000000000000
143+000000006e6574207469746c650000001c00a10001005000f303000000000000
144+000000000000000000000000000000000108a20003000000ef03000000000000
145+090000000000000000000000000000006e6574207469746c650000001c00a200
146+01005000f303000000000000000000000000000000000000000000000108a300
147+03000000ef03000000000000090000000000000000000000000000006e657420
148+7469746c650000001c00a30001005000f3030000000000000000000000000000
149+00000000000000000108a40003000000ef030000000000000900000000000000
150+00000000000000006e6574207469746c650000001c00a40001005000f3030000
151+00000000000000000000000000000000000000000108a50003000000ef030000
152+00000000090000000000000000000000000000006e6574207469746c65000000
153+1c00a50001005000f30300000000000000000000000000000000000000000000
154+0108a60003000000ef0300000000000009000000000000000000000000000000
155+6e6574207469746c650000001c00a60001005000f30300000000000000000000
156+0000000000000000000000000108a70003000000ef0300000000000009000000
157+0000000000000000000000006e6574207469746c650000001c00a70001005000
158+f303000000000000000000000000000000000000000000000108a80003000000
159+ef03000000000000090000000000000000000000000000006e6574207469746c
160+650000001c00a80001005000f303000000000000000000000000000000000000
161+000000000108a90003000000ef03000000000000090000000000000000000000
162+000000006e6574207469746c650000001c00a90001005000f303000000000000
163+000000000000000000000000000000000108aa0003000000ef03000000000000
164+090000000000000000000000000000006e6574207469746c650000001c00aa00
165+01005000f303000000000000000000000000000000000000000000000108ab00
166+03000000ef03000000000000090000000000000000000000000000006e657420
167+7469746c650000001c00ab0001005000f3030000000000000000000000000000
168+00000000000000000108ac0003000000ef030000000000000900000000000000
169+00000000000000006e6574207469746c650000001c00ac0001005000f3030000
170+00000000000000000000000000000000000000000108ad0003000000ef030000
171+00000000090000000000000000000000000000006e6574207469746c65000000
172+1c00ad0001005000f30300000000000000000000000000000000000000000000
173+0108ae0003000000ef0300000000000009000000000000000000000000000000
174+6e6574207469746c650000001c00ae0001005000f30300000000000000000000
175+0000000000000000000000000108af0003000000ef0300000000000009000000
176+0000000000000000000000006e6574207469746c650000001c00af0001005000
177+f303000000000000000000000000000000000000000000000108b00003000000
178+ef03000000000000090000000000000000000000000000006e6574207469746c
179+650000001c00b00001005000f303000000000000000000000000000000000000
180+000000000108b10003000000ef03000000000000090000000000000000000000
181+000000006e6574207469746c650000001c00b10001005000f303000000000000
182+000000000000000000000000000000000108b20003000000ef03000000000000
183+090000000000000000000000000000006e6574207469746c650000001c00b200
184+01005000f303000000000000000000000000000000000000000000000108b300
185+03000000ef03000000000000090000000000000000000000000000006e657420
186+7469746c650000001c00b30001005000f3030000000000000000000000000000
187+00000000000000000108b40003000000ef030000000000000900000000000000
188+00000000000000006e6574207469746c650000001c00b40001005000f3030000
189+00000000000000000000000000000000000000000108b50003000000ef030000
190+00000000090000000000000000000000000000006e6574207469746c65000000
191+1c00b50001005000f30300000000000000000000000000000000000000000000
192+0108b60003000000ef0300000000000009000000000000000000000000000000
193+6e6574207469746c650000001c00b60001005000f30300000000000000000000
194+0000000000000000000000000108b70003000000ef0300000000000009000000
195+0000000000000000000000006e6574207469746c650000001c00b70001005000
196+f303000000000000000000000000000000000000000000000108b80003000000
197+ef03000000000000090000000000000000000000000000006e6574207469746c
198+650000001c00b80001005000f303000000000000000000000000000000000000
199+000000000108b90003000000ef03000000000000090000000000000000000000
200+000000006e6574207469746c650000001c00b90001005000f303000000000000
201+000000000000000000000000000000000108ba0003000000ef03000000000000
202+090000000000000000000000000000006e6574207469746c650000001c00ba00
203+01005000f303000000000000000000000000000000000000000000000108bb00
204+03000000ef03000000000000090000000000000000000000000000006e657420
205+7469746c650000001c00bb0001005000f3030000000000000000000000000000
206+00000000000000000108bc0003000000ef030000000000000900000000000000
207+00000000000000006e6574207469746c650000001c00bc0001005000f3030000
208+00000000000000000000000000000000000000000108bd0003000000ef030000
209+00000000090000000000000000000000000000006e6574207469746c65000000
210+1c00bd0001005000f30300000000000000000000000000000000000000000000
211+0108be0003000000ef0300000000000009000000000000000000000000000000
212+6e6574207469746c650000001c00be0001005000f30300000000000000000000
213+0000000000000000000000000108bf0003000000ef0300000000000009000000
214+0000000000000000000000006e6574207469746c650000001c00bf0001005000
215+f303000000000000000000000000000000000000000000000108c00003000000
216+ef03000000000000090000000000000000000000000000006e6574207469746c
217+650000001c00c00001005000f303000000000000000000000000000000000000
218+000000000108c10003000000ef03000000000000090000000000000000000000
219+000000006e6574207469746c650000001c00c10001005000f303000000000000
220+000000000000000000000000000000000108c20003000000ef03000000000000
221+090000000000000000000000000000006e6574207469746c650000001c00c200
222+01005000f303000000000000000000000000000000000000000000000108c300
223+03000000ef03000000000000090000000000000000000000000000006e657420
224+7469746c650000001c00c30001005000f3030000000000000000000000000000
225+00000000000000000108c40003000000ef030000000000000900000000000000
226+00000000000000006e6574207469746c650000001c00c40001005000f3030000
227+00000000000000000000000000000000000000000108c50003000000ef030000
228+00000000090000000000000000000000000000006e6574207469746c65000000
229+1c00c50001005000f30300000000000000000000000000000000000000000000
230+0108c60003000000ef0300000000000009000000000000000000000000000000
231+6e6574207469746c650000001c00c60001005000f30300000000000000000000
232+0000000000000000000000000108c70003000000ef0300000000000009000000
233+0000000000000000000000006e6574207469746c650000001c00c70001005000
234+f303000000000000000000000000000000000000000000000108c80003000000
235+ef03000000000000090000000000000000000000000000006e6574207469746c
236+650000001c00c80001005000f303000000000000000000000000000000000000
237+000000000108c90003000000ef03000000000000090000000000000000000000
238+000000006e6574207469746c650000001c00c90001005000f303000000000000
239+000000000000000000000000000000000108ca0003000000ef03000000000000
240+090000000000000000000000000000006e6574207469746c650000001c00ca00
241+01005000f303000000000000000000000000000000000000000000000108cb00
242+03000000ef03000000000000090000000000000000000000000000006e657420
243+7469746c650000001c00cb0001005000f3030000000000000000000000000000
244+00000000000000000108cc0003000000ef030000000000000900000000000000
245+00000000000000006e6574207469746c650000001c00cc0001005000f3030000
246+00000000000000000000000000000000000000000108cd0003000000ef030000
247+00000000090000000000000000000000000000006e6574207469746c65000000
248+1c00cd0001005000f30300000000000000000000000000000000000000000000
249+0108ce0003000000ef0300000000000009000000000000000000000000000000
250+6e6574207469746c650000001c00ce0001005000f30300000000000000000000
251+0000000000000000000000000108cf0003000000ef0300000000000009000000
252+0000000000000000000000006e6574207469746c650000001c00cf0001005000
253+f303000000000000000000000000000000000000000000000108d00003000000
254+ef03000000000000090000000000000000000000000000006e6574207469746c
255+650000001c00d00001005000f303000000000000000000000000000000000000
256+000000000108d10003000000ef03000000000000090000000000000000000000
257+000000006e6574207469746c650000001c00d10001005000f303000000000000
258+000000000000000000000000000000000108d20003000000ef03000000000000
259+090000000000000000000000000000006e6574207469746c650000001c00d200
260+01005000f303000000000000000000000000000000000000000000000108d300
261+03000000ef03000000000000090000000000000000000000000000006e657420
262+7469746c650000001c00d30001005000f3030000000000000000000000000000
263+00000000000000000108d40003000000ef030000000000000900000000000000
264+00000000000000006e6574207469746c650000001c00d40001005000f3030000
265+00000000000000000000000000000000000000000108d50003000000ef030000
266+00000000090000000000000000000000000000006e6574207469746c65000000
267+1c00d50001005000f30300000000000000000000000000000000000000000000
268+0108d60003000000ef0300000000000009000000000000000000000000000000
269+6e6574207469746c650000001c00d60001005000f30300000000000000000000
270+0000000000000000000000000108d70003000000ef0300000000000009000000
271+0000000000000000000000006e6574207469746c650000001c00d70001005000
272+f303000000000000000000000000000000000000000000000108d80003000000
273+ef03000000000000090000000000000000000000000000006e6574207469746c
274+650000001c00d80001005000f303000000000000000000000000000000000000
275+000000000108d90003000000ef03000000000000090000000000000000000000
276+000000006e6574207469746c650000001c00d90001005000f303000000000000
277+000000000000000000000000000000000108da0003000000ef03000000000000
278+090000000000000000000000000000006e6574207469746c650000001c00da00
279+01005000f303000000000000000000000000000000000000000000000108db00
280+03000000ef03000000000000090000000000000000000000000000006e657420
281+7469746c650000001c00db0001005000f3030000000000000000000000000000
282+00000000000000000108dc0003000000ef030000000000000900000000000000
283+00000000000000006e6574207469746c650000001c00dc0001005000f3030000
284+00000000000000000000000000000000000000000108dd0003000000ef030000
285+00000000090000000000000000000000000000006e6574207469746c65000000
286+1c00dd0001005000f30300000000000000000000000000000000000000000000
287+0108de0003000000ef0300000000000009000000000000000000000000000000
288+6e6574207469746c650000001c00de0001005000f30300000000000000000000
289+0000000000000000000000000108df0003000000ef0300000000000009000000
290+0000000000000000000000006e6574207469746c650000001c00df0001005000
291+f303000000000000000000000000000000000000000000000108e00003000000
292+ef03000000000000090000000000000000000000000000006e6574207469746c
293+650000001c00e00001005000f303000000000000000000000000000000000000
294+000000000108e10003000000ef03000000000000090000000000000000000000
295+000000006e6574207469746c650000001c00e10001005000f303000000000000
296+000000000000000000000000000000000108e20003000000ef03000000000000
297+090000000000000000000000000000006e6574207469746c650000001c00e200
298+01005000f303000000000000000000000000000000000000000000000108e300
299+03000000ef03000000000000090000000000000000000000000000006e657420
300+7469746c650000001c00e30001005000f3030000000000000000000000000000
301+00000000000000000108e40003000000ef030000000000000900000000000000
302+00000000000000006e6574207469746c650000001c00e40001005000f3030000
303+00000000000000000000000000000000000000000108e50003000000ef030000
304+00000000090000000000000000000000000000006e6574207469746c65000000
305+1c00e50001005000f30300000000000000000000000000000000000000000000
306+0108e60003000000ef0300000000000009000000000000000000000000000000
307+6e6574207469746c650000001c00e60001005000f30300000000000000000000
308+0000000000000000000000000108e70003000000ef0300000000000009000000
309+0000000000000000000000006e6574207469746c650000001c00e70001005000
310+f303000000000000000000000000000000000000000000000108e80003000000
311+ef03000000000000090000000000000000000000000000006e6574207469746c
312+650000001c00e80001005000f303000000000000000000000000000000000000
313+000000000108e90003000000ef03000000000000090000000000000000000000
314+000000006e6574207469746c650000001c00e90001005000f303000000000000
315+000000000000000000000000000000000108ea0003000000ef03000000000000
316+090000000000000000000000000000006e6574207469746c650000001c00ea00
317+01005000f303000000000000000000000000000000000000000000000108eb00
318+03000000ef03000000000000090000000000000000000000000000006e657420
319+7469746c650000001c00eb0001005000f3030000000000000000000000000000
320+00000000000000000108ec0003000000ef030000000000000900000000000000
321+00000000000000006e6574207469746c650000001c00ec0001005000f3030000
322+00000000000000000000000000000000000000000108ed0003000000ef030000
323+00000000090000000000000000000000000000006e6574207469746c65000000
324+1c00ed0001005000f30300000000000000000000000000000000000000000000
325+0108ee0003000000ef0300000000000009000000000000000000000000000000
326+6e6574207469746c650000001c00ee0001005000f30300000000000000000000
327+0000000000000000000000000108ef0003000000ef0300000000000009000000
328+0000000000000000000000006e6574207469746c650000001c00ef0001005000
329+f303000000000000000000000000000000000000000000000108f00003000000
330+ef03000000000000090000000000000000000000000000006e6574207469746c
331+650000001c00f00001005000f303000000000000000000000000000000000000
332+000000000108f10003000000ef03000000000000090000000000000000000000
333+000000006e6574207469746c650000001c00f10001005000f303000000000000
334+000000000000000000000000000000000108f20003000000ef03000000000000
335+090000000000000000000000000000006e6574207469746c650000001c00f200
336+01005000f303000000000000000000000000000000000000000000000108f300
337+03000000ef03000000000000090000000000000000000000000000006e657420
338+7469746c650000001c00f30001005000f3030000000000000000000000000000
339+00000000000000000108f40003000000ef030000000000000900000000000000
340+00000000000000006e6574207469746c650000001c00f40001005000f3030000
341+00000000000000000000000000000000000000000108f50003000000ef030000
342+00000000090000000000000000000000000000006e6574207469746c65000000
343+1c00f50001005000f30300000000000000000000000000000000000000000000
344+0108f60003000000ef0300000000000009000000000000000000000000000000
345+6e6574207469746c650000001c00f60001005000f30300000000000000000000
346+0000000000000000000000000108f70003000000ef0300000000000009000000
347+0000000000000000000000006e6574207469746c650000001c00f70001005000
348+f303000000000000000000000000000000000000000000000108f80003000000
349+ef03000000000000090000000000000000000000000000006e6574207469746c
350+650000001c00f80001005000f303000000000000000000000000000000000000
351+000000000108f90003000000ef03000000000000090000000000000000000000
352+000000006e6574207469746c650000001c00f90001005000f303000000000000
353+000000000000000000000000000000000108fa0003000000ef03000000000000
354+090000000000000000000000000000006e6574207469746c650000001c00fa00
355+01005000f303000000000000000000000000000000000000000000000108fb00
356+03000000ef03000000000000090000000000000000000000000000006e657420
357+7469746c650000001c00fb0001005000f3030000000000000000000000000000
358+00000000000000000108fc0003000000ef030000000000000900000000000000
359+00000000000000006e6574207469746c650000001c00fc0001005000f3030000
360+00000000000000000000000000000000000000000108fd0003000000ef030000
361+00000000090000000000000000000000000000006e6574207469746c65000000
362+1c00fd0001005000f30300000000000000000000000000000000000000000000
363+0108fe0003000000ef0300000000000009000000000000000000000000000000
364+6e6574207469746c650000001c00fe0001005000f30300000000000000000000
365+0000000000000000000000000108ff0003000000ef0300000000000009000000
366+0000000000000000000000006e6574207469746c650000001c00ff0001005000
367+f303000000000000000000000000000000000000000000000108000103000000
368+ef03000000000000090000000000000000000000000000006e6574207469746c
369+650000001c00000101005000f303000000000000000000000000000000000000
370+000000000108010103000000ef03000000000000090000000000000000000000
371+000000006e6574207469746c650000001c00010101005000f303000000000000
372+000000000000000000000000000000000108020103000000ef03000000000000
373+090000000000000000000000000000006e6574207469746c650000001c000201
374+01005000f3030000000000000000000000000000000000000000000001080301
375+03000000ef03000000000000090000000000000000000000000000006e657420
376+7469746c650000001c00030101005000f3030000000000000000000000000000
377+00000000000000000108040103000000ef030000000000000900000000000000
378+00000000000000006e6574207469746c650000001c00040101005000f3030000
379+00000000000000000000000000000000000000000108050103000000ef030000
380+00000000090000000000000000000000000000006e6574207469746c65000000
381+1c00050101005000f30300000000000000000000000000000000000000000000
382+0108060103000000ef0300000000000009000000000000000000000000000000
383+6e6574207469746c650000001c00060101005000f30300000000000000000000
384+0000000000000000000000000108070103000000ef0300000000000009000000
385+0000000000000000000000006e6574207469746c650000001c00070101005000
386+f303000000000000000000000000000000000000000000000108080103000000
387+ef03000000000000090000000000000000000000000000006e6574207469746c
388+650000001c00080101005000f303000000000000000000000000000000000000
389+000000000108090103000000ef03000000000000090000000000000000000000
390+000000006e6574207469746c650000001c00090101005000f303000000000000
391+0000000000000000000000000000000001080a0103000000ef03000000000000
392+090000000000000000000000000000006e6574207469746c650000001c000a01
393+01005000f3030000000000000000000000000000000000000000000001080b01
394+03000000ef03000000000000090000000000000000000000000000006e657420
395+7469746c650000001c000b0101005000f3030000000000000000000000000000
396+000000000000000001080c0103000000ef030000000000000900000000000000
397+00000000000000006e6574207469746c650000001c000c0101005000f3030000
398+000000000000000000000000000000000000000001080d0103000000ef030000
399+00000000090000000000000000000000000000006e6574207469746c65000000
400+1c000d0101005000f30300000000000000000000000000000000000000000000
401+01080e0103000000ef0300000000000009000000000000000000000000000000
402+6e6574207469746c650000001c000e0101005000f30300000000000000000000
403+00000000000000000000000001080f0103000000ef0300000000000009000000
404+0000000000000000000000006e6574207469746c650000001c000f0101005000
405+f303000000000000000000000000000000000000000000000108100103000000
406+ef03000000000000090000000000000000000000000000006e6574207469746c
407+650000001c00100101005000f303000000000000000000000000000000000000
408+000000000108110103000000ef03000000000000090000000000000000000000
409+000000006e6574207469746c650000001c00110101005000f303000000000000
410+000000000000000000000000000000000108120103000000ef03000000000000
411+090000000000000000000000000000006e6574207469746c650000001c001201
412+01005000f3030000000000000000000000000000000000000000000001081301
413+03000000ef03000000000000090000000000000000000000000000006e657420
414+7469746c650000001c00130101005000f3030000000000000000000000000000
415+00000000000000000108140103000000ef030000000000000900000000000000
416+00000000000000006e6574207469746c650000001c00140101005000f3030000
417+00000000000000000000000000000000000000000108150103000000ef030000
418+00000000090000000000000000000000000000006e6574207469746c65000000
419+1c00150101005000f30300000000000000000000000000000000000000000000
420+0108160103000000ef0300000000000009000000000000000000000000000000
421+6e6574207469746c650000001c00160101005000f30300000000000000000000
422+0000000000000000000000000108170103000000ef0300000000000009000000
423+0000000000000000000000006e6574207469746c650000001c00170101005000
424+f303000000000000000000000000000000000000000000000108180103000000
425+ef03000000000000090000000000000000000000000000006e6574207469746c
426+650000001c00180101005000f303000000000000000000000000000000000000
427+000000000108190103000000ef03000000000000090000000000000000000000
428+000000006e6574207469746c650000001c00190101005000f303000000000000
429+0000000000000000000000000000000001081a0103000000ef03000000000000
430+090000000000000000000000000000006e6574207469746c650000001c001a01
431+01005000f3030000000000000000000000000000000000000000000001081b01
432+03000000ef03000000000000090000000000000000000000000000006e657420
433+7469746c650000001c001b0101005000f3030000000000000000000000000000
434+000000000000000001081c0103000000ef030000000000000900000000000000
435+00000000000000006e6574207469746c650000001c001c0101005000f3030000
436+000000000000000000000000000000000000000001081d0103000000ef030000
437+00000000090000000000000000000000000000006e6574207469746c65000000
438+1c001d0101005000f30300000000000000000000000000000000000000000000
439+01081e0103000000ef0300000000000009000000000000000000000000000000
440+6e6574207469746c650000001c001e0101005000f30300000000000000000000
441+00000000000000000000000001081f0103000000ef0300000000000009000000
442+0000000000000000000000006e6574207469746c650000001c001f0101005000
443+f303000000000000000000000000000000000000000000000108200103000000
444+ef03000000000000090000000000000000000000000000006e6574207469746c
445+650000001c00200101005000f303000000000000000000000000000000000000
446+000000000108210103000000ef03000000000000090000000000000000000000
447+000000006e6574207469746c650000001c00210101005000f303000000000000
448+000000000000000000000000000000000108220103000000ef03000000000000
449+090000000000000000000000000000006e6574207469746c650000001c002201
450+01005000f3030000000000000000000000000000000000000000000001082301
451+03000000ef03000000000000090000000000000000000000000000006e657420
452+7469746c650000001c00230101005000f3030000000000000000000000000000
453+00000000000000000108240103000000ef030000000000000900000000000000
454+00000000000000006e6574207469746c650000001c00240101005000f3030000
455+00000000000000000000000000000000000000000108250103000000ef030000
456+00000000090000000000000000000000000000006e6574207469746c65000000
457+1c00250101005000f30300000000000000000000000000000000000000000000
458+0108260103000000ef0300000000000009000000000000000000000000000000
459+6e6574207469746c650000001c00260101005000f30300000000000000000000
460+0000000000000000000000000108270103000000ef0300000000000009000000
461+0000000000000000000000006e6574207469746c650000001c00270101005000
462+f303000000000000000000000000000000000000000000000108280103000000
463+ef03000000000000090000000000000000000000000000006e6574207469746c
464+650000001c00280101005000f303000000000000000000000000000000000000
465+000000000108290103000000ef03000000000000090000000000000000000000
466+000000006e6574207469746c650000001c00290101005000f303000000000000
467+0000000000000000000000000000000001082a0103000000ef03000000000000
468+090000000000000000000000000000006e6574207469746c650000001c002a01
469+01005000f3030000000000000000000000000000000000000000000001082b01
470+03000000ef03000000000000090000000000000000000000000000006e657420
471+7469746c650000001c002b0101005000f3030000000000000000000000000000
472+000000000000000001082c0103000000ef030000000000000900000000000000
473+00000000000000006e6574207469746c650000001c002c0101005000f3030000
474+000000000000000000000000000000000000000001082d0103000000ef030000
475+00000000090000000000000000000000000000006e6574207469746c65000000
476+1c002d0101005000f30300000000000000000000000000000000000000000000
477+01082e0103000000ef0300000000000009000000000000000000000000000000
478+6e6574207469746c650000001c002e0101005000f30300000000000000000000
479+00000000000000000000000001082f0103000000ef0300000000000009000000
480+0000000000000000000000006e6574207469746c650000001c002f0101005000
481+f303000000000000000000000000000000000000000000000108300103000000
482+ef03000000000000090000000000000000000000000000006e6574207469746c
483+650000001c00300101005000f303000000000000000000000000000000000000
484+000000000108310103000000ef03000000000000090000000000000000000000
485+000000006e6574207469746c650000001c00310101005000f303000000000000
486+000000000000000000000000000000000108320103000000ef03000000000000
487+090000000000000000000000000000006e6574207469746c650000001c003201
488+01005000f3030000000000000000000000000000000000000000000001083301
489+03000000ef03000000000000090000000000000000000000000000006e657420
490+7469746c650000001c00330101005000f3030000000000000000000000000000
491+00000000000000000108340103000000ef030000000000000900000000000000
492+00000000000000006e6574207469746c650000001c00340101005000f3030000
493+00000000000000000000000000000000000000000108350103000000ef030000
494+00000000090000000000000000000000000000006e6574207469746c65000000
495+1c00350101005000f30300000000000000000000000000000000000000000000
496+0108360103000000ef0300000000000009000000000000000000000000000000
497+6e6574207469746c650000001c00360101005000f30300000000000000000000
498+0000000000000000000000000108370103000000ef0300000000000009000000
499+0000000000000000000000006e6574207469746c650000001c00370101005000
500+f303000000000000000000000000000000000000000000000108380103000000
501+ef03000000000000090000000000000000000000000000006e6574207469746c
502+650000001c00380101005000f303000000000000000000000000000000000000
503+000000000108390103000000ef03000000000000090000000000000000000000
504+000000006e6574207469746c650000001c00390101005000f303000000000000
505+0000000000000000000000000000000001083a0103000000ef03000000000000
506+090000000000000000000000000000006e6574207469746c650000001c003a01
507+01005000f3030000000000000000000000000000000000000000000001083b01
508+03000000ef03000000000000090000000000000000000000000000006e657420
509+7469746c650000001c003b0101005000f3030000000000000000000000000000
510+000000000000000001083c0103000000ef030000000000000900000000000000
511+00000000000000006e6574207469746c650000001c003c0101005000f3030000
512+000000000000000000000000000000000000000001083d0103000000ef030000
513+00000000090000000000000000000000000000006e6574207469746c65000000
514+1c003d0101005000f30300000000000000000000000000000000000000000000
515+01083e0103000000ef0300000000000009000000000000000000000000000000
516+6e6574207469746c650000001c003e0101005000f30300000000000000000000
517+00000000000000000000000001083f0103000000ef0300000000000009000000
518+0000000000000000000000006e6574207469746c650000001c003f0101005000
519+f303000000000000000000000000000000000000000000000108400103000000
520+ef03000000000000090000000000000000000000000000006e6574207469746c
521+650000001c00400101005000f303000000000000000000000000000000000000
522+000000000108410103000000ef03000000000000090000000000000000000000
523+000000006e6574207469746c650000001c00410101005000f303000000000000
524+000000000000000000000000000000000108420103000000ef03000000000000
525+090000000000000000000000000000006e6574207469746c650000001c004201
526+01005000f3030000000000000000000000000000000000000000000001084301
527+03000000ef03000000000000090000000000000000000000000000006e657420
528+7469746c650000001c00430101005000f3030000000000000000000000000000
529+00000000000000000108440103000000ef030000000000000900000000000000
530+00000000000000006e6574207469746c650000001c00440101005000f3030000
531+00000000000000000000000000000000000000000108450103000000ef030000
532+00000000090000000000000000000000000000006e6574207469746c65000000
533+1c00450101005000f30300000000000000000000000000000000000000000000
534+0108460103000000ef0300000000000009000000000000000000000000000000
535+6e6574207469746c650000001c00460101005000f30300000000000000000000
536+0000000000000000000000000108470103000000ef0300000000000009000000
537+0000000000000000000000006e6574207469746c650000001c00470101005000
538+f303000000000000000000000000000000000000000000000108480103000000
539+ef03000000000000090000000000000000000000000000006e6574207469746c
540+650000001c00480101005000f303000000000000000000000000000000000000
541+000000000108490103000000ef03000000000000090000000000000000000000
542+000000006e6574207469746c650000001c00490101005000f303000000000000
543+0000000000000000000000000000000001084a0103000000ef03000000000000
544+090000000000000000000000000000006e6574207469746c650000001c004a01
545+01005000f3030000000000000000000000000000000000000000000001084b01
546+03000000ef03000000000000090000000000000000000000000000006e657420
547+7469746c650000001c004b0101005000f3030000000000000000000000000000
548+000000000000000001084c0103000000ef030000000000000900000000000000
549+00000000000000006e6574207469746c650000001c004c0101005000f3030000
550+000000000000000000000000000000000000000001084d0103000000ef030000
551+00000000090000000000000000000000000000006e6574207469746c65000000
552+1c004d0101005000f30300000000000000000000000000000000000000000000
553+01084e0103000000ef0300000000000009000000000000000000000000000000
554+6e6574207469746c650000001c004e0101005000f30300000000000000000000
555+00000000000000000000000001084f0103000000ef0300000000000009000000
556+0000000000000000000000006e6574207469746c650000001c004f0101005000
557+f303000000000000000000000000000000000000000000000108500103000000
558+ef03000000000000090000000000000000000000000000006e6574207469746c
559+650000001c00500101005000f303000000000000000000000000000000000000
560+000000000108510103000000ef03000000000000090000000000000000000000
561+000000006e6574207469746c650000001c00510101005000f303000000000000
562+000000000000000000000000000000000108520103000000ef03000000000000
563+090000000000000000000000000000006e6574207469746c650000001c005201
564+01005000f3030000000000000000000000000000000000000000000001085301
565+03000000ef03000000000000090000000000000000000000000000006e657420
566+7469746c650000001c00530101005000f3030000000000000000000000000000
567+00000000000000000108540103000000ef030000000000000900000000000000
568+00000000000000006e6574207469746c650000001c00540101005000f3030000
569+00000000000000000000000000000000000000000108550103000000ef030000
570+00000000090000000000000000000000000000006e6574207469746c65000000
571+1c00550101005000f30300000000000000000000000000000000000000000000
572+0108560103000000ef0300000000000009000000000000000000000000000000
573+6e6574207469746c650000001c00560101005000f30300000000000000000000
574+0000000000000000000000000108570103000000ef0300000000000009000000
575+0000000000000000000000006e6574207469746c650000001c00570101005000
576+f303000000000000000000000000000000000000000000000108580103000000
577+ef03000000000000090000000000000000000000000000006e6574207469746c
578+650000001c00580101005000f303000000000000000000000000000000000000
579+000000000108590103000000ef03000000000000090000000000000000000000
580+000000006e6574207469746c650000001c00590101005000f303000000000000
581+0000000000000000000000000000000001085a0103000000ef03000000000000
582+090000000000000000000000000000006e6574207469746c650000001c005a01
583+01005000f3030000000000000000000000000000000000000000000001085b01
584+03000000ef03000000000000090000000000000000000000000000006e657420
585+7469746c650000001c005b0101005000f3030000000000000000000000000000
586+000000000000000001085c0103000000ef030000000000000900000000000000
587+00000000000000006e6574207469746c650000001c005c0101005000f3030000
588+000000000000000000000000000000000000000001085d0103000000ef030000
589+00000000090000000000000000000000000000006e6574207469746c65000000
590+1c005d0101005000f30300000000000000000000000000000000000000000000
591+01085e0103000000ef0300000000000009000000000000000000000000000000
592+6e6574207469746c650000001c005e0101005000f30300000000000000000000
593+00000000000000000000000001085f0103000000ef0300000000000009000000
594+0000000000000000000000006e6574207469746c650000001c005f0101005000
595+f303000000000000000000000000000000000000000000000108600103000000
596+ef03000000000000090000000000000000000000000000006e6574207469746c
597+650000001c00600101005000f303000000000000000000000000000000000000
598+000000000108610103000000ef03000000000000090000000000000000000000
599+000000006e6574207469746c650000001c00610101005000f303000000000000
600+000000000000000000000000000000000108620103000000ef03000000000000
601+090000000000000000000000000000006e6574207469746c650000001c006201
602+01005000f3030000000000000000000000000000000000000000000001086301
603+03000000ef03000000000000090000000000000000000000000000006e657420
604+7469746c650000001c00630101005000f3030000000000000000000000000000
605+00000000000000000108640103000000ef030000000000000900000000000000
606+00000000000000006e6574207469746c650000001c00640101005000f3030000
607+00000000000000000000000000000000000000000108650103000000ef030000
608+00000000090000000000000000000000000000006e6574207469746c65000000
609+1c00650101005000f30300000000000000000000000000000000000000000000
610+0108660103000000ef0300000000000009000000000000000000000000000000
611+6e6574207469746c650000001c00660101005000f30300000000000000000000
612+0000000000000000000000000108670103000000ef0300000000000009000000
613+0000000000000000000000006e6574207469746c650000001c00670101005000
614+f303000000000000000000000000000000000000000000000108680103000000
615+ef03000000000000090000000000000000000000000000006e6574207469746c
616+650000001c00680101005000f303000000000000000000000000000000000000
617+000000000108690103000000ef03000000000000090000000000000000000000
618+000000006e6574207469746c650000001c00690101005000f303000000000000
619+0000000000000000000000000000000001086a0103000000ef03000000000000
620+090000000000000000000000000000006e6574207469746c650000001c006a01
621+01005000f3030000000000000000000000000000000000000000000001086b01
622+03000000ef03000000000000090000000000000000000000000000006e657420
623+7469746c650000001c006b0101005000f3030000000000000000000000000000
624+000000000000000001086c0103000000ef030000000000000900000000000000
625+00000000000000006e6574207469746c650000001c006c0101005000f3030000
626+000000000000000000000000000000000000000001086d0103000000ef030000
627+00000000090000000000000000000000000000006e6574207469746c65000000
628+1c006d0101005000f30300000000000000000000000000000000000000000000
629+01086e0103000000ef0300000000000009000000000000000000000000000000
630+6e6574207469746c650000001c006e0101005000f30300000000000000000000
631+00000000000000000000000001086f0103000000ef0300000000000009000000
632+0000000000000000000000006e6574207469746c650000001c006f0101005000
633+f303000000000000000000000000000000000000000000000108700103000000
634+ef03000000000000090000000000000000000000000000006e6574207469746c
635+650000001c00700101005000f303000000000000000000000000000000000000
636+000000000108710103000000ef03000000000000090000000000000000000000
637+000000006e6574207469746c650000001c00710101005000f303000000000000
638+000000000000000000000000000000000108720103000000ef03000000000000
639+090000000000000000000000000000006e6574207469746c650000001c007201
640+01005000f3030000000000000000000000000000000000000000000001087301
641+03000000ef03000000000000090000000000000000000000000000006e657420
642+7469746c650000001c00730101005000f3030000000000000000000000000000
643+00000000000000000108740103000000ef030000000000000900000000000000
644+00000000000000006e6574207469746c650000001c00740101005000f3030000
645+00000000000000000000000000000000000000000108750103000000ef030000
646+00000000090000000000000000000000000000006e6574207469746c65000000
647+1c00750101005000f30300000000000000000000000000000000000000000000
648+0108760103000000ef0300000000000009000000000000000000000000000000
649+6e6574207469746c650000001c00760101005000f30300000000000000000000
650+0000000000000000000000000108770103000000ef0300000000000009000000
651+0000000000000000000000006e6574207469746c650000001c00770101005000
652+f303000000000000000000000000000000000000000000000108780103000000
653+ef03000000000000090000000000000000000000000000006e6574207469746c
654+650000001c00780101005000f303000000000000000000000000000000000000
655+000000000108790103000000ef03000000000000090000000000000000000000
656+000000006e6574207469746c650000001c00790101005000f303000000000000
657+0000000000000000000000000000000001087a0103000000ef03000000000000
658+090000000000000000000000000000006e6574207469746c650000001c007a01
659+01005000f3030000000000000000000000000000000000000000000001087b01
660+03000000ef03000000000000090000000000000000000000000000006e657420
661+7469746c650000001c007b0101005000f3030000000000000000000000000000
662+000000000000000001087c0103000000ef030000000000000900000000000000
663+00000000000000006e6574207469746c650000001c007c0101005000f3030000
664+000000000000000000000000000000000000000001087d0103000000ef030000
665+00000000090000000000000000000000000000006e6574207469746c65000000
666+1c007d0101005000f30300000000000000000000000000000000000000000000
667+01087e0103000000ef0300000000000009000000000000000000000000000000
668+6e6574207469746c650000001c007e0101005000f30300000000000000000000
669+00000000000000000000000001087f0103000000ef0300000000000009000000
670+0000000000000000000000006e6574207469746c650000001c007f0101005000
671+f303000000000000000000000000000000000000000000000108800103000000
672+ef03000000000000090000000000000000000000000000006e6574207469746c
673+650000001c00800101005000f303000000000000000000000000000000000000
674+000000000108810103000000ef03000000000000090000000000000000000000
675+000000006e6574207469746c650000001c00810101005000f303000000000000
676+000000000000000000000000000000000108820103000000ef03000000000000
677+090000000000000000000000000000006e6574207469746c650000001c008201
678+01005000f3030000000000000000000000000000000000000000000001088301
679+03000000ef03000000000000090000000000000000000000000000006e657420
680+7469746c650000001c00830101005000f3030000000000000000000000000000
681+00000000000000000108840103000000ef030000000000000900000000000000
682+00000000000000006e6574207469746c650000001c00840101005000f3030000
683+00000000000000000000000000000000000000000108850103000000ef030000
684+00000000090000000000000000000000000000006e6574207469746c65000000
685+1c00850101005000f30300000000000000000000000000000000000000000000
686+0108860103000000ef0300000000000009000000000000000000000000000000
687+6e6574207469746c650000001c00860101005000f30300000000000000000000
688+0000000000000000000000000108870103000000ef0300000000000009000000
689+0000000000000000000000006e6574207469746c650000001c00870101005000
690+f303000000000000000000000000000000000000000000000108880103000000
691+ef03000000000000090000000000000000000000000000006e6574207469746c
692+650000001c00880101005000f303000000000000000000000000000000000000
693+000000000108890103000000ef03000000000000090000000000000000000000
694+000000006e6574207469746c650000001c00890101005000f303000000000000
695+0000000000000000000000000000000001088a0103000000ef03000000000000
696+090000000000000000000000000000006e6574207469746c650000001c008a01
697+01005000f3030000000000000000000000000000000000000000000001088b01
698+03000000ef03000000000000090000000000000000000000000000006e657420
699+7469746c650000001c008b0101005000f3030000000000000000000000000000
700+000000000000000001088c0103000000ef030000000000000900000000000000
701+00000000000000006e6574207469746c650000001c008c0101005000f3030000
702+000000000000000000000000000000000000000001088d0103000000ef030000
703+00000000090000000000000000000000000000006e6574207469746c65000000
704+1c008d0101005000f30300000000000000000000000000000000000000000000
705+01088e0103000000ef0300000000000009000000000000000000000000000000
706+6e6574207469746c650000001c008e0101005000f30300000000000000000000
707+00000000000000000000000001088f0103000000ef0300000000000009000000
708+0000000000000000000000006e6574207469746c650000001c008f0101005000
709+f303000000000000000000000000000000000000000000000108900103000000
710+ef03000000000000090000000000000000000000000000006e6574207469746c
711+650000001c00900101005000f303000000000000000000000000000000000000
712+000000000108910103000000ef03000000000000090000000000000000000000
713+000000006e6574207469746c650000001c00910101005000f303000000000000
714+000000000000000000000000000000000108920103000000ef03000000000000
715+090000000000000000000000000000006e6574207469746c650000001c009201
716+01005000f3030000000000000000000000000000000000000000000001089301
717+03000000ef03000000000000090000000000000000000000000000006e657420
718+7469746c650000001c00930101005000f3030000000000000000000000000000
719+00000000000000000108940103000000ef030000000000000900000000000000
720+00000000000000006e6574207469746c650000001c00940101005000f3030000
721+00000000000000000000000000000000000000000108950103000000ef030000
722+00000000090000000000000000000000000000006e6574207469746c65000000
723+1c00950101005000f30300000000000000000000000000000000000000000000
724+0108960103000000ef0300000000000009000000000000000000000000000000
725+6e6574207469746c650000001c00960101005000f30300000000000000000000
726+0000000000000000000000000108970103000000ef0300000000000009000000
727+0000000000000000000000006e6574207469746c650000001c00970101005000
728+f303000000000000000000000000000000000000000000000108980103000000
729+ef03000000000000090000000000000000000000000000006e6574207469746c
730+650000001c00980101005000f303000000000000000000000000000000000000
731+000000000108990103000000ef03000000000000090000000000000000000000
732+000000006e6574207469746c650000001c00990101005000f303000000000000
733+0000000000000000000000000000000001089a0103000000ef03000000000000
734+090000000000000000000000000000006e6574207469746c650000001c009a01
735+01005000f3030000000000000000000000000000000000000000000001089b01
736+03000000ef03000000000000090000000000000000000000000000006e657420
737+7469746c650000001c009b0101005000f3030000000000000000000000000000
738+000000000000000001089c0103000000ef030000000000000900000000000000
739+00000000000000006e6574207469746c650000001c009c0101005000f3030000
740+000000000000000000000000000000000000000001089d0103000000ef030000
741+00000000090000000000000000000000000000006e6574207469746c65000000
742+1c009d0101005000f30300000000000000000000000000000000000000000000
743+01089e0103000000ef0300000000000009000000000000000000000000000000
744+6e6574207469746c650000001c009e0101005000f30300000000000000000000
745+00000000000000000000000001089f0103000000ef0300000000000009000000
746+0000000000000000000000006e6574207469746c650000001c009f0101005000
747+f303000000000000000000000000000000000000000000000108a00103000000
748+ef03000000000000090000000000000000000000000000006e6574207469746c
749+650000001c00a00101005000f303000000000000000000000000000000000000
750+000000000108a10103000000ef03000000000000090000000000000000000000
751+000000006e6574207469746c650000001c00a10101005000f303000000000000
752+000000000000000000000000000000000108a20103000000ef03000000000000
753+090000000000000000000000000000006e6574207469746c650000001c00a201
754+01005000f303000000000000000000000000000000000000000000000108a301
755+03000000ef03000000000000090000000000000000000000000000006e657420
756+7469746c650000001c00a30101005000f3030000000000000000000000000000
757+00000000000000000108a40103000000ef030000000000000900000000000000
758+00000000000000006e6574207469746c650000001c00a40101005000f3030000
759+00000000000000000000000000000000000000000108a50103000000ef030000
760+00000000090000000000000000000000000000006e6574207469746c65000000
761+1c00a50101005000f30300000000000000000000000000000000000000000000
762+0108a60103000000ef0300000000000009000000000000000000000000000000
763+6e6574207469746c650000001c00a60101005000f30300000000000000000000
764+0000000000000000000000000108a70103000000ef0300000000000009000000
765+0000000000000000000000006e6574207469746c650000001c00a70101005000
766+f303000000000000000000000000000000000000000000000108a80103000000
767+ef03000000000000090000000000000000000000000000006e6574207469746c
768+650000001c00a80101005000f303000000000000000000000000000000000000
769+000000000108a90103000000ef03000000000000090000000000000000000000
770+000000006e6574207469746c650000001c00a90101005000f303000000000000
771+000000000000000000000000000000000108aa0103000000ef03000000000000
772+090000000000000000000000000000006e6574207469746c650000001c00aa01
773+01005000f303000000000000000000000000000000000000000000000108ab01
774+03000000ef03000000000000090000000000000000000000000000006e657420
775+7469746c650000001c00ab0101005000f3030000000000000000000000000000
776+00000000000000000108ac0103000000ef030000000000000900000000000000
777+00000000000000006e6574207469746c650000001c00ac0101005000f3030000
778+00000000000000000000000000000000000000000108ad0103000000ef030000
779+00000000090000000000000000000000000000006e6574207469746c65000000
780+1c00ad0101005000f30300000000000000000000000000000000000000000000
781+0108ae0103000000ef0300000000000009000000000000000000000000000000
782+6e6574207469746c650000001c00ae0101005000f30300000000000000000000
783+0000000000000000000000000108af0103000000ef0300000000000009000000
784+0000000000000000000000006e6574207469746c650000001c00af0101005000
785+f303000000000000000000000000000000000000000000000108b00103000000
786+ef03000000000000090000000000000000000000000000006e6574207469746c
787+650000001c00b00101005000f303000000000000000000000000000000000000
788+000000000108b10103000000ef03000000000000090000000000000000000000
789+000000006e6574207469746c650000001c00b10101005000f303000000000000
790+000000000000000000000000000000000108b20103000000ef03000000000000
791+090000000000000000000000000000006e6574207469746c650000001c00b201
792+01005000f303000000000000000000000000000000000000000000000108b301
793+03000000ef03000000000000090000000000000000000000000000006e657420
794+7469746c650000001c00b30101005000f3030000000000000000000000000000
795+00000000000000000108b40103000000ef030000000000000900000000000000
796+00000000000000006e6574207469746c650000001c00b40101005000f3030000
797+00000000000000000000000000000000000000000108b50103000000ef030000
798+00000000090000000000000000000000000000006e6574207469746c65000000
799+1c00b50101005000f30300000000000000000000000000000000000000000000
800+0108b60103000000ef0300000000000009000000000000000000000000000000
801+6e6574207469746c650000001c00b60101005000f30300000000000000000000
802+0000000000000000000000000108b70103000000ef0300000000000009000000
803+0000000000000000000000006e6574207469746c650000001c00b70101005000
804+f303000000000000000000000000000000000000000000000108b80103000000
805+ef03000000000000090000000000000000000000000000006e6574207469746c
806+650000001c00b80101005000f303000000000000000000000000000000000000
807+000000000108b90103000000ef03000000000000090000000000000000000000
808+000000006e6574207469746c650000001c00b90101005000f303000000000000
809+000000000000000000000000000000000108ba0103000000ef03000000000000
810+090000000000000000000000000000006e6574207469746c650000001c00ba01
811+01005000f303000000000000000000000000000000000000000000000108bb01
812+03000000ef03000000000000090000000000000000000000000000006e657420
813+7469746c650000001c00bb0101005000f3030000000000000000000000000000
814+00000000000000000108bc0103000000ef030000000000000900000000000000
815+00000000000000006e6574207469746c650000001c00bc0101005000f3030000
816+00000000000000000000000000000000000000000108bd0103000000ef030000
817+00000000090000000000000000000000000000006e6574207469746c65000000
818+1c00bd0101005000f30300000000000000000000000000000000000000000000
819+0108be0103000000ef0300000000000009000000000000000000000000000000
820+6e6574207469746c650000001c00be0101005000f30300000000000000000000
821+0000000000000000000000000108bf0103000000ef0300000000000009000000
822+0000000000000000000000006e6574207469746c650000001c00bf0101005000
823+f303000000000000000000000000000000000000000000000108c00103000000
824+ef03000000000000090000000000000000000000000000006e6574207469746c
825+650000001c00c00101005000f303000000000000000000000000000000000000
826+000000000108c10103000000ef03000000000000090000000000000000000000
827+000000006e6574207469746c650000001c00c10101005000f303000000000000
828+000000000000000000000000000000000108c20103000000ef03000000000000
829+090000000000000000000000000000006e6574207469746c650000001c00c201
830+01005000f303000000000000000000000000000000000000000000000108c301
831+03000000ef03000000000000090000000000000000000000000000006e657420
832+7469746c650000001c00c30101005000f3030000000000000000000000000000
833+00000000000000000108c40103000000ef030000000000000900000000000000
834+00000000000000006e6574207469746c650000001c00c40101005000f3030000
835+00000000000000000000000000000000000000000108c50103000000ef030000
836+00000000090000000000000000000000000000006e6574207469746c65000000
837+1c00c50101005000f30300000000000000000000000000000000000000000000
838+0108c60103000000ef0300000000000009000000000000000000000000000000
839+6e6574207469746c650000001c00c60101005000f30300000000000000000000
840+0000000000000000000000000108c70103000000ef0300000000000009000000
841+0000000000000000000000006e6574207469746c650000001c00c70101005000
842+f303000000000000000000000000000000000000000000000108c80103000000
843+ef03000000000000090000000000000000000000000000006e6574207469746c
844+650000001c00c80101005000f303000000000000000000000000000000000000
845+000000000108c90103000000ef03000000000000090000000000000000000000
846+000000006e6574207469746c650000001c00c90101005000f303000000000000
847+000000000000000000000000000000000108ca0103000000ef03000000000000
848+090000000000000000000000000000006e6574207469746c650000001c00ca01
849+01005000f303000000000000000000000000000000000000000000000108cb01
850+03000000ef03000000000000090000000000000000000000000000006e657420
851+7469746c650000001c00cb0101005000f3030000000000000000000000000000
852+00000000000000000108cc0103000000ef030000000000000900000000000000
853+00000000000000006e6574207469746c650000001c00cc0101005000f3030000
854+00000000000000000000000000000000000000000108cd0103000000ef030000
855+00000000090000000000000000000000000000006e6574207469746c65000000
856+1c00cd0101005000f30300000000000000000000000000000000000000000000
857+0108ce0103000000ef0300000000000009000000000000000000000000000000
858+6e6574207469746c650000001c00ce0101005000f30300000000000000000000
859+0000000000000000000000000108cf0103000000ef0300000000000009000000
860+0000000000000000000000006e6574207469746c650000001c00cf0101005000
861+f303000000000000000000000000000000000000000000000108d00103000000
862+ef03000000000000090000000000000000000000000000006e6574207469746c
863+650000001c00d00101005000f303000000000000000000000000000000000000
864+000000000108d10103000000ef03000000000000090000000000000000000000
865+000000006e6574207469746c650000001c00d10101005000f303000000000000
866+000000000000000000000000000000000108d20103000000ef03000000000000
867+090000000000000000000000000000006e6574207469746c650000001c00d201
868+01005000f303000000000000000000000000000000000000000000000108d301
869+03000000ef03000000000000090000000000000000000000000000006e657420
870+7469746c650000001c00d30101005000f3030000000000000000000000000000
871+00000000000000000108d40103000000ef030000000000000900000000000000
872+00000000000000006e6574207469746c650000001c00d40101005000f3030000
873+00000000000000000000000000000000000000000108d50103000000ef030000
874+00000000090000000000000000000000000000006e6574207469746c65000000
875+1c00d50101005000f30300000000000000000000000000000000000000000000
876+0108d60103000000ef0300000000000009000000000000000000000000000000
877+6e6574207469746c650000001c00d60101005000f30300000000000000000000
878+0000000000000000000000000108d70103000000ef0300000000000009000000
879+0000000000000000000000006e6574207469746c650000001c00d70101005000
880+f303000000000000000000000000000000000000000000000108d80103000000
881+ef03000000000000090000000000000000000000000000006e6574207469746c
882+650000001c00d80101005000f303000000000000000000000000000000000000
883+000000000108d90103000000ef03000000000000090000000000000000000000
884+000000006e6574207469746c650000001c00d90101005000f303000000000000
885+000000000000000000000000000000000108da0103000000ef03000000000000
886+090000000000000000000000000000006e6574207469746c650000001c00da01
887+01005000f303000000000000000000000000000000000000000000000108db01
888+03000000ef03000000000000090000000000000000000000000000006e657420
889+7469746c650000001c00db0101005000f3030000000000000000000000000000
890+00000000000000000108dc0103000000ef030000000000000900000000000000
891+00000000000000006e6574207469746c650000001c00dc0101005000f3030000
892+00000000000000000000000000000000000000000108dd0103000000ef030000
893+00000000090000000000000000000000000000006e6574207469746c65000000
894+1c00dd0101005000f30300000000000000000000000000000000000000000000
895+0108de0103000000ef0300000000000009000000000000000000000000000000
896+6e6574207469746c650000001c00de0101005000f30300000000000000000000
897+0000000000000000000000000108df0103000000ef0300000000000009000000
898+0000000000000000000000006e6574207469746c650000001c00df0101005000
899+f303000000000000000000000000000000000000000000000108e00103000000
900+ef03000000000000090000000000000000000000000000006e6574207469746c
901+650000001c00e00101005000f303000000000000000000000000000000000000
902+000000000108e10103000000ef03000000000000090000000000000000000000
903+000000006e6574207469746c650000001c00e10101005000f303000000000000
904+000000000000000000000000000000000108e20103000000ef03000000000000
905+090000000000000000000000000000006e6574207469746c650000001c00e201
906+01005000f303000000000000000000000000000000000000000000000108e301
907+03000000ef03000000000000090000000000000000000000000000006e657420
908+7469746c650000001c00e30101005000f3030000000000000000000000000000
909+00000000000000000108e40103000000ef030000000000000900000000000000
910+00000000000000006e6574207469746c650000001c00e40101005000f3030000
911+00000000000000000000000000000000000000000108e50103000000ef030000
912+00000000090000000000000000000000000000006e6574207469746c65000000
913+1c00e50101005000f30300000000000000000000000000000000000000000000
914+0108e60103000000ef0300000000000009000000000000000000000000000000
915+6e6574207469746c650000001c00e60101005000f30300000000000000000000
916+0000000000000000000000000108e70103000000ef0300000000000009000000
917+0000000000000000000000006e6574207469746c650000001c00e70101005000
918+f303000000000000000000000000000000000000000000000108e80103000000
919+ef03000000000000090000000000000000000000000000006e6574207469746c
920+650000001c00e80101005000f303000000000000000000000000000000000000
921+000000000108e90103000000ef03000000000000090000000000000000000000
922+000000006e6574207469746c650000001c00e90101005000f303000000000000
923+000000000000000000000000000000000108ea0103000000ef03000000000000
924+090000000000000000000000000000006e6574207469746c650000001c00ea01
925+01005000f303000000000000000000000000000000000000000000000108eb01
926+03000000ef03000000000000090000000000000000000000000000006e657420
927+7469746c650000001c00eb0101005000f3030000000000000000000000000000
928+00000000000000000108ec0103000000ef030000000000000900000000000000
929+00000000000000006e6574207469746c650000001c00ec0101005000f3030000
930+00000000000000000000000000000000000000000108ed0103000000ef030000
931+00000000090000000000000000000000000000006e6574207469746c65000000
932+1c00ed0101005000f30300000000000000000000000000000000000000000000
933+0108ee0103000000ef0300000000000009000000000000000000000000000000
934+6e6574207469746c650000001c00ee0101005000f30300000000000000000000
935+0000000000000000000000000108ef0103000000ef0300000000000009000000
936+0000000000000000000000006e6574207469746c650000001c00ef0101005000
937+f303000000000000000000000000000000000000000000000108f00103000000
938+ef03000000000000090000000000000000000000000000006e6574207469746c
939+650000001c00f00101005000f303000000000000000000000000000000000000
940+000000000108f10103000000ef03000000000000090000000000000000000000
941+000000006e6574207469746c650000001c00f10101005000f303000000000000
942+000000000000000000000000000000000108f20103000000ef03000000000000
943+090000000000000000000000000000006e6574207469746c650000001c00f201
944+01005000f303000000000000000000000000000000000000000000000108f301
945+03000000ef03000000000000090000000000000000000000000000006e657420
946+7469746c650000001c00f30101005000f3030000000000000000000000000000
947+00000000000000000108f40103000000ef030000000000000900000000000000
948+00000000000000006e6574207469746c650000001c00f40101005000f3030000
949+00000000000000000000000000000000000000000108f50103000000ef030000
950+00000000090000000000000000000000000000006e6574207469746c65000000
A internal/adapters/wayland/xwm/testdata/streams/steady.hex +2150 -0
@@ -0,0 +1,2150 @@
1+1c00650001005000f30300000000000000000000000000000000000000000000
2+0108660003000000ef0300000000000009000000000000000000000000000000
3+6e6574207469746c650000001600660001005000010050000000000000000000
4+0a0014002c01c800000000000900660001005000000000000000000000000000
5+0000000000000000000000001300660000010000010050000000000000000000
6+0000000000000000000000001c00660001005000f30300000000000000000000
7+0000000000000000000000000108670003000000ef0300000000000009000000
8+0000000000000000000000006e6574207469746c650000001600670001005000
9+0100500000000000000000000a0014002c01c800000000000900670001005000
10+0000000000000000000000000000000000000000000000001300670000010000
11+0100500000000000000000000000000000000000000000001c00670001005000
12+f303000000000000000000000000000000000000000000000108680003000000
13+ef03000000000000090000000000000000000000000000006e6574207469746c
14+6500000016006800010050000100500000000000000000000a0014002c01c800
15+0000000009006800010050000000000000000000000000000000000000000000
16+0000000013006800000100000100500000000000000000000000000000000000
17+000000001c00680001005000f303000000000000000000000000000000000000
18+000000000108690003000000ef03000000000000090000000000000000000000
19+000000006e6574207469746c6500000016006900010050000100500000000000
20+000000000a0014002c01c8000000000009006900010050000000000000000000
21+0000000000000000000000000000000013006900000100000100500000000000
22+000000000000000000000000000000001c00690001005000f303000000000000
23+0000000000000000000000000000000001086a0003000000ef03000000000000
24+090000000000000000000000000000006e6574207469746c6500000016006a00
25+010050000100500000000000000000000a0014002c01c8000000000009006a00
26+0100500000000000000000000000000000000000000000000000000013006a00
27+000100000100500000000000000000000000000000000000000000001c006a00
28+01005000f3030000000000000000000000000000000000000000000001086b00
29+03000000ef03000000000000090000000000000000000000000000006e657420
30+7469746c6500000016006b00010050000100500000000000000000000a001400
31+2c01c8000000000009006b000100500000000000000000000000000000000000
32+000000000000000013006b000001000001005000000000000000000000000000
33+00000000000000001c006b0001005000f3030000000000000000000000000000
34+000000000000000001086c0003000000ef030000000000000900000000000000
35+00000000000000006e6574207469746c6500000016006c000100500001005000
36+00000000000000000a0014002c01c8000000000009006c000100500000000000
37+000000000000000000000000000000000000000013006c000001000001005000
38+00000000000000000000000000000000000000001c006c0001005000f3030000
39+000000000000000000000000000000000000000001086d0003000000ef030000
40+00000000090000000000000000000000000000006e6574207469746c65000000
41+16006d00010050000100500000000000000000000a0014002c01c80000000000
42+09006d0001005000000000000000000000000000000000000000000000000000
43+13006d0000010000010050000000000000000000000000000000000000000000
44+1c006d0001005000f30300000000000000000000000000000000000000000000
45+01086e0003000000ef0300000000000009000000000000000000000000000000
46+6e6574207469746c6500000016006e0001005000010050000000000000000000
47+0a0014002c01c8000000000009006e0001005000000000000000000000000000
48+00000000000000000000000013006e0000010000010050000000000000000000
49+0000000000000000000000001c006e0001005000f30300000000000000000000
50+00000000000000000000000001086f0003000000ef0300000000000009000000
51+0000000000000000000000006e6574207469746c6500000016006f0001005000
52+0100500000000000000000000a0014002c01c8000000000009006f0001005000
53+00000000000000000000000000000000000000000000000013006f0000010000
54+0100500000000000000000000000000000000000000000001c006f0001005000
55+f303000000000000000000000000000000000000000000000108700003000000
56+ef03000000000000090000000000000000000000000000006e6574207469746c
57+6500000016007000010050000100500000000000000000000a0014002c01c800
58+0000000009007000010050000000000000000000000000000000000000000000
59+0000000013007000000100000100500000000000000000000000000000000000
60+000000001c00700001005000f303000000000000000000000000000000000000
61+000000000108710003000000ef03000000000000090000000000000000000000
62+000000006e6574207469746c6500000016007100010050000100500000000000
63+000000000a0014002c01c8000000000009007100010050000000000000000000
64+0000000000000000000000000000000013007100000100000100500000000000
65+000000000000000000000000000000001c00710001005000f303000000000000
66+000000000000000000000000000000000108720003000000ef03000000000000
67+090000000000000000000000000000006e6574207469746c6500000016007200
68+010050000100500000000000000000000a0014002c01c8000000000009007200
69+0100500000000000000000000000000000000000000000000000000013007200
70+000100000100500000000000000000000000000000000000000000001c007200
71+01005000f3030000000000000000000000000000000000000000000001087300
72+03000000ef03000000000000090000000000000000000000000000006e657420
73+7469746c6500000016007300010050000100500000000000000000000a001400
74+2c01c80000000000090073000100500000000000000000000000000000000000
75+0000000000000000130073000001000001005000000000000000000000000000
76+00000000000000001c00730001005000f3030000000000000000000000000000
77+00000000000000000108740003000000ef030000000000000900000000000000
78+00000000000000006e6574207469746c65000000160074000100500001005000
79+00000000000000000a0014002c01c80000000000090074000100500000000000
80+0000000000000000000000000000000000000000130074000001000001005000
81+00000000000000000000000000000000000000001c00740001005000f3030000
82+00000000000000000000000000000000000000000108750003000000ef030000
83+00000000090000000000000000000000000000006e6574207469746c65000000
84+16007500010050000100500000000000000000000a0014002c01c80000000000
85+0900750001005000000000000000000000000000000000000000000000000000
86+1300750000010000010050000000000000000000000000000000000000000000
87+1c00750001005000f30300000000000000000000000000000000000000000000
88+0108760003000000ef0300000000000009000000000000000000000000000000
89+6e6574207469746c650000001600760001005000010050000000000000000000
90+0a0014002c01c800000000000900760001005000000000000000000000000000
91+0000000000000000000000001300760000010000010050000000000000000000
92+0000000000000000000000001c00760001005000f30300000000000000000000
93+0000000000000000000000000108770003000000ef0300000000000009000000
94+0000000000000000000000006e6574207469746c650000001600770001005000
95+0100500000000000000000000a0014002c01c800000000000900770001005000
96+0000000000000000000000000000000000000000000000001300770000010000
97+0100500000000000000000000000000000000000000000001c00770001005000
98+f303000000000000000000000000000000000000000000000108780003000000
99+ef03000000000000090000000000000000000000000000006e6574207469746c
100+6500000016007800010050000100500000000000000000000a0014002c01c800
101+0000000009007800010050000000000000000000000000000000000000000000
102+0000000013007800000100000100500000000000000000000000000000000000
103+000000001c00780001005000f303000000000000000000000000000000000000
104+000000000108790003000000ef03000000000000090000000000000000000000
105+000000006e6574207469746c6500000016007900010050000100500000000000
106+000000000a0014002c01c8000000000009007900010050000000000000000000
107+0000000000000000000000000000000013007900000100000100500000000000
108+000000000000000000000000000000001c00790001005000f303000000000000
109+0000000000000000000000000000000001087a0003000000ef03000000000000
110+090000000000000000000000000000006e6574207469746c6500000016007a00
111+010050000100500000000000000000000a0014002c01c8000000000009007a00
112+0100500000000000000000000000000000000000000000000000000013007a00
113+000100000100500000000000000000000000000000000000000000001c007a00
114+01005000f3030000000000000000000000000000000000000000000001087b00
115+03000000ef03000000000000090000000000000000000000000000006e657420
116+7469746c6500000016007b00010050000100500000000000000000000a001400
117+2c01c8000000000009007b000100500000000000000000000000000000000000
118+000000000000000013007b000001000001005000000000000000000000000000
119+00000000000000001c007b0001005000f3030000000000000000000000000000
120+000000000000000001087c0003000000ef030000000000000900000000000000
121+00000000000000006e6574207469746c6500000016007c000100500001005000
122+00000000000000000a0014002c01c8000000000009007c000100500000000000
123+000000000000000000000000000000000000000013007c000001000001005000
124+00000000000000000000000000000000000000001c007c0001005000f3030000
125+000000000000000000000000000000000000000001087d0003000000ef030000
126+00000000090000000000000000000000000000006e6574207469746c65000000
127+16007d00010050000100500000000000000000000a0014002c01c80000000000
128+09007d0001005000000000000000000000000000000000000000000000000000
129+13007d0000010000010050000000000000000000000000000000000000000000
130+1c007d0001005000f30300000000000000000000000000000000000000000000
131+01087e0003000000ef0300000000000009000000000000000000000000000000
132+6e6574207469746c6500000016007e0001005000010050000000000000000000
133+0a0014002c01c8000000000009007e0001005000000000000000000000000000
134+00000000000000000000000013007e0000010000010050000000000000000000
135+0000000000000000000000001c007e0001005000f30300000000000000000000
136+00000000000000000000000001087f0003000000ef0300000000000009000000
137+0000000000000000000000006e6574207469746c6500000016007f0001005000
138+0100500000000000000000000a0014002c01c8000000000009007f0001005000
139+00000000000000000000000000000000000000000000000013007f0000010000
140+0100500000000000000000000000000000000000000000001c007f0001005000
141+f303000000000000000000000000000000000000000000000108800003000000
142+ef03000000000000090000000000000000000000000000006e6574207469746c
143+6500000016008000010050000100500000000000000000000a0014002c01c800
144+0000000009008000010050000000000000000000000000000000000000000000
145+0000000013008000000100000100500000000000000000000000000000000000
146+000000001c00800001005000f303000000000000000000000000000000000000
147+000000000108810003000000ef03000000000000090000000000000000000000
148+000000006e6574207469746c6500000016008100010050000100500000000000
149+000000000a0014002c01c8000000000009008100010050000000000000000000
150+0000000000000000000000000000000013008100000100000100500000000000
151+000000000000000000000000000000001c00810001005000f303000000000000
152+000000000000000000000000000000000108820003000000ef03000000000000
153+090000000000000000000000000000006e6574207469746c6500000016008200
154+010050000100500000000000000000000a0014002c01c8000000000009008200
155+0100500000000000000000000000000000000000000000000000000013008200
156+000100000100500000000000000000000000000000000000000000001c008200
157+01005000f3030000000000000000000000000000000000000000000001088300
158+03000000ef03000000000000090000000000000000000000000000006e657420
159+7469746c6500000016008300010050000100500000000000000000000a001400
160+2c01c80000000000090083000100500000000000000000000000000000000000
161+0000000000000000130083000001000001005000000000000000000000000000
162+00000000000000001c00830001005000f3030000000000000000000000000000
163+00000000000000000108840003000000ef030000000000000900000000000000
164+00000000000000006e6574207469746c65000000160084000100500001005000
165+00000000000000000a0014002c01c80000000000090084000100500000000000
166+0000000000000000000000000000000000000000130084000001000001005000
167+00000000000000000000000000000000000000001c00840001005000f3030000
168+00000000000000000000000000000000000000000108850003000000ef030000
169+00000000090000000000000000000000000000006e6574207469746c65000000
170+16008500010050000100500000000000000000000a0014002c01c80000000000
171+0900850001005000000000000000000000000000000000000000000000000000
172+1300850000010000010050000000000000000000000000000000000000000000
173+1c00850001005000f30300000000000000000000000000000000000000000000
174+0108860003000000ef0300000000000009000000000000000000000000000000
175+6e6574207469746c650000001600860001005000010050000000000000000000
176+0a0014002c01c800000000000900860001005000000000000000000000000000
177+0000000000000000000000001300860000010000010050000000000000000000
178+0000000000000000000000001c00860001005000f30300000000000000000000
179+0000000000000000000000000108870003000000ef0300000000000009000000
180+0000000000000000000000006e6574207469746c650000001600870001005000
181+0100500000000000000000000a0014002c01c800000000000900870001005000
182+0000000000000000000000000000000000000000000000001300870000010000
183+0100500000000000000000000000000000000000000000001c00870001005000
184+f303000000000000000000000000000000000000000000000108880003000000
185+ef03000000000000090000000000000000000000000000006e6574207469746c
186+6500000016008800010050000100500000000000000000000a0014002c01c800
187+0000000009008800010050000000000000000000000000000000000000000000
188+0000000013008800000100000100500000000000000000000000000000000000
189+000000001c00880001005000f303000000000000000000000000000000000000
190+000000000108890003000000ef03000000000000090000000000000000000000
191+000000006e6574207469746c6500000016008900010050000100500000000000
192+000000000a0014002c01c8000000000009008900010050000000000000000000
193+0000000000000000000000000000000013008900000100000100500000000000
194+000000000000000000000000000000001c00890001005000f303000000000000
195+0000000000000000000000000000000001088a0003000000ef03000000000000
196+090000000000000000000000000000006e6574207469746c6500000016008a00
197+010050000100500000000000000000000a0014002c01c8000000000009008a00
198+0100500000000000000000000000000000000000000000000000000013008a00
199+000100000100500000000000000000000000000000000000000000001c008a00
200+01005000f3030000000000000000000000000000000000000000000001088b00
201+03000000ef03000000000000090000000000000000000000000000006e657420
202+7469746c6500000016008b00010050000100500000000000000000000a001400
203+2c01c8000000000009008b000100500000000000000000000000000000000000
204+000000000000000013008b000001000001005000000000000000000000000000
205+00000000000000001c008b0001005000f3030000000000000000000000000000
206+000000000000000001088c0003000000ef030000000000000900000000000000
207+00000000000000006e6574207469746c6500000016008c000100500001005000
208+00000000000000000a0014002c01c8000000000009008c000100500000000000
209+000000000000000000000000000000000000000013008c000001000001005000
210+00000000000000000000000000000000000000001c008c0001005000f3030000
211+000000000000000000000000000000000000000001088d0003000000ef030000
212+00000000090000000000000000000000000000006e6574207469746c65000000
213+16008d00010050000100500000000000000000000a0014002c01c80000000000
214+09008d0001005000000000000000000000000000000000000000000000000000
215+13008d0000010000010050000000000000000000000000000000000000000000
216+1c008d0001005000f30300000000000000000000000000000000000000000000
217+01088e0003000000ef0300000000000009000000000000000000000000000000
218+6e6574207469746c6500000016008e0001005000010050000000000000000000
219+0a0014002c01c8000000000009008e0001005000000000000000000000000000
220+00000000000000000000000013008e0000010000010050000000000000000000
221+0000000000000000000000001c008e0001005000f30300000000000000000000
222+00000000000000000000000001088f0003000000ef0300000000000009000000
223+0000000000000000000000006e6574207469746c6500000016008f0001005000
224+0100500000000000000000000a0014002c01c8000000000009008f0001005000
225+00000000000000000000000000000000000000000000000013008f0000010000
226+0100500000000000000000000000000000000000000000001c008f0001005000
227+f303000000000000000000000000000000000000000000000108900003000000
228+ef03000000000000090000000000000000000000000000006e6574207469746c
229+6500000016009000010050000100500000000000000000000a0014002c01c800
230+0000000009009000010050000000000000000000000000000000000000000000
231+0000000013009000000100000100500000000000000000000000000000000000
232+000000001c00900001005000f303000000000000000000000000000000000000
233+000000000108910003000000ef03000000000000090000000000000000000000
234+000000006e6574207469746c6500000016009100010050000100500000000000
235+000000000a0014002c01c8000000000009009100010050000000000000000000
236+0000000000000000000000000000000013009100000100000100500000000000
237+000000000000000000000000000000001c00910001005000f303000000000000
238+000000000000000000000000000000000108920003000000ef03000000000000
239+090000000000000000000000000000006e6574207469746c6500000016009200
240+010050000100500000000000000000000a0014002c01c8000000000009009200
241+0100500000000000000000000000000000000000000000000000000013009200
242+000100000100500000000000000000000000000000000000000000001c009200
243+01005000f3030000000000000000000000000000000000000000000001089300
244+03000000ef03000000000000090000000000000000000000000000006e657420
245+7469746c6500000016009300010050000100500000000000000000000a001400
246+2c01c80000000000090093000100500000000000000000000000000000000000
247+0000000000000000130093000001000001005000000000000000000000000000
248+00000000000000001c00930001005000f3030000000000000000000000000000
249+00000000000000000108940003000000ef030000000000000900000000000000
250+00000000000000006e6574207469746c65000000160094000100500001005000
251+00000000000000000a0014002c01c80000000000090094000100500000000000
252+0000000000000000000000000000000000000000130094000001000001005000
253+00000000000000000000000000000000000000001c00940001005000f3030000
254+00000000000000000000000000000000000000000108950003000000ef030000
255+00000000090000000000000000000000000000006e6574207469746c65000000
256+16009500010050000100500000000000000000000a0014002c01c80000000000
257+0900950001005000000000000000000000000000000000000000000000000000
258+1300950000010000010050000000000000000000000000000000000000000000
259+1c00950001005000f30300000000000000000000000000000000000000000000
260+0108960003000000ef0300000000000009000000000000000000000000000000
261+6e6574207469746c650000001600960001005000010050000000000000000000
262+0a0014002c01c800000000000900960001005000000000000000000000000000
263+0000000000000000000000001300960000010000010050000000000000000000
264+0000000000000000000000001c00960001005000f30300000000000000000000
265+0000000000000000000000000108970003000000ef0300000000000009000000
266+0000000000000000000000006e6574207469746c650000001600970001005000
267+0100500000000000000000000a0014002c01c800000000000900970001005000
268+0000000000000000000000000000000000000000000000001300970000010000
269+0100500000000000000000000000000000000000000000001c00970001005000
270+f303000000000000000000000000000000000000000000000108980003000000
271+ef03000000000000090000000000000000000000000000006e6574207469746c
272+6500000016009800010050000100500000000000000000000a0014002c01c800
273+0000000009009800010050000000000000000000000000000000000000000000
274+0000000013009800000100000100500000000000000000000000000000000000
275+000000001c00980001005000f303000000000000000000000000000000000000
276+000000000108990003000000ef03000000000000090000000000000000000000
277+000000006e6574207469746c6500000016009900010050000100500000000000
278+000000000a0014002c01c8000000000009009900010050000000000000000000
279+0000000000000000000000000000000013009900000100000100500000000000
280+000000000000000000000000000000001c00990001005000f303000000000000
281+0000000000000000000000000000000001089a0003000000ef03000000000000
282+090000000000000000000000000000006e6574207469746c6500000016009a00
283+010050000100500000000000000000000a0014002c01c8000000000009009a00
284+0100500000000000000000000000000000000000000000000000000013009a00
285+000100000100500000000000000000000000000000000000000000001c009a00
286+01005000f3030000000000000000000000000000000000000000000001089b00
287+03000000ef03000000000000090000000000000000000000000000006e657420
288+7469746c6500000016009b00010050000100500000000000000000000a001400
289+2c01c8000000000009009b000100500000000000000000000000000000000000
290+000000000000000013009b000001000001005000000000000000000000000000
291+00000000000000001c009b0001005000f3030000000000000000000000000000
292+000000000000000001089c0003000000ef030000000000000900000000000000
293+00000000000000006e6574207469746c6500000016009c000100500001005000
294+00000000000000000a0014002c01c8000000000009009c000100500000000000
295+000000000000000000000000000000000000000013009c000001000001005000
296+00000000000000000000000000000000000000001c009c0001005000f3030000
297+000000000000000000000000000000000000000001089d0003000000ef030000
298+00000000090000000000000000000000000000006e6574207469746c65000000
299+16009d00010050000100500000000000000000000a0014002c01c80000000000
300+09009d0001005000000000000000000000000000000000000000000000000000
301+13009d0000010000010050000000000000000000000000000000000000000000
302+1c009d0001005000f30300000000000000000000000000000000000000000000
303+01089e0003000000ef0300000000000009000000000000000000000000000000
304+6e6574207469746c6500000016009e0001005000010050000000000000000000
305+0a0014002c01c8000000000009009e0001005000000000000000000000000000
306+00000000000000000000000013009e0000010000010050000000000000000000
307+0000000000000000000000001c009e0001005000f30300000000000000000000
308+00000000000000000000000001089f0003000000ef0300000000000009000000
309+0000000000000000000000006e6574207469746c6500000016009f0001005000
310+0100500000000000000000000a0014002c01c8000000000009009f0001005000
311+00000000000000000000000000000000000000000000000013009f0000010000
312+0100500000000000000000000000000000000000000000001c009f0001005000
313+f303000000000000000000000000000000000000000000000108a00003000000
314+ef03000000000000090000000000000000000000000000006e6574207469746c
315+650000001600a000010050000100500000000000000000000a0014002c01c800
316+000000000900a000010050000000000000000000000000000000000000000000
317+000000001300a000000100000100500000000000000000000000000000000000
318+000000001c00a00001005000f303000000000000000000000000000000000000
319+000000000108a10003000000ef03000000000000090000000000000000000000
320+000000006e6574207469746c650000001600a100010050000100500000000000
321+000000000a0014002c01c800000000000900a100010050000000000000000000
322+000000000000000000000000000000001300a100000100000100500000000000
323+000000000000000000000000000000001c00a10001005000f303000000000000
324+000000000000000000000000000000000108a20003000000ef03000000000000
325+090000000000000000000000000000006e6574207469746c650000001600a200
326+010050000100500000000000000000000a0014002c01c800000000000900a200
327+010050000000000000000000000000000000000000000000000000001300a200
328+000100000100500000000000000000000000000000000000000000001c00a200
329+01005000f303000000000000000000000000000000000000000000000108a300
330+03000000ef03000000000000090000000000000000000000000000006e657420
331+7469746c650000001600a300010050000100500000000000000000000a001400
332+2c01c800000000000900a3000100500000000000000000000000000000000000
333+00000000000000001300a3000001000001005000000000000000000000000000
334+00000000000000001c00a30001005000f3030000000000000000000000000000
335+00000000000000000108a40003000000ef030000000000000900000000000000
336+00000000000000006e6574207469746c650000001600a4000100500001005000
337+00000000000000000a0014002c01c800000000000900a4000100500000000000
338+00000000000000000000000000000000000000001300a4000001000001005000
339+00000000000000000000000000000000000000001c00a40001005000f3030000
340+00000000000000000000000000000000000000000108a50003000000ef030000
341+00000000090000000000000000000000000000006e6574207469746c65000000
342+1600a500010050000100500000000000000000000a0014002c01c80000000000
343+0900a50001005000000000000000000000000000000000000000000000000000
344+1300a50000010000010050000000000000000000000000000000000000000000
345+1c00a50001005000f30300000000000000000000000000000000000000000000
346+0108a60003000000ef0300000000000009000000000000000000000000000000
347+6e6574207469746c650000001600a60001005000010050000000000000000000
348+0a0014002c01c800000000000900a60001005000000000000000000000000000
349+0000000000000000000000001300a60000010000010050000000000000000000
350+0000000000000000000000001c00a60001005000f30300000000000000000000
351+0000000000000000000000000108a70003000000ef0300000000000009000000
352+0000000000000000000000006e6574207469746c650000001600a70001005000
353+0100500000000000000000000a0014002c01c800000000000900a70001005000
354+0000000000000000000000000000000000000000000000001300a70000010000
355+0100500000000000000000000000000000000000000000001c00a70001005000
356+f303000000000000000000000000000000000000000000000108a80003000000
357+ef03000000000000090000000000000000000000000000006e6574207469746c
358+650000001600a800010050000100500000000000000000000a0014002c01c800
359+000000000900a800010050000000000000000000000000000000000000000000
360+000000001300a800000100000100500000000000000000000000000000000000
361+000000001c00a80001005000f303000000000000000000000000000000000000
362+000000000108a90003000000ef03000000000000090000000000000000000000
363+000000006e6574207469746c650000001600a900010050000100500000000000
364+000000000a0014002c01c800000000000900a900010050000000000000000000
365+000000000000000000000000000000001300a900000100000100500000000000
366+000000000000000000000000000000001c00a90001005000f303000000000000
367+000000000000000000000000000000000108aa0003000000ef03000000000000
368+090000000000000000000000000000006e6574207469746c650000001600aa00
369+010050000100500000000000000000000a0014002c01c800000000000900aa00
370+010050000000000000000000000000000000000000000000000000001300aa00
371+000100000100500000000000000000000000000000000000000000001c00aa00
372+01005000f303000000000000000000000000000000000000000000000108ab00
373+03000000ef03000000000000090000000000000000000000000000006e657420
374+7469746c650000001600ab00010050000100500000000000000000000a001400
375+2c01c800000000000900ab000100500000000000000000000000000000000000
376+00000000000000001300ab000001000001005000000000000000000000000000
377+00000000000000001c00ab0001005000f3030000000000000000000000000000
378+00000000000000000108ac0003000000ef030000000000000900000000000000
379+00000000000000006e6574207469746c650000001600ac000100500001005000
380+00000000000000000a0014002c01c800000000000900ac000100500000000000
381+00000000000000000000000000000000000000001300ac000001000001005000
382+00000000000000000000000000000000000000001c00ac0001005000f3030000
383+00000000000000000000000000000000000000000108ad0003000000ef030000
384+00000000090000000000000000000000000000006e6574207469746c65000000
385+1600ad00010050000100500000000000000000000a0014002c01c80000000000
386+0900ad0001005000000000000000000000000000000000000000000000000000
387+1300ad0000010000010050000000000000000000000000000000000000000000
388+1c00ad0001005000f30300000000000000000000000000000000000000000000
389+0108ae0003000000ef0300000000000009000000000000000000000000000000
390+6e6574207469746c650000001600ae0001005000010050000000000000000000
391+0a0014002c01c800000000000900ae0001005000000000000000000000000000
392+0000000000000000000000001300ae0000010000010050000000000000000000
393+0000000000000000000000001c00ae0001005000f30300000000000000000000
394+0000000000000000000000000108af0003000000ef0300000000000009000000
395+0000000000000000000000006e6574207469746c650000001600af0001005000
396+0100500000000000000000000a0014002c01c800000000000900af0001005000
397+0000000000000000000000000000000000000000000000001300af0000010000
398+0100500000000000000000000000000000000000000000001c00af0001005000
399+f303000000000000000000000000000000000000000000000108b00003000000
400+ef03000000000000090000000000000000000000000000006e6574207469746c
401+650000001600b000010050000100500000000000000000000a0014002c01c800
402+000000000900b000010050000000000000000000000000000000000000000000
403+000000001300b000000100000100500000000000000000000000000000000000
404+000000001c00b00001005000f303000000000000000000000000000000000000
405+000000000108b10003000000ef03000000000000090000000000000000000000
406+000000006e6574207469746c650000001600b100010050000100500000000000
407+000000000a0014002c01c800000000000900b100010050000000000000000000
408+000000000000000000000000000000001300b100000100000100500000000000
409+000000000000000000000000000000001c00b10001005000f303000000000000
410+000000000000000000000000000000000108b20003000000ef03000000000000
411+090000000000000000000000000000006e6574207469746c650000001600b200
412+010050000100500000000000000000000a0014002c01c800000000000900b200
413+010050000000000000000000000000000000000000000000000000001300b200
414+000100000100500000000000000000000000000000000000000000001c00b200
415+01005000f303000000000000000000000000000000000000000000000108b300
416+03000000ef03000000000000090000000000000000000000000000006e657420
417+7469746c650000001600b300010050000100500000000000000000000a001400
418+2c01c800000000000900b3000100500000000000000000000000000000000000
419+00000000000000001300b3000001000001005000000000000000000000000000
420+00000000000000001c00b30001005000f3030000000000000000000000000000
421+00000000000000000108b40003000000ef030000000000000900000000000000
422+00000000000000006e6574207469746c650000001600b4000100500001005000
423+00000000000000000a0014002c01c800000000000900b4000100500000000000
424+00000000000000000000000000000000000000001300b4000001000001005000
425+00000000000000000000000000000000000000001c00b40001005000f3030000
426+00000000000000000000000000000000000000000108b50003000000ef030000
427+00000000090000000000000000000000000000006e6574207469746c65000000
428+1600b500010050000100500000000000000000000a0014002c01c80000000000
429+0900b50001005000000000000000000000000000000000000000000000000000
430+1300b50000010000010050000000000000000000000000000000000000000000
431+1c00b50001005000f30300000000000000000000000000000000000000000000
432+0108b60003000000ef0300000000000009000000000000000000000000000000
433+6e6574207469746c650000001600b60001005000010050000000000000000000
434+0a0014002c01c800000000000900b60001005000000000000000000000000000
435+0000000000000000000000001300b60000010000010050000000000000000000
436+0000000000000000000000001c00b60001005000f30300000000000000000000
437+0000000000000000000000000108b70003000000ef0300000000000009000000
438+0000000000000000000000006e6574207469746c650000001600b70001005000
439+0100500000000000000000000a0014002c01c800000000000900b70001005000
440+0000000000000000000000000000000000000000000000001300b70000010000
441+0100500000000000000000000000000000000000000000001c00b70001005000
442+f303000000000000000000000000000000000000000000000108b80003000000
443+ef03000000000000090000000000000000000000000000006e6574207469746c
444+650000001600b800010050000100500000000000000000000a0014002c01c800
445+000000000900b800010050000000000000000000000000000000000000000000
446+000000001300b800000100000100500000000000000000000000000000000000
447+000000001c00b80001005000f303000000000000000000000000000000000000
448+000000000108b90003000000ef03000000000000090000000000000000000000
449+000000006e6574207469746c650000001600b900010050000100500000000000
450+000000000a0014002c01c800000000000900b900010050000000000000000000
451+000000000000000000000000000000001300b900000100000100500000000000
452+000000000000000000000000000000001c00b90001005000f303000000000000
453+000000000000000000000000000000000108ba0003000000ef03000000000000
454+090000000000000000000000000000006e6574207469746c650000001600ba00
455+010050000100500000000000000000000a0014002c01c800000000000900ba00
456+010050000000000000000000000000000000000000000000000000001300ba00
457+000100000100500000000000000000000000000000000000000000001c00ba00
458+01005000f303000000000000000000000000000000000000000000000108bb00
459+03000000ef03000000000000090000000000000000000000000000006e657420
460+7469746c650000001600bb00010050000100500000000000000000000a001400
461+2c01c800000000000900bb000100500000000000000000000000000000000000
462+00000000000000001300bb000001000001005000000000000000000000000000
463+00000000000000001c00bb0001005000f3030000000000000000000000000000
464+00000000000000000108bc0003000000ef030000000000000900000000000000
465+00000000000000006e6574207469746c650000001600bc000100500001005000
466+00000000000000000a0014002c01c800000000000900bc000100500000000000
467+00000000000000000000000000000000000000001300bc000001000001005000
468+00000000000000000000000000000000000000001c00bc0001005000f3030000
469+00000000000000000000000000000000000000000108bd0003000000ef030000
470+00000000090000000000000000000000000000006e6574207469746c65000000
471+1600bd00010050000100500000000000000000000a0014002c01c80000000000
472+0900bd0001005000000000000000000000000000000000000000000000000000
473+1300bd0000010000010050000000000000000000000000000000000000000000
474+1c00bd0001005000f30300000000000000000000000000000000000000000000
475+0108be0003000000ef0300000000000009000000000000000000000000000000
476+6e6574207469746c650000001600be0001005000010050000000000000000000
477+0a0014002c01c800000000000900be0001005000000000000000000000000000
478+0000000000000000000000001300be0000010000010050000000000000000000
479+0000000000000000000000001c00be0001005000f30300000000000000000000
480+0000000000000000000000000108bf0003000000ef0300000000000009000000
481+0000000000000000000000006e6574207469746c650000001600bf0001005000
482+0100500000000000000000000a0014002c01c800000000000900bf0001005000
483+0000000000000000000000000000000000000000000000001300bf0000010000
484+0100500000000000000000000000000000000000000000001c00bf0001005000
485+f303000000000000000000000000000000000000000000000108c00003000000
486+ef03000000000000090000000000000000000000000000006e6574207469746c
487+650000001600c000010050000100500000000000000000000a0014002c01c800
488+000000000900c000010050000000000000000000000000000000000000000000
489+000000001300c000000100000100500000000000000000000000000000000000
490+000000001c00c00001005000f303000000000000000000000000000000000000
491+000000000108c10003000000ef03000000000000090000000000000000000000
492+000000006e6574207469746c650000001600c100010050000100500000000000
493+000000000a0014002c01c800000000000900c100010050000000000000000000
494+000000000000000000000000000000001300c100000100000100500000000000
495+000000000000000000000000000000001c00c10001005000f303000000000000
496+000000000000000000000000000000000108c20003000000ef03000000000000
497+090000000000000000000000000000006e6574207469746c650000001600c200
498+010050000100500000000000000000000a0014002c01c800000000000900c200
499+010050000000000000000000000000000000000000000000000000001300c200
500+000100000100500000000000000000000000000000000000000000001c00c200
501+01005000f303000000000000000000000000000000000000000000000108c300
502+03000000ef03000000000000090000000000000000000000000000006e657420
503+7469746c650000001600c300010050000100500000000000000000000a001400
504+2c01c800000000000900c3000100500000000000000000000000000000000000
505+00000000000000001300c3000001000001005000000000000000000000000000
506+00000000000000001c00c30001005000f3030000000000000000000000000000
507+00000000000000000108c40003000000ef030000000000000900000000000000
508+00000000000000006e6574207469746c650000001600c4000100500001005000
509+00000000000000000a0014002c01c800000000000900c4000100500000000000
510+00000000000000000000000000000000000000001300c4000001000001005000
511+00000000000000000000000000000000000000001c00c40001005000f3030000
512+00000000000000000000000000000000000000000108c50003000000ef030000
513+00000000090000000000000000000000000000006e6574207469746c65000000
514+1600c500010050000100500000000000000000000a0014002c01c80000000000
515+0900c50001005000000000000000000000000000000000000000000000000000
516+1300c50000010000010050000000000000000000000000000000000000000000
517+1c00c50001005000f30300000000000000000000000000000000000000000000
518+0108c60003000000ef0300000000000009000000000000000000000000000000
519+6e6574207469746c650000001600c60001005000010050000000000000000000
520+0a0014002c01c800000000000900c60001005000000000000000000000000000
521+0000000000000000000000001300c60000010000010050000000000000000000
522+0000000000000000000000001c00c60001005000f30300000000000000000000
523+0000000000000000000000000108c70003000000ef0300000000000009000000
524+0000000000000000000000006e6574207469746c650000001600c70001005000
525+0100500000000000000000000a0014002c01c800000000000900c70001005000
526+0000000000000000000000000000000000000000000000001300c70000010000
527+0100500000000000000000000000000000000000000000001c00c70001005000
528+f303000000000000000000000000000000000000000000000108c80003000000
529+ef03000000000000090000000000000000000000000000006e6574207469746c
530+650000001600c800010050000100500000000000000000000a0014002c01c800
531+000000000900c800010050000000000000000000000000000000000000000000
532+000000001300c800000100000100500000000000000000000000000000000000
533+000000001c00c80001005000f303000000000000000000000000000000000000
534+000000000108c90003000000ef03000000000000090000000000000000000000
535+000000006e6574207469746c650000001600c900010050000100500000000000
536+000000000a0014002c01c800000000000900c900010050000000000000000000
537+000000000000000000000000000000001300c900000100000100500000000000
538+000000000000000000000000000000001c00c90001005000f303000000000000
539+000000000000000000000000000000000108ca0003000000ef03000000000000
540+090000000000000000000000000000006e6574207469746c650000001600ca00
541+010050000100500000000000000000000a0014002c01c800000000000900ca00
542+010050000000000000000000000000000000000000000000000000001300ca00
543+000100000100500000000000000000000000000000000000000000001c00ca00
544+01005000f303000000000000000000000000000000000000000000000108cb00
545+03000000ef03000000000000090000000000000000000000000000006e657420
546+7469746c650000001600cb00010050000100500000000000000000000a001400
547+2c01c800000000000900cb000100500000000000000000000000000000000000
548+00000000000000001300cb000001000001005000000000000000000000000000
549+00000000000000001c00cb0001005000f3030000000000000000000000000000
550+00000000000000000108cc0003000000ef030000000000000900000000000000
551+00000000000000006e6574207469746c650000001600cc000100500001005000
552+00000000000000000a0014002c01c800000000000900cc000100500000000000
553+00000000000000000000000000000000000000001300cc000001000001005000
554+00000000000000000000000000000000000000001c00cc0001005000f3030000
555+00000000000000000000000000000000000000000108cd0003000000ef030000
556+00000000090000000000000000000000000000006e6574207469746c65000000
557+1600cd00010050000100500000000000000000000a0014002c01c80000000000
558+0900cd0001005000000000000000000000000000000000000000000000000000
559+1300cd0000010000010050000000000000000000000000000000000000000000
560+1c00cd0001005000f30300000000000000000000000000000000000000000000
561+0108ce0003000000ef0300000000000009000000000000000000000000000000
562+6e6574207469746c650000001600ce0001005000010050000000000000000000
563+0a0014002c01c800000000000900ce0001005000000000000000000000000000
564+0000000000000000000000001300ce0000010000010050000000000000000000
565+0000000000000000000000001c00ce0001005000f30300000000000000000000
566+0000000000000000000000000108cf0003000000ef0300000000000009000000
567+0000000000000000000000006e6574207469746c650000001600cf0001005000
568+0100500000000000000000000a0014002c01c800000000000900cf0001005000
569+0000000000000000000000000000000000000000000000001300cf0000010000
570+0100500000000000000000000000000000000000000000001c00cf0001005000
571+f303000000000000000000000000000000000000000000000108d00003000000
572+ef03000000000000090000000000000000000000000000006e6574207469746c
573+650000001600d000010050000100500000000000000000000a0014002c01c800
574+000000000900d000010050000000000000000000000000000000000000000000
575+000000001300d000000100000100500000000000000000000000000000000000
576+000000001c00d00001005000f303000000000000000000000000000000000000
577+000000000108d10003000000ef03000000000000090000000000000000000000
578+000000006e6574207469746c650000001600d100010050000100500000000000
579+000000000a0014002c01c800000000000900d100010050000000000000000000
580+000000000000000000000000000000001300d100000100000100500000000000
581+000000000000000000000000000000001c00d10001005000f303000000000000
582+000000000000000000000000000000000108d20003000000ef03000000000000
583+090000000000000000000000000000006e6574207469746c650000001600d200
584+010050000100500000000000000000000a0014002c01c800000000000900d200
585+010050000000000000000000000000000000000000000000000000001300d200
586+000100000100500000000000000000000000000000000000000000001c00d200
587+01005000f303000000000000000000000000000000000000000000000108d300
588+03000000ef03000000000000090000000000000000000000000000006e657420
589+7469746c650000001600d300010050000100500000000000000000000a001400
590+2c01c800000000000900d3000100500000000000000000000000000000000000
591+00000000000000001300d3000001000001005000000000000000000000000000
592+00000000000000001c00d30001005000f3030000000000000000000000000000
593+00000000000000000108d40003000000ef030000000000000900000000000000
594+00000000000000006e6574207469746c650000001600d4000100500001005000
595+00000000000000000a0014002c01c800000000000900d4000100500000000000
596+00000000000000000000000000000000000000001300d4000001000001005000
597+00000000000000000000000000000000000000001c00d40001005000f3030000
598+00000000000000000000000000000000000000000108d50003000000ef030000
599+00000000090000000000000000000000000000006e6574207469746c65000000
600+1600d500010050000100500000000000000000000a0014002c01c80000000000
601+0900d50001005000000000000000000000000000000000000000000000000000
602+1300d50000010000010050000000000000000000000000000000000000000000
603+1c00d50001005000f30300000000000000000000000000000000000000000000
604+0108d60003000000ef0300000000000009000000000000000000000000000000
605+6e6574207469746c650000001600d60001005000010050000000000000000000
606+0a0014002c01c800000000000900d60001005000000000000000000000000000
607+0000000000000000000000001300d60000010000010050000000000000000000
608+0000000000000000000000001c00d60001005000f30300000000000000000000
609+0000000000000000000000000108d70003000000ef0300000000000009000000
610+0000000000000000000000006e6574207469746c650000001600d70001005000
611+0100500000000000000000000a0014002c01c800000000000900d70001005000
612+0000000000000000000000000000000000000000000000001300d70000010000
613+0100500000000000000000000000000000000000000000001c00d70001005000
614+f303000000000000000000000000000000000000000000000108d80003000000
615+ef03000000000000090000000000000000000000000000006e6574207469746c
616+650000001600d800010050000100500000000000000000000a0014002c01c800
617+000000000900d800010050000000000000000000000000000000000000000000
618+000000001300d800000100000100500000000000000000000000000000000000
619+000000001c00d80001005000f303000000000000000000000000000000000000
620+000000000108d90003000000ef03000000000000090000000000000000000000
621+000000006e6574207469746c650000001600d900010050000100500000000000
622+000000000a0014002c01c800000000000900d900010050000000000000000000
623+000000000000000000000000000000001300d900000100000100500000000000
624+000000000000000000000000000000001c00d90001005000f303000000000000
625+000000000000000000000000000000000108da0003000000ef03000000000000
626+090000000000000000000000000000006e6574207469746c650000001600da00
627+010050000100500000000000000000000a0014002c01c800000000000900da00
628+010050000000000000000000000000000000000000000000000000001300da00
629+000100000100500000000000000000000000000000000000000000001c00da00
630+01005000f303000000000000000000000000000000000000000000000108db00
631+03000000ef03000000000000090000000000000000000000000000006e657420
632+7469746c650000001600db00010050000100500000000000000000000a001400
633+2c01c800000000000900db000100500000000000000000000000000000000000
634+00000000000000001300db000001000001005000000000000000000000000000
635+00000000000000001c00db0001005000f3030000000000000000000000000000
636+00000000000000000108dc0003000000ef030000000000000900000000000000
637+00000000000000006e6574207469746c650000001600dc000100500001005000
638+00000000000000000a0014002c01c800000000000900dc000100500000000000
639+00000000000000000000000000000000000000001300dc000001000001005000
640+00000000000000000000000000000000000000001c00dc0001005000f3030000
641+00000000000000000000000000000000000000000108dd0003000000ef030000
642+00000000090000000000000000000000000000006e6574207469746c65000000
643+1600dd00010050000100500000000000000000000a0014002c01c80000000000
644+0900dd0001005000000000000000000000000000000000000000000000000000
645+1300dd0000010000010050000000000000000000000000000000000000000000
646+1c00dd0001005000f30300000000000000000000000000000000000000000000
647+0108de0003000000ef0300000000000009000000000000000000000000000000
648+6e6574207469746c650000001600de0001005000010050000000000000000000
649+0a0014002c01c800000000000900de0001005000000000000000000000000000
650+0000000000000000000000001300de0000010000010050000000000000000000
651+0000000000000000000000001c00de0001005000f30300000000000000000000
652+0000000000000000000000000108df0003000000ef0300000000000009000000
653+0000000000000000000000006e6574207469746c650000001600df0001005000
654+0100500000000000000000000a0014002c01c800000000000900df0001005000
655+0000000000000000000000000000000000000000000000001300df0000010000
656+0100500000000000000000000000000000000000000000001c00df0001005000
657+f303000000000000000000000000000000000000000000000108e00003000000
658+ef03000000000000090000000000000000000000000000006e6574207469746c
659+650000001600e000010050000100500000000000000000000a0014002c01c800
660+000000000900e000010050000000000000000000000000000000000000000000
661+000000001300e000000100000100500000000000000000000000000000000000
662+000000001c00e00001005000f303000000000000000000000000000000000000
663+000000000108e10003000000ef03000000000000090000000000000000000000
664+000000006e6574207469746c650000001600e100010050000100500000000000
665+000000000a0014002c01c800000000000900e100010050000000000000000000
666+000000000000000000000000000000001300e100000100000100500000000000
667+000000000000000000000000000000001c00e10001005000f303000000000000
668+000000000000000000000000000000000108e20003000000ef03000000000000
669+090000000000000000000000000000006e6574207469746c650000001600e200
670+010050000100500000000000000000000a0014002c01c800000000000900e200
671+010050000000000000000000000000000000000000000000000000001300e200
672+000100000100500000000000000000000000000000000000000000001c00e200
673+01005000f303000000000000000000000000000000000000000000000108e300
674+03000000ef03000000000000090000000000000000000000000000006e657420
675+7469746c650000001600e300010050000100500000000000000000000a001400
676+2c01c800000000000900e3000100500000000000000000000000000000000000
677+00000000000000001300e3000001000001005000000000000000000000000000
678+00000000000000001c00e30001005000f3030000000000000000000000000000
679+00000000000000000108e40003000000ef030000000000000900000000000000
680+00000000000000006e6574207469746c650000001600e4000100500001005000
681+00000000000000000a0014002c01c800000000000900e4000100500000000000
682+00000000000000000000000000000000000000001300e4000001000001005000
683+00000000000000000000000000000000000000001c00e40001005000f3030000
684+00000000000000000000000000000000000000000108e50003000000ef030000
685+00000000090000000000000000000000000000006e6574207469746c65000000
686+1600e500010050000100500000000000000000000a0014002c01c80000000000
687+0900e50001005000000000000000000000000000000000000000000000000000
688+1300e50000010000010050000000000000000000000000000000000000000000
689+1c00e50001005000f30300000000000000000000000000000000000000000000
690+0108e60003000000ef0300000000000009000000000000000000000000000000
691+6e6574207469746c650000001600e60001005000010050000000000000000000
692+0a0014002c01c800000000000900e60001005000000000000000000000000000
693+0000000000000000000000001300e60000010000010050000000000000000000
694+0000000000000000000000001c00e60001005000f30300000000000000000000
695+0000000000000000000000000108e70003000000ef0300000000000009000000
696+0000000000000000000000006e6574207469746c650000001600e70001005000
697+0100500000000000000000000a0014002c01c800000000000900e70001005000
698+0000000000000000000000000000000000000000000000001300e70000010000
699+0100500000000000000000000000000000000000000000001c00e70001005000
700+f303000000000000000000000000000000000000000000000108e80003000000
701+ef03000000000000090000000000000000000000000000006e6574207469746c
702+650000001600e800010050000100500000000000000000000a0014002c01c800
703+000000000900e800010050000000000000000000000000000000000000000000
704+000000001300e800000100000100500000000000000000000000000000000000
705+000000001c00e80001005000f303000000000000000000000000000000000000
706+000000000108e90003000000ef03000000000000090000000000000000000000
707+000000006e6574207469746c650000001600e900010050000100500000000000
708+000000000a0014002c01c800000000000900e900010050000000000000000000
709+000000000000000000000000000000001300e900000100000100500000000000
710+000000000000000000000000000000001c00e90001005000f303000000000000
711+000000000000000000000000000000000108ea0003000000ef03000000000000
712+090000000000000000000000000000006e6574207469746c650000001600ea00
713+010050000100500000000000000000000a0014002c01c800000000000900ea00
714+010050000000000000000000000000000000000000000000000000001300ea00
715+000100000100500000000000000000000000000000000000000000001c00ea00
716+01005000f303000000000000000000000000000000000000000000000108eb00
717+03000000ef03000000000000090000000000000000000000000000006e657420
718+7469746c650000001600eb00010050000100500000000000000000000a001400
719+2c01c800000000000900eb000100500000000000000000000000000000000000
720+00000000000000001300eb000001000001005000000000000000000000000000
721+00000000000000001c00eb0001005000f3030000000000000000000000000000
722+00000000000000000108ec0003000000ef030000000000000900000000000000
723+00000000000000006e6574207469746c650000001600ec000100500001005000
724+00000000000000000a0014002c01c800000000000900ec000100500000000000
725+00000000000000000000000000000000000000001300ec000001000001005000
726+00000000000000000000000000000000000000001c00ec0001005000f3030000
727+00000000000000000000000000000000000000000108ed0003000000ef030000
728+00000000090000000000000000000000000000006e6574207469746c65000000
729+1600ed00010050000100500000000000000000000a0014002c01c80000000000
730+0900ed0001005000000000000000000000000000000000000000000000000000
731+1300ed0000010000010050000000000000000000000000000000000000000000
732+1c00ed0001005000f30300000000000000000000000000000000000000000000
733+0108ee0003000000ef0300000000000009000000000000000000000000000000
734+6e6574207469746c650000001600ee0001005000010050000000000000000000
735+0a0014002c01c800000000000900ee0001005000000000000000000000000000
736+0000000000000000000000001300ee0000010000010050000000000000000000
737+0000000000000000000000001c00ee0001005000f30300000000000000000000
738+0000000000000000000000000108ef0003000000ef0300000000000009000000
739+0000000000000000000000006e6574207469746c650000001600ef0001005000
740+0100500000000000000000000a0014002c01c800000000000900ef0001005000
741+0000000000000000000000000000000000000000000000001300ef0000010000
742+0100500000000000000000000000000000000000000000001c00ef0001005000
743+f303000000000000000000000000000000000000000000000108f00003000000
744+ef03000000000000090000000000000000000000000000006e6574207469746c
745+650000001600f000010050000100500000000000000000000a0014002c01c800
746+000000000900f000010050000000000000000000000000000000000000000000
747+000000001300f000000100000100500000000000000000000000000000000000
748+000000001c00f00001005000f303000000000000000000000000000000000000
749+000000000108f10003000000ef03000000000000090000000000000000000000
750+000000006e6574207469746c650000001600f100010050000100500000000000
751+000000000a0014002c01c800000000000900f100010050000000000000000000
752+000000000000000000000000000000001300f100000100000100500000000000
753+000000000000000000000000000000001c00f10001005000f303000000000000
754+000000000000000000000000000000000108f20003000000ef03000000000000
755+090000000000000000000000000000006e6574207469746c650000001600f200
756+010050000100500000000000000000000a0014002c01c800000000000900f200
757+010050000000000000000000000000000000000000000000000000001300f200
758+000100000100500000000000000000000000000000000000000000001c00f200
759+01005000f303000000000000000000000000000000000000000000000108f300
760+03000000ef03000000000000090000000000000000000000000000006e657420
761+7469746c650000001600f300010050000100500000000000000000000a001400
762+2c01c800000000000900f3000100500000000000000000000000000000000000
763+00000000000000001300f3000001000001005000000000000000000000000000
764+00000000000000001c00f30001005000f3030000000000000000000000000000
765+00000000000000000108f40003000000ef030000000000000900000000000000
766+00000000000000006e6574207469746c650000001600f4000100500001005000
767+00000000000000000a0014002c01c800000000000900f4000100500000000000
768+00000000000000000000000000000000000000001300f4000001000001005000
769+00000000000000000000000000000000000000001c00f40001005000f3030000
770+00000000000000000000000000000000000000000108f50003000000ef030000
771+00000000090000000000000000000000000000006e6574207469746c65000000
772+1600f500010050000100500000000000000000000a0014002c01c80000000000
773+0900f50001005000000000000000000000000000000000000000000000000000
774+1300f50000010000010050000000000000000000000000000000000000000000
775+1c00f50001005000f30300000000000000000000000000000000000000000000
776+0108f60003000000ef0300000000000009000000000000000000000000000000
777+6e6574207469746c650000001600f60001005000010050000000000000000000
778+0a0014002c01c800000000000900f60001005000000000000000000000000000
779+0000000000000000000000001300f60000010000010050000000000000000000
780+0000000000000000000000001c00f60001005000f30300000000000000000000
781+0000000000000000000000000108f70003000000ef0300000000000009000000
782+0000000000000000000000006e6574207469746c650000001600f70001005000
783+0100500000000000000000000a0014002c01c800000000000900f70001005000
784+0000000000000000000000000000000000000000000000001300f70000010000
785+0100500000000000000000000000000000000000000000001c00f70001005000
786+f303000000000000000000000000000000000000000000000108f80003000000
787+ef03000000000000090000000000000000000000000000006e6574207469746c
788+650000001600f800010050000100500000000000000000000a0014002c01c800
789+000000000900f800010050000000000000000000000000000000000000000000
790+000000001300f800000100000100500000000000000000000000000000000000
791+000000001c00f80001005000f303000000000000000000000000000000000000
792+000000000108f90003000000ef03000000000000090000000000000000000000
793+000000006e6574207469746c650000001600f900010050000100500000000000
794+000000000a0014002c01c800000000000900f900010050000000000000000000
795+000000000000000000000000000000001300f900000100000100500000000000
796+000000000000000000000000000000001c00f90001005000f303000000000000
797+000000000000000000000000000000000108fa0003000000ef03000000000000
798+090000000000000000000000000000006e6574207469746c650000001600fa00
799+010050000100500000000000000000000a0014002c01c800000000000900fa00
800+010050000000000000000000000000000000000000000000000000001300fa00
801+000100000100500000000000000000000000000000000000000000001c00fa00
802+01005000f303000000000000000000000000000000000000000000000108fb00
803+03000000ef03000000000000090000000000000000000000000000006e657420
804+7469746c650000001600fb00010050000100500000000000000000000a001400
805+2c01c800000000000900fb000100500000000000000000000000000000000000
806+00000000000000001300fb000001000001005000000000000000000000000000
807+00000000000000001c00fb0001005000f3030000000000000000000000000000
808+00000000000000000108fc0003000000ef030000000000000900000000000000
809+00000000000000006e6574207469746c650000001600fc000100500001005000
810+00000000000000000a0014002c01c800000000000900fc000100500000000000
811+00000000000000000000000000000000000000001300fc000001000001005000
812+00000000000000000000000000000000000000001c00fc0001005000f3030000
813+00000000000000000000000000000000000000000108fd0003000000ef030000
814+00000000090000000000000000000000000000006e6574207469746c65000000
815+1600fd00010050000100500000000000000000000a0014002c01c80000000000
816+0900fd0001005000000000000000000000000000000000000000000000000000
817+1300fd0000010000010050000000000000000000000000000000000000000000
818+1c00fd0001005000f30300000000000000000000000000000000000000000000
819+0108fe0003000000ef0300000000000009000000000000000000000000000000
820+6e6574207469746c650000001600fe0001005000010050000000000000000000
821+0a0014002c01c800000000000900fe0001005000000000000000000000000000
822+0000000000000000000000001300fe0000010000010050000000000000000000
823+0000000000000000000000001c00fe0001005000f30300000000000000000000
824+0000000000000000000000000108ff0003000000ef0300000000000009000000
825+0000000000000000000000006e6574207469746c650000001600ff0001005000
826+0100500000000000000000000a0014002c01c800000000000900ff0001005000
827+0000000000000000000000000000000000000000000000001300ff0000010000
828+0100500000000000000000000000000000000000000000001c00ff0001005000
829+f303000000000000000000000000000000000000000000000108000103000000
830+ef03000000000000090000000000000000000000000000006e6574207469746c
831+6500000016000001010050000100500000000000000000000a0014002c01c800
832+0000000009000001010050000000000000000000000000000000000000000000
833+0000000013000001000100000100500000000000000000000000000000000000
834+000000001c00000101005000f303000000000000000000000000000000000000
835+000000000108010103000000ef03000000000000090000000000000000000000
836+000000006e6574207469746c6500000016000101010050000100500000000000
837+000000000a0014002c01c8000000000009000101010050000000000000000000
838+0000000000000000000000000000000013000101000100000100500000000000
839+000000000000000000000000000000001c00010101005000f303000000000000
840+000000000000000000000000000000000108020103000000ef03000000000000
841+090000000000000000000000000000006e6574207469746c6500000016000201
842+010050000100500000000000000000000a0014002c01c8000000000009000201
843+0100500000000000000000000000000000000000000000000000000013000201
844+000100000100500000000000000000000000000000000000000000001c000201
845+01005000f3030000000000000000000000000000000000000000000001080301
846+03000000ef03000000000000090000000000000000000000000000006e657420
847+7469746c6500000016000301010050000100500000000000000000000a001400
848+2c01c80000000000090003010100500000000000000000000000000000000000
849+0000000000000000130003010001000001005000000000000000000000000000
850+00000000000000001c00030101005000f3030000000000000000000000000000
851+00000000000000000108040103000000ef030000000000000900000000000000
852+00000000000000006e6574207469746c65000000160004010100500001005000
853+00000000000000000a0014002c01c80000000000090004010100500000000000
854+0000000000000000000000000000000000000000130004010001000001005000
855+00000000000000000000000000000000000000001c00040101005000f3030000
856+00000000000000000000000000000000000000000108050103000000ef030000
857+00000000090000000000000000000000000000006e6574207469746c65000000
858+16000501010050000100500000000000000000000a0014002c01c80000000000
859+0900050101005000000000000000000000000000000000000000000000000000
860+1300050100010000010050000000000000000000000000000000000000000000
861+1c00050101005000f30300000000000000000000000000000000000000000000
862+0108060103000000ef0300000000000009000000000000000000000000000000
863+6e6574207469746c650000001600060101005000010050000000000000000000
864+0a0014002c01c800000000000900060101005000000000000000000000000000
865+0000000000000000000000001300060100010000010050000000000000000000
866+0000000000000000000000001c00060101005000f30300000000000000000000
867+0000000000000000000000000108070103000000ef0300000000000009000000
868+0000000000000000000000006e6574207469746c650000001600070101005000
869+0100500000000000000000000a0014002c01c800000000000900070101005000
870+0000000000000000000000000000000000000000000000001300070100010000
871+0100500000000000000000000000000000000000000000001c00070101005000
872+f303000000000000000000000000000000000000000000000108080103000000
873+ef03000000000000090000000000000000000000000000006e6574207469746c
874+6500000016000801010050000100500000000000000000000a0014002c01c800
875+0000000009000801010050000000000000000000000000000000000000000000
876+0000000013000801000100000100500000000000000000000000000000000000
877+000000001c00080101005000f303000000000000000000000000000000000000
878+000000000108090103000000ef03000000000000090000000000000000000000
879+000000006e6574207469746c6500000016000901010050000100500000000000
880+000000000a0014002c01c8000000000009000901010050000000000000000000
881+0000000000000000000000000000000013000901000100000100500000000000
882+000000000000000000000000000000001c00090101005000f303000000000000
883+0000000000000000000000000000000001080a0103000000ef03000000000000
884+090000000000000000000000000000006e6574207469746c6500000016000a01
885+010050000100500000000000000000000a0014002c01c8000000000009000a01
886+0100500000000000000000000000000000000000000000000000000013000a01
887+000100000100500000000000000000000000000000000000000000001c000a01
888+01005000f3030000000000000000000000000000000000000000000001080b01
889+03000000ef03000000000000090000000000000000000000000000006e657420
890+7469746c6500000016000b01010050000100500000000000000000000a001400
891+2c01c8000000000009000b010100500000000000000000000000000000000000
892+000000000000000013000b010001000001005000000000000000000000000000
893+00000000000000001c000b0101005000f3030000000000000000000000000000
894+000000000000000001080c0103000000ef030000000000000900000000000000
895+00000000000000006e6574207469746c6500000016000c010100500001005000
896+00000000000000000a0014002c01c8000000000009000c010100500000000000
897+000000000000000000000000000000000000000013000c010001000001005000
898+00000000000000000000000000000000000000001c000c0101005000f3030000
899+000000000000000000000000000000000000000001080d0103000000ef030000
900+00000000090000000000000000000000000000006e6574207469746c65000000
901+16000d01010050000100500000000000000000000a0014002c01c80000000000
902+09000d0101005000000000000000000000000000000000000000000000000000
903+13000d0100010000010050000000000000000000000000000000000000000000
904+1c000d0101005000f30300000000000000000000000000000000000000000000
905+01080e0103000000ef0300000000000009000000000000000000000000000000
906+6e6574207469746c6500000016000e0101005000010050000000000000000000
907+0a0014002c01c8000000000009000e0101005000000000000000000000000000
908+00000000000000000000000013000e0100010000010050000000000000000000
909+0000000000000000000000001c000e0101005000f30300000000000000000000
910+00000000000000000000000001080f0103000000ef0300000000000009000000
911+0000000000000000000000006e6574207469746c6500000016000f0101005000
912+0100500000000000000000000a0014002c01c8000000000009000f0101005000
913+00000000000000000000000000000000000000000000000013000f0100010000
914+0100500000000000000000000000000000000000000000001c000f0101005000
915+f303000000000000000000000000000000000000000000000108100103000000
916+ef03000000000000090000000000000000000000000000006e6574207469746c
917+6500000016001001010050000100500000000000000000000a0014002c01c800
918+0000000009001001010050000000000000000000000000000000000000000000
919+0000000013001001000100000100500000000000000000000000000000000000
920+000000001c00100101005000f303000000000000000000000000000000000000
921+000000000108110103000000ef03000000000000090000000000000000000000
922+000000006e6574207469746c6500000016001101010050000100500000000000
923+000000000a0014002c01c8000000000009001101010050000000000000000000
924+0000000000000000000000000000000013001101000100000100500000000000
925+000000000000000000000000000000001c00110101005000f303000000000000
926+000000000000000000000000000000000108120103000000ef03000000000000
927+090000000000000000000000000000006e6574207469746c6500000016001201
928+010050000100500000000000000000000a0014002c01c8000000000009001201
929+0100500000000000000000000000000000000000000000000000000013001201
930+000100000100500000000000000000000000000000000000000000001c001201
931+01005000f3030000000000000000000000000000000000000000000001081301
932+03000000ef03000000000000090000000000000000000000000000006e657420
933+7469746c6500000016001301010050000100500000000000000000000a001400
934+2c01c80000000000090013010100500000000000000000000000000000000000
935+0000000000000000130013010001000001005000000000000000000000000000
936+00000000000000001c00130101005000f3030000000000000000000000000000
937+00000000000000000108140103000000ef030000000000000900000000000000
938+00000000000000006e6574207469746c65000000160014010100500001005000
939+00000000000000000a0014002c01c80000000000090014010100500000000000
940+0000000000000000000000000000000000000000130014010001000001005000
941+00000000000000000000000000000000000000001c00140101005000f3030000
942+00000000000000000000000000000000000000000108150103000000ef030000
943+00000000090000000000000000000000000000006e6574207469746c65000000
944+16001501010050000100500000000000000000000a0014002c01c80000000000
945+0900150101005000000000000000000000000000000000000000000000000000
946+1300150100010000010050000000000000000000000000000000000000000000
947+1c00150101005000f30300000000000000000000000000000000000000000000
948+0108160103000000ef0300000000000009000000000000000000000000000000
949+6e6574207469746c650000001600160101005000010050000000000000000000
950+0a0014002c01c800000000000900160101005000000000000000000000000000
951+0000000000000000000000001300160100010000010050000000000000000000
952+0000000000000000000000001c00160101005000f30300000000000000000000
953+0000000000000000000000000108170103000000ef0300000000000009000000
954+0000000000000000000000006e6574207469746c650000001600170101005000
955+0100500000000000000000000a0014002c01c800000000000900170101005000
956+0000000000000000000000000000000000000000000000001300170100010000
957+0100500000000000000000000000000000000000000000001c00170101005000
958+f303000000000000000000000000000000000000000000000108180103000000
959+ef03000000000000090000000000000000000000000000006e6574207469746c
960+6500000016001801010050000100500000000000000000000a0014002c01c800
961+0000000009001801010050000000000000000000000000000000000000000000
962+0000000013001801000100000100500000000000000000000000000000000000
963+000000001c00180101005000f303000000000000000000000000000000000000
964+000000000108190103000000ef03000000000000090000000000000000000000
965+000000006e6574207469746c6500000016001901010050000100500000000000
966+000000000a0014002c01c8000000000009001901010050000000000000000000
967+0000000000000000000000000000000013001901000100000100500000000000
968+000000000000000000000000000000001c00190101005000f303000000000000
969+0000000000000000000000000000000001081a0103000000ef03000000000000
970+090000000000000000000000000000006e6574207469746c6500000016001a01
971+010050000100500000000000000000000a0014002c01c8000000000009001a01
972+0100500000000000000000000000000000000000000000000000000013001a01
973+000100000100500000000000000000000000000000000000000000001c001a01
974+01005000f3030000000000000000000000000000000000000000000001081b01
975+03000000ef03000000000000090000000000000000000000000000006e657420
976+7469746c6500000016001b01010050000100500000000000000000000a001400
977+2c01c8000000000009001b010100500000000000000000000000000000000000
978+000000000000000013001b010001000001005000000000000000000000000000
979+00000000000000001c001b0101005000f3030000000000000000000000000000
980+000000000000000001081c0103000000ef030000000000000900000000000000
981+00000000000000006e6574207469746c6500000016001c010100500001005000
982+00000000000000000a0014002c01c8000000000009001c010100500000000000
983+000000000000000000000000000000000000000013001c010001000001005000
984+00000000000000000000000000000000000000001c001c0101005000f3030000
985+000000000000000000000000000000000000000001081d0103000000ef030000
986+00000000090000000000000000000000000000006e6574207469746c65000000
987+16001d01010050000100500000000000000000000a0014002c01c80000000000
988+09001d0101005000000000000000000000000000000000000000000000000000
989+13001d0100010000010050000000000000000000000000000000000000000000
990+1c001d0101005000f30300000000000000000000000000000000000000000000
991+01081e0103000000ef0300000000000009000000000000000000000000000000
992+6e6574207469746c6500000016001e0101005000010050000000000000000000
993+0a0014002c01c8000000000009001e0101005000000000000000000000000000
994+00000000000000000000000013001e0100010000010050000000000000000000
995+0000000000000000000000001c001e0101005000f30300000000000000000000
996+00000000000000000000000001081f0103000000ef0300000000000009000000
997+0000000000000000000000006e6574207469746c6500000016001f0101005000
998+0100500000000000000000000a0014002c01c8000000000009001f0101005000
999+00000000000000000000000000000000000000000000000013001f0100010000
1000+0100500000000000000000000000000000000000000000001c001f0101005000
1001+f303000000000000000000000000000000000000000000000108200103000000
1002+ef03000000000000090000000000000000000000000000006e6574207469746c
1003+6500000016002001010050000100500000000000000000000a0014002c01c800
1004+0000000009002001010050000000000000000000000000000000000000000000
1005+0000000013002001000100000100500000000000000000000000000000000000
1006+000000001c00200101005000f303000000000000000000000000000000000000
1007+000000000108210103000000ef03000000000000090000000000000000000000
1008+000000006e6574207469746c6500000016002101010050000100500000000000
1009+000000000a0014002c01c8000000000009002101010050000000000000000000
1010+0000000000000000000000000000000013002101000100000100500000000000
1011+000000000000000000000000000000001c00210101005000f303000000000000
1012+000000000000000000000000000000000108220103000000ef03000000000000
1013+090000000000000000000000000000006e6574207469746c6500000016002201
1014+010050000100500000000000000000000a0014002c01c8000000000009002201
1015+0100500000000000000000000000000000000000000000000000000013002201
1016+000100000100500000000000000000000000000000000000000000001c002201
1017+01005000f3030000000000000000000000000000000000000000000001082301
1018+03000000ef03000000000000090000000000000000000000000000006e657420
1019+7469746c6500000016002301010050000100500000000000000000000a001400
1020+2c01c80000000000090023010100500000000000000000000000000000000000
1021+0000000000000000130023010001000001005000000000000000000000000000
1022+00000000000000001c00230101005000f3030000000000000000000000000000
1023+00000000000000000108240103000000ef030000000000000900000000000000
1024+00000000000000006e6574207469746c65000000160024010100500001005000
1025+00000000000000000a0014002c01c80000000000090024010100500000000000
1026+0000000000000000000000000000000000000000130024010001000001005000
1027+00000000000000000000000000000000000000001c00240101005000f3030000
1028+00000000000000000000000000000000000000000108250103000000ef030000
1029+00000000090000000000000000000000000000006e6574207469746c65000000
1030+16002501010050000100500000000000000000000a0014002c01c80000000000
1031+0900250101005000000000000000000000000000000000000000000000000000
1032+1300250100010000010050000000000000000000000000000000000000000000
1033+1c00250101005000f30300000000000000000000000000000000000000000000
1034+0108260103000000ef0300000000000009000000000000000000000000000000
1035+6e6574207469746c650000001600260101005000010050000000000000000000
1036+0a0014002c01c800000000000900260101005000000000000000000000000000
1037+0000000000000000000000001300260100010000010050000000000000000000
1038+0000000000000000000000001c00260101005000f30300000000000000000000
1039+0000000000000000000000000108270103000000ef0300000000000009000000
1040+0000000000000000000000006e6574207469746c650000001600270101005000
1041+0100500000000000000000000a0014002c01c800000000000900270101005000
1042+0000000000000000000000000000000000000000000000001300270100010000
1043+0100500000000000000000000000000000000000000000001c00270101005000
1044+f303000000000000000000000000000000000000000000000108280103000000
1045+ef03000000000000090000000000000000000000000000006e6574207469746c
1046+6500000016002801010050000100500000000000000000000a0014002c01c800
1047+0000000009002801010050000000000000000000000000000000000000000000
1048+0000000013002801000100000100500000000000000000000000000000000000
1049+000000001c00280101005000f303000000000000000000000000000000000000
1050+000000000108290103000000ef03000000000000090000000000000000000000
1051+000000006e6574207469746c6500000016002901010050000100500000000000
1052+000000000a0014002c01c8000000000009002901010050000000000000000000
1053+0000000000000000000000000000000013002901000100000100500000000000
1054+000000000000000000000000000000001c00290101005000f303000000000000
1055+0000000000000000000000000000000001082a0103000000ef03000000000000
1056+090000000000000000000000000000006e6574207469746c6500000016002a01
1057+010050000100500000000000000000000a0014002c01c8000000000009002a01
1058+0100500000000000000000000000000000000000000000000000000013002a01
1059+000100000100500000000000000000000000000000000000000000001c002a01
1060+01005000f3030000000000000000000000000000000000000000000001082b01
1061+03000000ef03000000000000090000000000000000000000000000006e657420
1062+7469746c6500000016002b01010050000100500000000000000000000a001400
1063+2c01c8000000000009002b010100500000000000000000000000000000000000
1064+000000000000000013002b010001000001005000000000000000000000000000
1065+00000000000000001c002b0101005000f3030000000000000000000000000000
1066+000000000000000001082c0103000000ef030000000000000900000000000000
1067+00000000000000006e6574207469746c6500000016002c010100500001005000
1068+00000000000000000a0014002c01c8000000000009002c010100500000000000
1069+000000000000000000000000000000000000000013002c010001000001005000
1070+00000000000000000000000000000000000000001c002c0101005000f3030000
1071+000000000000000000000000000000000000000001082d0103000000ef030000
1072+00000000090000000000000000000000000000006e6574207469746c65000000
1073+16002d01010050000100500000000000000000000a0014002c01c80000000000
1074+09002d0101005000000000000000000000000000000000000000000000000000
1075+13002d0100010000010050000000000000000000000000000000000000000000
1076+1c002d0101005000f30300000000000000000000000000000000000000000000
1077+01082e0103000000ef0300000000000009000000000000000000000000000000
1078+6e6574207469746c6500000016002e0101005000010050000000000000000000
1079+0a0014002c01c8000000000009002e0101005000000000000000000000000000
1080+00000000000000000000000013002e0100010000010050000000000000000000
1081+0000000000000000000000001c002e0101005000f30300000000000000000000
1082+00000000000000000000000001082f0103000000ef0300000000000009000000
1083+0000000000000000000000006e6574207469746c6500000016002f0101005000
1084+0100500000000000000000000a0014002c01c8000000000009002f0101005000
1085+00000000000000000000000000000000000000000000000013002f0100010000
1086+0100500000000000000000000000000000000000000000001c002f0101005000
1087+f303000000000000000000000000000000000000000000000108300103000000
1088+ef03000000000000090000000000000000000000000000006e6574207469746c
1089+6500000016003001010050000100500000000000000000000a0014002c01c800
1090+0000000009003001010050000000000000000000000000000000000000000000
1091+0000000013003001000100000100500000000000000000000000000000000000
1092+000000001c00300101005000f303000000000000000000000000000000000000
1093+000000000108310103000000ef03000000000000090000000000000000000000
1094+000000006e6574207469746c6500000016003101010050000100500000000000
1095+000000000a0014002c01c8000000000009003101010050000000000000000000
1096+0000000000000000000000000000000013003101000100000100500000000000
1097+000000000000000000000000000000001c00310101005000f303000000000000
1098+000000000000000000000000000000000108320103000000ef03000000000000
1099+090000000000000000000000000000006e6574207469746c6500000016003201
1100+010050000100500000000000000000000a0014002c01c8000000000009003201
1101+0100500000000000000000000000000000000000000000000000000013003201
1102+000100000100500000000000000000000000000000000000000000001c003201
1103+01005000f3030000000000000000000000000000000000000000000001083301
1104+03000000ef03000000000000090000000000000000000000000000006e657420
1105+7469746c6500000016003301010050000100500000000000000000000a001400
1106+2c01c80000000000090033010100500000000000000000000000000000000000
1107+0000000000000000130033010001000001005000000000000000000000000000
1108+00000000000000001c00330101005000f3030000000000000000000000000000
1109+00000000000000000108340103000000ef030000000000000900000000000000
1110+00000000000000006e6574207469746c65000000160034010100500001005000
1111+00000000000000000a0014002c01c80000000000090034010100500000000000
1112+0000000000000000000000000000000000000000130034010001000001005000
1113+00000000000000000000000000000000000000001c00340101005000f3030000
1114+00000000000000000000000000000000000000000108350103000000ef030000
1115+00000000090000000000000000000000000000006e6574207469746c65000000
1116+16003501010050000100500000000000000000000a0014002c01c80000000000
1117+0900350101005000000000000000000000000000000000000000000000000000
1118+1300350100010000010050000000000000000000000000000000000000000000
1119+1c00350101005000f30300000000000000000000000000000000000000000000
1120+0108360103000000ef0300000000000009000000000000000000000000000000
1121+6e6574207469746c650000001600360101005000010050000000000000000000
1122+0a0014002c01c800000000000900360101005000000000000000000000000000
1123+0000000000000000000000001300360100010000010050000000000000000000
1124+0000000000000000000000001c00360101005000f30300000000000000000000
1125+0000000000000000000000000108370103000000ef0300000000000009000000
1126+0000000000000000000000006e6574207469746c650000001600370101005000
1127+0100500000000000000000000a0014002c01c800000000000900370101005000
1128+0000000000000000000000000000000000000000000000001300370100010000
1129+0100500000000000000000000000000000000000000000001c00370101005000
1130+f303000000000000000000000000000000000000000000000108380103000000
1131+ef03000000000000090000000000000000000000000000006e6574207469746c
1132+6500000016003801010050000100500000000000000000000a0014002c01c800
1133+0000000009003801010050000000000000000000000000000000000000000000
1134+0000000013003801000100000100500000000000000000000000000000000000
1135+000000001c00380101005000f303000000000000000000000000000000000000
1136+000000000108390103000000ef03000000000000090000000000000000000000
1137+000000006e6574207469746c6500000016003901010050000100500000000000
1138+000000000a0014002c01c8000000000009003901010050000000000000000000
1139+0000000000000000000000000000000013003901000100000100500000000000
1140+000000000000000000000000000000001c00390101005000f303000000000000
1141+0000000000000000000000000000000001083a0103000000ef03000000000000
1142+090000000000000000000000000000006e6574207469746c6500000016003a01
1143+010050000100500000000000000000000a0014002c01c8000000000009003a01
1144+0100500000000000000000000000000000000000000000000000000013003a01
1145+000100000100500000000000000000000000000000000000000000001c003a01
1146+01005000f3030000000000000000000000000000000000000000000001083b01
1147+03000000ef03000000000000090000000000000000000000000000006e657420
1148+7469746c6500000016003b01010050000100500000000000000000000a001400
1149+2c01c8000000000009003b010100500000000000000000000000000000000000
1150+000000000000000013003b010001000001005000000000000000000000000000
1151+00000000000000001c003b0101005000f3030000000000000000000000000000
1152+000000000000000001083c0103000000ef030000000000000900000000000000
1153+00000000000000006e6574207469746c6500000016003c010100500001005000
1154+00000000000000000a0014002c01c8000000000009003c010100500000000000
1155+000000000000000000000000000000000000000013003c010001000001005000
1156+00000000000000000000000000000000000000001c003c0101005000f3030000
1157+000000000000000000000000000000000000000001083d0103000000ef030000
1158+00000000090000000000000000000000000000006e6574207469746c65000000
1159+16003d01010050000100500000000000000000000a0014002c01c80000000000
1160+09003d0101005000000000000000000000000000000000000000000000000000
1161+13003d0100010000010050000000000000000000000000000000000000000000
1162+1c003d0101005000f30300000000000000000000000000000000000000000000
1163+01083e0103000000ef0300000000000009000000000000000000000000000000
1164+6e6574207469746c6500000016003e0101005000010050000000000000000000
1165+0a0014002c01c8000000000009003e0101005000000000000000000000000000
1166+00000000000000000000000013003e0100010000010050000000000000000000
1167+0000000000000000000000001c003e0101005000f30300000000000000000000
1168+00000000000000000000000001083f0103000000ef0300000000000009000000
1169+0000000000000000000000006e6574207469746c6500000016003f0101005000
1170+0100500000000000000000000a0014002c01c8000000000009003f0101005000
1171+00000000000000000000000000000000000000000000000013003f0100010000
1172+0100500000000000000000000000000000000000000000001c003f0101005000
1173+f303000000000000000000000000000000000000000000000108400103000000
1174+ef03000000000000090000000000000000000000000000006e6574207469746c
1175+6500000016004001010050000100500000000000000000000a0014002c01c800
1176+0000000009004001010050000000000000000000000000000000000000000000
1177+0000000013004001000100000100500000000000000000000000000000000000
1178+000000001c00400101005000f303000000000000000000000000000000000000
1179+000000000108410103000000ef03000000000000090000000000000000000000
1180+000000006e6574207469746c6500000016004101010050000100500000000000
1181+000000000a0014002c01c8000000000009004101010050000000000000000000
1182+0000000000000000000000000000000013004101000100000100500000000000
1183+000000000000000000000000000000001c00410101005000f303000000000000
1184+000000000000000000000000000000000108420103000000ef03000000000000
1185+090000000000000000000000000000006e6574207469746c6500000016004201
1186+010050000100500000000000000000000a0014002c01c8000000000009004201
1187+0100500000000000000000000000000000000000000000000000000013004201
1188+000100000100500000000000000000000000000000000000000000001c004201
1189+01005000f3030000000000000000000000000000000000000000000001084301
1190+03000000ef03000000000000090000000000000000000000000000006e657420
1191+7469746c6500000016004301010050000100500000000000000000000a001400
1192+2c01c80000000000090043010100500000000000000000000000000000000000
1193+0000000000000000130043010001000001005000000000000000000000000000
1194+00000000000000001c00430101005000f3030000000000000000000000000000
1195+00000000000000000108440103000000ef030000000000000900000000000000
1196+00000000000000006e6574207469746c65000000160044010100500001005000
1197+00000000000000000a0014002c01c80000000000090044010100500000000000
1198+0000000000000000000000000000000000000000130044010001000001005000
1199+00000000000000000000000000000000000000001c00440101005000f3030000
1200+00000000000000000000000000000000000000000108450103000000ef030000
1201+00000000090000000000000000000000000000006e6574207469746c65000000
1202+16004501010050000100500000000000000000000a0014002c01c80000000000
1203+0900450101005000000000000000000000000000000000000000000000000000
1204+1300450100010000010050000000000000000000000000000000000000000000
1205+1c00450101005000f30300000000000000000000000000000000000000000000
1206+0108460103000000ef0300000000000009000000000000000000000000000000
1207+6e6574207469746c650000001600460101005000010050000000000000000000
1208+0a0014002c01c800000000000900460101005000000000000000000000000000
1209+0000000000000000000000001300460100010000010050000000000000000000
1210+0000000000000000000000001c00460101005000f30300000000000000000000
1211+0000000000000000000000000108470103000000ef0300000000000009000000
1212+0000000000000000000000006e6574207469746c650000001600470101005000
1213+0100500000000000000000000a0014002c01c800000000000900470101005000
1214+0000000000000000000000000000000000000000000000001300470100010000
1215+0100500000000000000000000000000000000000000000001c00470101005000
1216+f303000000000000000000000000000000000000000000000108480103000000
1217+ef03000000000000090000000000000000000000000000006e6574207469746c
1218+6500000016004801010050000100500000000000000000000a0014002c01c800
1219+0000000009004801010050000000000000000000000000000000000000000000
1220+0000000013004801000100000100500000000000000000000000000000000000
1221+000000001c00480101005000f303000000000000000000000000000000000000
1222+000000000108490103000000ef03000000000000090000000000000000000000
1223+000000006e6574207469746c6500000016004901010050000100500000000000
1224+000000000a0014002c01c8000000000009004901010050000000000000000000
1225+0000000000000000000000000000000013004901000100000100500000000000
1226+000000000000000000000000000000001c00490101005000f303000000000000
1227+0000000000000000000000000000000001084a0103000000ef03000000000000
1228+090000000000000000000000000000006e6574207469746c6500000016004a01
1229+010050000100500000000000000000000a0014002c01c8000000000009004a01
1230+0100500000000000000000000000000000000000000000000000000013004a01
1231+000100000100500000000000000000000000000000000000000000001c004a01
1232+01005000f3030000000000000000000000000000000000000000000001084b01
1233+03000000ef03000000000000090000000000000000000000000000006e657420
1234+7469746c6500000016004b01010050000100500000000000000000000a001400
1235+2c01c8000000000009004b010100500000000000000000000000000000000000
1236+000000000000000013004b010001000001005000000000000000000000000000
1237+00000000000000001c004b0101005000f3030000000000000000000000000000
1238+000000000000000001084c0103000000ef030000000000000900000000000000
1239+00000000000000006e6574207469746c6500000016004c010100500001005000
1240+00000000000000000a0014002c01c8000000000009004c010100500000000000
1241+000000000000000000000000000000000000000013004c010001000001005000
1242+00000000000000000000000000000000000000001c004c0101005000f3030000
1243+000000000000000000000000000000000000000001084d0103000000ef030000
1244+00000000090000000000000000000000000000006e6574207469746c65000000
1245+16004d01010050000100500000000000000000000a0014002c01c80000000000
1246+09004d0101005000000000000000000000000000000000000000000000000000
1247+13004d0100010000010050000000000000000000000000000000000000000000
1248+1c004d0101005000f30300000000000000000000000000000000000000000000
1249+01084e0103000000ef0300000000000009000000000000000000000000000000
1250+6e6574207469746c6500000016004e0101005000010050000000000000000000
1251+0a0014002c01c8000000000009004e0101005000000000000000000000000000
1252+00000000000000000000000013004e0100010000010050000000000000000000
1253+0000000000000000000000001c004e0101005000f30300000000000000000000
1254+00000000000000000000000001084f0103000000ef0300000000000009000000
1255+0000000000000000000000006e6574207469746c6500000016004f0101005000
1256+0100500000000000000000000a0014002c01c8000000000009004f0101005000
1257+00000000000000000000000000000000000000000000000013004f0100010000
1258+0100500000000000000000000000000000000000000000001c004f0101005000
1259+f303000000000000000000000000000000000000000000000108500103000000
1260+ef03000000000000090000000000000000000000000000006e6574207469746c
1261+6500000016005001010050000100500000000000000000000a0014002c01c800
1262+0000000009005001010050000000000000000000000000000000000000000000
1263+0000000013005001000100000100500000000000000000000000000000000000
1264+000000001c00500101005000f303000000000000000000000000000000000000
1265+000000000108510103000000ef03000000000000090000000000000000000000
1266+000000006e6574207469746c6500000016005101010050000100500000000000
1267+000000000a0014002c01c8000000000009005101010050000000000000000000
1268+0000000000000000000000000000000013005101000100000100500000000000
1269+000000000000000000000000000000001c00510101005000f303000000000000
1270+000000000000000000000000000000000108520103000000ef03000000000000
1271+090000000000000000000000000000006e6574207469746c6500000016005201
1272+010050000100500000000000000000000a0014002c01c8000000000009005201
1273+0100500000000000000000000000000000000000000000000000000013005201
1274+000100000100500000000000000000000000000000000000000000001c005201
1275+01005000f3030000000000000000000000000000000000000000000001085301
1276+03000000ef03000000000000090000000000000000000000000000006e657420
1277+7469746c6500000016005301010050000100500000000000000000000a001400
1278+2c01c80000000000090053010100500000000000000000000000000000000000
1279+0000000000000000130053010001000001005000000000000000000000000000
1280+00000000000000001c00530101005000f3030000000000000000000000000000
1281+00000000000000000108540103000000ef030000000000000900000000000000
1282+00000000000000006e6574207469746c65000000160054010100500001005000
1283+00000000000000000a0014002c01c80000000000090054010100500000000000
1284+0000000000000000000000000000000000000000130054010001000001005000
1285+00000000000000000000000000000000000000001c00540101005000f3030000
1286+00000000000000000000000000000000000000000108550103000000ef030000
1287+00000000090000000000000000000000000000006e6574207469746c65000000
1288+16005501010050000100500000000000000000000a0014002c01c80000000000
1289+0900550101005000000000000000000000000000000000000000000000000000
1290+1300550100010000010050000000000000000000000000000000000000000000
1291+1c00550101005000f30300000000000000000000000000000000000000000000
1292+0108560103000000ef0300000000000009000000000000000000000000000000
1293+6e6574207469746c650000001600560101005000010050000000000000000000
1294+0a0014002c01c800000000000900560101005000000000000000000000000000
1295+0000000000000000000000001300560100010000010050000000000000000000
1296+0000000000000000000000001c00560101005000f30300000000000000000000
1297+0000000000000000000000000108570103000000ef0300000000000009000000
1298+0000000000000000000000006e6574207469746c650000001600570101005000
1299+0100500000000000000000000a0014002c01c800000000000900570101005000
1300+0000000000000000000000000000000000000000000000001300570100010000
1301+0100500000000000000000000000000000000000000000001c00570101005000
1302+f303000000000000000000000000000000000000000000000108580103000000
1303+ef03000000000000090000000000000000000000000000006e6574207469746c
1304+6500000016005801010050000100500000000000000000000a0014002c01c800
1305+0000000009005801010050000000000000000000000000000000000000000000
1306+0000000013005801000100000100500000000000000000000000000000000000
1307+000000001c00580101005000f303000000000000000000000000000000000000
1308+000000000108590103000000ef03000000000000090000000000000000000000
1309+000000006e6574207469746c6500000016005901010050000100500000000000
1310+000000000a0014002c01c8000000000009005901010050000000000000000000
1311+0000000000000000000000000000000013005901000100000100500000000000
1312+000000000000000000000000000000001c00590101005000f303000000000000
1313+0000000000000000000000000000000001085a0103000000ef03000000000000
1314+090000000000000000000000000000006e6574207469746c6500000016005a01
1315+010050000100500000000000000000000a0014002c01c8000000000009005a01
1316+0100500000000000000000000000000000000000000000000000000013005a01
1317+000100000100500000000000000000000000000000000000000000001c005a01
1318+01005000f3030000000000000000000000000000000000000000000001085b01
1319+03000000ef03000000000000090000000000000000000000000000006e657420
1320+7469746c6500000016005b01010050000100500000000000000000000a001400
1321+2c01c8000000000009005b010100500000000000000000000000000000000000
1322+000000000000000013005b010001000001005000000000000000000000000000
1323+00000000000000001c005b0101005000f3030000000000000000000000000000
1324+000000000000000001085c0103000000ef030000000000000900000000000000
1325+00000000000000006e6574207469746c6500000016005c010100500001005000
1326+00000000000000000a0014002c01c8000000000009005c010100500000000000
1327+000000000000000000000000000000000000000013005c010001000001005000
1328+00000000000000000000000000000000000000001c005c0101005000f3030000
1329+000000000000000000000000000000000000000001085d0103000000ef030000
1330+00000000090000000000000000000000000000006e6574207469746c65000000
1331+16005d01010050000100500000000000000000000a0014002c01c80000000000
1332+09005d0101005000000000000000000000000000000000000000000000000000
1333+13005d0100010000010050000000000000000000000000000000000000000000
1334+1c005d0101005000f30300000000000000000000000000000000000000000000
1335+01085e0103000000ef0300000000000009000000000000000000000000000000
1336+6e6574207469746c6500000016005e0101005000010050000000000000000000
1337+0a0014002c01c8000000000009005e0101005000000000000000000000000000
1338+00000000000000000000000013005e0100010000010050000000000000000000
1339+0000000000000000000000001c005e0101005000f30300000000000000000000
1340+00000000000000000000000001085f0103000000ef0300000000000009000000
1341+0000000000000000000000006e6574207469746c6500000016005f0101005000
1342+0100500000000000000000000a0014002c01c8000000000009005f0101005000
1343+00000000000000000000000000000000000000000000000013005f0100010000
1344+0100500000000000000000000000000000000000000000001c005f0101005000
1345+f303000000000000000000000000000000000000000000000108600103000000
1346+ef03000000000000090000000000000000000000000000006e6574207469746c
1347+6500000016006001010050000100500000000000000000000a0014002c01c800
1348+0000000009006001010050000000000000000000000000000000000000000000
1349+0000000013006001000100000100500000000000000000000000000000000000
1350+000000001c00600101005000f303000000000000000000000000000000000000
1351+000000000108610103000000ef03000000000000090000000000000000000000
1352+000000006e6574207469746c6500000016006101010050000100500000000000
1353+000000000a0014002c01c8000000000009006101010050000000000000000000
1354+0000000000000000000000000000000013006101000100000100500000000000
1355+000000000000000000000000000000001c00610101005000f303000000000000
1356+000000000000000000000000000000000108620103000000ef03000000000000
1357+090000000000000000000000000000006e6574207469746c6500000016006201
1358+010050000100500000000000000000000a0014002c01c8000000000009006201
1359+0100500000000000000000000000000000000000000000000000000013006201
1360+000100000100500000000000000000000000000000000000000000001c006201
1361+01005000f3030000000000000000000000000000000000000000000001086301
1362+03000000ef03000000000000090000000000000000000000000000006e657420
1363+7469746c6500000016006301010050000100500000000000000000000a001400
1364+2c01c80000000000090063010100500000000000000000000000000000000000
1365+0000000000000000130063010001000001005000000000000000000000000000
1366+00000000000000001c00630101005000f3030000000000000000000000000000
1367+00000000000000000108640103000000ef030000000000000900000000000000
1368+00000000000000006e6574207469746c65000000160064010100500001005000
1369+00000000000000000a0014002c01c80000000000090064010100500000000000
1370+0000000000000000000000000000000000000000130064010001000001005000
1371+00000000000000000000000000000000000000001c00640101005000f3030000
1372+00000000000000000000000000000000000000000108650103000000ef030000
1373+00000000090000000000000000000000000000006e6574207469746c65000000
1374+16006501010050000100500000000000000000000a0014002c01c80000000000
1375+0900650101005000000000000000000000000000000000000000000000000000
1376+1300650100010000010050000000000000000000000000000000000000000000
1377+1c00650101005000f30300000000000000000000000000000000000000000000
1378+0108660103000000ef0300000000000009000000000000000000000000000000
1379+6e6574207469746c650000001600660101005000010050000000000000000000
1380+0a0014002c01c800000000000900660101005000000000000000000000000000
1381+0000000000000000000000001300660100010000010050000000000000000000
1382+0000000000000000000000001c00660101005000f30300000000000000000000
1383+0000000000000000000000000108670103000000ef0300000000000009000000
1384+0000000000000000000000006e6574207469746c650000001600670101005000
1385+0100500000000000000000000a0014002c01c800000000000900670101005000
1386+0000000000000000000000000000000000000000000000001300670100010000
1387+0100500000000000000000000000000000000000000000001c00670101005000
1388+f303000000000000000000000000000000000000000000000108680103000000
1389+ef03000000000000090000000000000000000000000000006e6574207469746c
1390+6500000016006801010050000100500000000000000000000a0014002c01c800
1391+0000000009006801010050000000000000000000000000000000000000000000
1392+0000000013006801000100000100500000000000000000000000000000000000
1393+000000001c00680101005000f303000000000000000000000000000000000000
1394+000000000108690103000000ef03000000000000090000000000000000000000
1395+000000006e6574207469746c6500000016006901010050000100500000000000
1396+000000000a0014002c01c8000000000009006901010050000000000000000000
1397+0000000000000000000000000000000013006901000100000100500000000000
1398+000000000000000000000000000000001c00690101005000f303000000000000
1399+0000000000000000000000000000000001086a0103000000ef03000000000000
1400+090000000000000000000000000000006e6574207469746c6500000016006a01
1401+010050000100500000000000000000000a0014002c01c8000000000009006a01
1402+0100500000000000000000000000000000000000000000000000000013006a01
1403+000100000100500000000000000000000000000000000000000000001c006a01
1404+01005000f3030000000000000000000000000000000000000000000001086b01
1405+03000000ef03000000000000090000000000000000000000000000006e657420
1406+7469746c6500000016006b01010050000100500000000000000000000a001400
1407+2c01c8000000000009006b010100500000000000000000000000000000000000
1408+000000000000000013006b010001000001005000000000000000000000000000
1409+00000000000000001c006b0101005000f3030000000000000000000000000000
1410+000000000000000001086c0103000000ef030000000000000900000000000000
1411+00000000000000006e6574207469746c6500000016006c010100500001005000
1412+00000000000000000a0014002c01c8000000000009006c010100500000000000
1413+000000000000000000000000000000000000000013006c010001000001005000
1414+00000000000000000000000000000000000000001c006c0101005000f3030000
1415+000000000000000000000000000000000000000001086d0103000000ef030000
1416+00000000090000000000000000000000000000006e6574207469746c65000000
1417+16006d01010050000100500000000000000000000a0014002c01c80000000000
1418+09006d0101005000000000000000000000000000000000000000000000000000
1419+13006d0100010000010050000000000000000000000000000000000000000000
1420+1c006d0101005000f30300000000000000000000000000000000000000000000
1421+01086e0103000000ef0300000000000009000000000000000000000000000000
1422+6e6574207469746c6500000016006e0101005000010050000000000000000000
1423+0a0014002c01c8000000000009006e0101005000000000000000000000000000
1424+00000000000000000000000013006e0100010000010050000000000000000000
1425+0000000000000000000000001c006e0101005000f30300000000000000000000
1426+00000000000000000000000001086f0103000000ef0300000000000009000000
1427+0000000000000000000000006e6574207469746c6500000016006f0101005000
1428+0100500000000000000000000a0014002c01c8000000000009006f0101005000
1429+00000000000000000000000000000000000000000000000013006f0100010000
1430+0100500000000000000000000000000000000000000000001c006f0101005000
1431+f303000000000000000000000000000000000000000000000108700103000000
1432+ef03000000000000090000000000000000000000000000006e6574207469746c
1433+6500000016007001010050000100500000000000000000000a0014002c01c800
1434+0000000009007001010050000000000000000000000000000000000000000000
1435+0000000013007001000100000100500000000000000000000000000000000000
1436+000000001c00700101005000f303000000000000000000000000000000000000
1437+000000000108710103000000ef03000000000000090000000000000000000000
1438+000000006e6574207469746c6500000016007101010050000100500000000000
1439+000000000a0014002c01c8000000000009007101010050000000000000000000
1440+0000000000000000000000000000000013007101000100000100500000000000
1441+000000000000000000000000000000001c00710101005000f303000000000000
1442+000000000000000000000000000000000108720103000000ef03000000000000
1443+090000000000000000000000000000006e6574207469746c6500000016007201
1444+010050000100500000000000000000000a0014002c01c8000000000009007201
1445+0100500000000000000000000000000000000000000000000000000013007201
1446+000100000100500000000000000000000000000000000000000000001c007201
1447+01005000f3030000000000000000000000000000000000000000000001087301
1448+03000000ef03000000000000090000000000000000000000000000006e657420
1449+7469746c6500000016007301010050000100500000000000000000000a001400
1450+2c01c80000000000090073010100500000000000000000000000000000000000
1451+0000000000000000130073010001000001005000000000000000000000000000
1452+00000000000000001c00730101005000f3030000000000000000000000000000
1453+00000000000000000108740103000000ef030000000000000900000000000000
1454+00000000000000006e6574207469746c65000000160074010100500001005000
1455+00000000000000000a0014002c01c80000000000090074010100500000000000
1456+0000000000000000000000000000000000000000130074010001000001005000
1457+00000000000000000000000000000000000000001c00740101005000f3030000
1458+00000000000000000000000000000000000000000108750103000000ef030000
1459+00000000090000000000000000000000000000006e6574207469746c65000000
1460+16007501010050000100500000000000000000000a0014002c01c80000000000
1461+0900750101005000000000000000000000000000000000000000000000000000
1462+1300750100010000010050000000000000000000000000000000000000000000
1463+1c00750101005000f30300000000000000000000000000000000000000000000
1464+0108760103000000ef0300000000000009000000000000000000000000000000
1465+6e6574207469746c650000001600760101005000010050000000000000000000
1466+0a0014002c01c800000000000900760101005000000000000000000000000000
1467+0000000000000000000000001300760100010000010050000000000000000000
1468+0000000000000000000000001c00760101005000f30300000000000000000000
1469+0000000000000000000000000108770103000000ef0300000000000009000000
1470+0000000000000000000000006e6574207469746c650000001600770101005000
1471+0100500000000000000000000a0014002c01c800000000000900770101005000
1472+0000000000000000000000000000000000000000000000001300770100010000
1473+0100500000000000000000000000000000000000000000001c00770101005000
1474+f303000000000000000000000000000000000000000000000108780103000000
1475+ef03000000000000090000000000000000000000000000006e6574207469746c
1476+6500000016007801010050000100500000000000000000000a0014002c01c800
1477+0000000009007801010050000000000000000000000000000000000000000000
1478+0000000013007801000100000100500000000000000000000000000000000000
1479+000000001c00780101005000f303000000000000000000000000000000000000
1480+000000000108790103000000ef03000000000000090000000000000000000000
1481+000000006e6574207469746c6500000016007901010050000100500000000000
1482+000000000a0014002c01c8000000000009007901010050000000000000000000
1483+0000000000000000000000000000000013007901000100000100500000000000
1484+000000000000000000000000000000001c00790101005000f303000000000000
1485+0000000000000000000000000000000001087a0103000000ef03000000000000
1486+090000000000000000000000000000006e6574207469746c6500000016007a01
1487+010050000100500000000000000000000a0014002c01c8000000000009007a01
1488+0100500000000000000000000000000000000000000000000000000013007a01
1489+000100000100500000000000000000000000000000000000000000001c007a01
1490+01005000f3030000000000000000000000000000000000000000000001087b01
1491+03000000ef03000000000000090000000000000000000000000000006e657420
1492+7469746c6500000016007b01010050000100500000000000000000000a001400
1493+2c01c8000000000009007b010100500000000000000000000000000000000000
1494+000000000000000013007b010001000001005000000000000000000000000000
1495+00000000000000001c007b0101005000f3030000000000000000000000000000
1496+000000000000000001087c0103000000ef030000000000000900000000000000
1497+00000000000000006e6574207469746c6500000016007c010100500001005000
1498+00000000000000000a0014002c01c8000000000009007c010100500000000000
1499+000000000000000000000000000000000000000013007c010001000001005000
1500+00000000000000000000000000000000000000001c007c0101005000f3030000
1501+000000000000000000000000000000000000000001087d0103000000ef030000
1502+00000000090000000000000000000000000000006e6574207469746c65000000
1503+16007d01010050000100500000000000000000000a0014002c01c80000000000
1504+09007d0101005000000000000000000000000000000000000000000000000000
1505+13007d0100010000010050000000000000000000000000000000000000000000
1506+1c007d0101005000f30300000000000000000000000000000000000000000000
1507+01087e0103000000ef0300000000000009000000000000000000000000000000
1508+6e6574207469746c6500000016007e0101005000010050000000000000000000
1509+0a0014002c01c8000000000009007e0101005000000000000000000000000000
1510+00000000000000000000000013007e0100010000010050000000000000000000
1511+0000000000000000000000001c007e0101005000f30300000000000000000000
1512+00000000000000000000000001087f0103000000ef0300000000000009000000
1513+0000000000000000000000006e6574207469746c6500000016007f0101005000
1514+0100500000000000000000000a0014002c01c8000000000009007f0101005000
1515+00000000000000000000000000000000000000000000000013007f0100010000
1516+0100500000000000000000000000000000000000000000001c007f0101005000
1517+f303000000000000000000000000000000000000000000000108800103000000
1518+ef03000000000000090000000000000000000000000000006e6574207469746c
1519+6500000016008001010050000100500000000000000000000a0014002c01c800
1520+0000000009008001010050000000000000000000000000000000000000000000
1521+0000000013008001000100000100500000000000000000000000000000000000
1522+000000001c00800101005000f303000000000000000000000000000000000000
1523+000000000108810103000000ef03000000000000090000000000000000000000
1524+000000006e6574207469746c6500000016008101010050000100500000000000
1525+000000000a0014002c01c8000000000009008101010050000000000000000000
1526+0000000000000000000000000000000013008101000100000100500000000000
1527+000000000000000000000000000000001c00810101005000f303000000000000
1528+000000000000000000000000000000000108820103000000ef03000000000000
1529+090000000000000000000000000000006e6574207469746c6500000016008201
1530+010050000100500000000000000000000a0014002c01c8000000000009008201
1531+0100500000000000000000000000000000000000000000000000000013008201
1532+000100000100500000000000000000000000000000000000000000001c008201
1533+01005000f3030000000000000000000000000000000000000000000001088301
1534+03000000ef03000000000000090000000000000000000000000000006e657420
1535+7469746c6500000016008301010050000100500000000000000000000a001400
1536+2c01c80000000000090083010100500000000000000000000000000000000000
1537+0000000000000000130083010001000001005000000000000000000000000000
1538+00000000000000001c00830101005000f3030000000000000000000000000000
1539+00000000000000000108840103000000ef030000000000000900000000000000
1540+00000000000000006e6574207469746c65000000160084010100500001005000
1541+00000000000000000a0014002c01c80000000000090084010100500000000000
1542+0000000000000000000000000000000000000000130084010001000001005000
1543+00000000000000000000000000000000000000001c00840101005000f3030000
1544+00000000000000000000000000000000000000000108850103000000ef030000
1545+00000000090000000000000000000000000000006e6574207469746c65000000
1546+16008501010050000100500000000000000000000a0014002c01c80000000000
1547+0900850101005000000000000000000000000000000000000000000000000000
1548+1300850100010000010050000000000000000000000000000000000000000000
1549+1c00850101005000f30300000000000000000000000000000000000000000000
1550+0108860103000000ef0300000000000009000000000000000000000000000000
1551+6e6574207469746c650000001600860101005000010050000000000000000000
1552+0a0014002c01c800000000000900860101005000000000000000000000000000
1553+0000000000000000000000001300860100010000010050000000000000000000
1554+0000000000000000000000001c00860101005000f30300000000000000000000
1555+0000000000000000000000000108870103000000ef0300000000000009000000
1556+0000000000000000000000006e6574207469746c650000001600870101005000
1557+0100500000000000000000000a0014002c01c800000000000900870101005000
1558+0000000000000000000000000000000000000000000000001300870100010000
1559+0100500000000000000000000000000000000000000000001c00870101005000
1560+f303000000000000000000000000000000000000000000000108880103000000
1561+ef03000000000000090000000000000000000000000000006e6574207469746c
1562+6500000016008801010050000100500000000000000000000a0014002c01c800
1563+0000000009008801010050000000000000000000000000000000000000000000
1564+0000000013008801000100000100500000000000000000000000000000000000
1565+000000001c00880101005000f303000000000000000000000000000000000000
1566+000000000108890103000000ef03000000000000090000000000000000000000
1567+000000006e6574207469746c6500000016008901010050000100500000000000
1568+000000000a0014002c01c8000000000009008901010050000000000000000000
1569+0000000000000000000000000000000013008901000100000100500000000000
1570+000000000000000000000000000000001c00890101005000f303000000000000
1571+0000000000000000000000000000000001088a0103000000ef03000000000000
1572+090000000000000000000000000000006e6574207469746c6500000016008a01
1573+010050000100500000000000000000000a0014002c01c8000000000009008a01
1574+0100500000000000000000000000000000000000000000000000000013008a01
1575+000100000100500000000000000000000000000000000000000000001c008a01
1576+01005000f3030000000000000000000000000000000000000000000001088b01
1577+03000000ef03000000000000090000000000000000000000000000006e657420
1578+7469746c6500000016008b01010050000100500000000000000000000a001400
1579+2c01c8000000000009008b010100500000000000000000000000000000000000
1580+000000000000000013008b010001000001005000000000000000000000000000
1581+00000000000000001c008b0101005000f3030000000000000000000000000000
1582+000000000000000001088c0103000000ef030000000000000900000000000000
1583+00000000000000006e6574207469746c6500000016008c010100500001005000
1584+00000000000000000a0014002c01c8000000000009008c010100500000000000
1585+000000000000000000000000000000000000000013008c010001000001005000
1586+00000000000000000000000000000000000000001c008c0101005000f3030000
1587+000000000000000000000000000000000000000001088d0103000000ef030000
1588+00000000090000000000000000000000000000006e6574207469746c65000000
1589+16008d01010050000100500000000000000000000a0014002c01c80000000000
1590+09008d0101005000000000000000000000000000000000000000000000000000
1591+13008d0100010000010050000000000000000000000000000000000000000000
1592+1c008d0101005000f30300000000000000000000000000000000000000000000
1593+01088e0103000000ef0300000000000009000000000000000000000000000000
1594+6e6574207469746c6500000016008e0101005000010050000000000000000000
1595+0a0014002c01c8000000000009008e0101005000000000000000000000000000
1596+00000000000000000000000013008e0100010000010050000000000000000000
1597+0000000000000000000000001c008e0101005000f30300000000000000000000
1598+00000000000000000000000001088f0103000000ef0300000000000009000000
1599+0000000000000000000000006e6574207469746c6500000016008f0101005000
1600+0100500000000000000000000a0014002c01c8000000000009008f0101005000
1601+00000000000000000000000000000000000000000000000013008f0100010000
1602+0100500000000000000000000000000000000000000000001c008f0101005000
1603+f303000000000000000000000000000000000000000000000108900103000000
1604+ef03000000000000090000000000000000000000000000006e6574207469746c
1605+6500000016009001010050000100500000000000000000000a0014002c01c800
1606+0000000009009001010050000000000000000000000000000000000000000000
1607+0000000013009001000100000100500000000000000000000000000000000000
1608+000000001c00900101005000f303000000000000000000000000000000000000
1609+000000000108910103000000ef03000000000000090000000000000000000000
1610+000000006e6574207469746c6500000016009101010050000100500000000000
1611+000000000a0014002c01c8000000000009009101010050000000000000000000
1612+0000000000000000000000000000000013009101000100000100500000000000
1613+000000000000000000000000000000001c00910101005000f303000000000000
1614+000000000000000000000000000000000108920103000000ef03000000000000
1615+090000000000000000000000000000006e6574207469746c6500000016009201
1616+010050000100500000000000000000000a0014002c01c8000000000009009201
1617+0100500000000000000000000000000000000000000000000000000013009201
1618+000100000100500000000000000000000000000000000000000000001c009201
1619+01005000f3030000000000000000000000000000000000000000000001089301
1620+03000000ef03000000000000090000000000000000000000000000006e657420
1621+7469746c6500000016009301010050000100500000000000000000000a001400
1622+2c01c80000000000090093010100500000000000000000000000000000000000
1623+0000000000000000130093010001000001005000000000000000000000000000
1624+00000000000000001c00930101005000f3030000000000000000000000000000
1625+00000000000000000108940103000000ef030000000000000900000000000000
1626+00000000000000006e6574207469746c65000000160094010100500001005000
1627+00000000000000000a0014002c01c80000000000090094010100500000000000
1628+0000000000000000000000000000000000000000130094010001000001005000
1629+00000000000000000000000000000000000000001c00940101005000f3030000
1630+00000000000000000000000000000000000000000108950103000000ef030000
1631+00000000090000000000000000000000000000006e6574207469746c65000000
1632+16009501010050000100500000000000000000000a0014002c01c80000000000
1633+0900950101005000000000000000000000000000000000000000000000000000
1634+1300950100010000010050000000000000000000000000000000000000000000
1635+1c00950101005000f30300000000000000000000000000000000000000000000
1636+0108960103000000ef0300000000000009000000000000000000000000000000
1637+6e6574207469746c650000001600960101005000010050000000000000000000
1638+0a0014002c01c800000000000900960101005000000000000000000000000000
1639+0000000000000000000000001300960100010000010050000000000000000000
1640+0000000000000000000000001c00960101005000f30300000000000000000000
1641+0000000000000000000000000108970103000000ef0300000000000009000000
1642+0000000000000000000000006e6574207469746c650000001600970101005000
1643+0100500000000000000000000a0014002c01c800000000000900970101005000
1644+0000000000000000000000000000000000000000000000001300970100010000
1645+0100500000000000000000000000000000000000000000001c00970101005000
1646+f303000000000000000000000000000000000000000000000108980103000000
1647+ef03000000000000090000000000000000000000000000006e6574207469746c
1648+6500000016009801010050000100500000000000000000000a0014002c01c800
1649+0000000009009801010050000000000000000000000000000000000000000000
1650+0000000013009801000100000100500000000000000000000000000000000000
1651+000000001c00980101005000f303000000000000000000000000000000000000
1652+000000000108990103000000ef03000000000000090000000000000000000000
1653+000000006e6574207469746c6500000016009901010050000100500000000000
1654+000000000a0014002c01c8000000000009009901010050000000000000000000
1655+0000000000000000000000000000000013009901000100000100500000000000
1656+000000000000000000000000000000001c00990101005000f303000000000000
1657+0000000000000000000000000000000001089a0103000000ef03000000000000
1658+090000000000000000000000000000006e6574207469746c6500000016009a01
1659+010050000100500000000000000000000a0014002c01c8000000000009009a01
1660+0100500000000000000000000000000000000000000000000000000013009a01
1661+000100000100500000000000000000000000000000000000000000001c009a01
1662+01005000f3030000000000000000000000000000000000000000000001089b01
1663+03000000ef03000000000000090000000000000000000000000000006e657420
1664+7469746c6500000016009b01010050000100500000000000000000000a001400
1665+2c01c8000000000009009b010100500000000000000000000000000000000000
1666+000000000000000013009b010001000001005000000000000000000000000000
1667+00000000000000001c009b0101005000f3030000000000000000000000000000
1668+000000000000000001089c0103000000ef030000000000000900000000000000
1669+00000000000000006e6574207469746c6500000016009c010100500001005000
1670+00000000000000000a0014002c01c8000000000009009c010100500000000000
1671+000000000000000000000000000000000000000013009c010001000001005000
1672+00000000000000000000000000000000000000001c009c0101005000f3030000
1673+000000000000000000000000000000000000000001089d0103000000ef030000
1674+00000000090000000000000000000000000000006e6574207469746c65000000
1675+16009d01010050000100500000000000000000000a0014002c01c80000000000
1676+09009d0101005000000000000000000000000000000000000000000000000000
1677+13009d0100010000010050000000000000000000000000000000000000000000
1678+1c009d0101005000f30300000000000000000000000000000000000000000000
1679+01089e0103000000ef0300000000000009000000000000000000000000000000
1680+6e6574207469746c6500000016009e0101005000010050000000000000000000
1681+0a0014002c01c8000000000009009e0101005000000000000000000000000000
1682+00000000000000000000000013009e0100010000010050000000000000000000
1683+0000000000000000000000001c009e0101005000f30300000000000000000000
1684+00000000000000000000000001089f0103000000ef0300000000000009000000
1685+0000000000000000000000006e6574207469746c6500000016009f0101005000
1686+0100500000000000000000000a0014002c01c8000000000009009f0101005000
1687+00000000000000000000000000000000000000000000000013009f0100010000
1688+0100500000000000000000000000000000000000000000001c009f0101005000
1689+f303000000000000000000000000000000000000000000000108a00103000000
1690+ef03000000000000090000000000000000000000000000006e6574207469746c
1691+650000001600a001010050000100500000000000000000000a0014002c01c800
1692+000000000900a001010050000000000000000000000000000000000000000000
1693+000000001300a001000100000100500000000000000000000000000000000000
1694+000000001c00a00101005000f303000000000000000000000000000000000000
1695+000000000108a10103000000ef03000000000000090000000000000000000000
1696+000000006e6574207469746c650000001600a101010050000100500000000000
1697+000000000a0014002c01c800000000000900a101010050000000000000000000
1698+000000000000000000000000000000001300a101000100000100500000000000
1699+000000000000000000000000000000001c00a10101005000f303000000000000
1700+000000000000000000000000000000000108a20103000000ef03000000000000
1701+090000000000000000000000000000006e6574207469746c650000001600a201
1702+010050000100500000000000000000000a0014002c01c800000000000900a201
1703+010050000000000000000000000000000000000000000000000000001300a201
1704+000100000100500000000000000000000000000000000000000000001c00a201
1705+01005000f303000000000000000000000000000000000000000000000108a301
1706+03000000ef03000000000000090000000000000000000000000000006e657420
1707+7469746c650000001600a301010050000100500000000000000000000a001400
1708+2c01c800000000000900a3010100500000000000000000000000000000000000
1709+00000000000000001300a3010001000001005000000000000000000000000000
1710+00000000000000001c00a30101005000f3030000000000000000000000000000
1711+00000000000000000108a40103000000ef030000000000000900000000000000
1712+00000000000000006e6574207469746c650000001600a4010100500001005000
1713+00000000000000000a0014002c01c800000000000900a4010100500000000000
1714+00000000000000000000000000000000000000001300a4010001000001005000
1715+00000000000000000000000000000000000000001c00a40101005000f3030000
1716+00000000000000000000000000000000000000000108a50103000000ef030000
1717+00000000090000000000000000000000000000006e6574207469746c65000000
1718+1600a501010050000100500000000000000000000a0014002c01c80000000000
1719+0900a50101005000000000000000000000000000000000000000000000000000
1720+1300a50100010000010050000000000000000000000000000000000000000000
1721+1c00a50101005000f30300000000000000000000000000000000000000000000
1722+0108a60103000000ef0300000000000009000000000000000000000000000000
1723+6e6574207469746c650000001600a60101005000010050000000000000000000
1724+0a0014002c01c800000000000900a60101005000000000000000000000000000
1725+0000000000000000000000001300a60100010000010050000000000000000000
1726+0000000000000000000000001c00a60101005000f30300000000000000000000
1727+0000000000000000000000000108a70103000000ef0300000000000009000000
1728+0000000000000000000000006e6574207469746c650000001600a70101005000
1729+0100500000000000000000000a0014002c01c800000000000900a70101005000
1730+0000000000000000000000000000000000000000000000001300a70100010000
1731+0100500000000000000000000000000000000000000000001c00a70101005000
1732+f303000000000000000000000000000000000000000000000108a80103000000
1733+ef03000000000000090000000000000000000000000000006e6574207469746c
1734+650000001600a801010050000100500000000000000000000a0014002c01c800
1735+000000000900a801010050000000000000000000000000000000000000000000
1736+000000001300a801000100000100500000000000000000000000000000000000
1737+000000001c00a80101005000f303000000000000000000000000000000000000
1738+000000000108a90103000000ef03000000000000090000000000000000000000
1739+000000006e6574207469746c650000001600a901010050000100500000000000
1740+000000000a0014002c01c800000000000900a901010050000000000000000000
1741+000000000000000000000000000000001300a901000100000100500000000000
1742+000000000000000000000000000000001c00a90101005000f303000000000000
1743+000000000000000000000000000000000108aa0103000000ef03000000000000
1744+090000000000000000000000000000006e6574207469746c650000001600aa01
1745+010050000100500000000000000000000a0014002c01c800000000000900aa01
1746+010050000000000000000000000000000000000000000000000000001300aa01
1747+000100000100500000000000000000000000000000000000000000001c00aa01
1748+01005000f303000000000000000000000000000000000000000000000108ab01
1749+03000000ef03000000000000090000000000000000000000000000006e657420
1750+7469746c650000001600ab01010050000100500000000000000000000a001400
1751+2c01c800000000000900ab010100500000000000000000000000000000000000
1752+00000000000000001300ab010001000001005000000000000000000000000000
1753+00000000000000001c00ab0101005000f3030000000000000000000000000000
1754+00000000000000000108ac0103000000ef030000000000000900000000000000
1755+00000000000000006e6574207469746c650000001600ac010100500001005000
1756+00000000000000000a0014002c01c800000000000900ac010100500000000000
1757+00000000000000000000000000000000000000001300ac010001000001005000
1758+00000000000000000000000000000000000000001c00ac0101005000f3030000
1759+00000000000000000000000000000000000000000108ad0103000000ef030000
1760+00000000090000000000000000000000000000006e6574207469746c65000000
1761+1600ad01010050000100500000000000000000000a0014002c01c80000000000
1762+0900ad0101005000000000000000000000000000000000000000000000000000
1763+1300ad0100010000010050000000000000000000000000000000000000000000
1764+1c00ad0101005000f30300000000000000000000000000000000000000000000
1765+0108ae0103000000ef0300000000000009000000000000000000000000000000
1766+6e6574207469746c650000001600ae0101005000010050000000000000000000
1767+0a0014002c01c800000000000900ae0101005000000000000000000000000000
1768+0000000000000000000000001300ae0100010000010050000000000000000000
1769+0000000000000000000000001c00ae0101005000f30300000000000000000000
1770+0000000000000000000000000108af0103000000ef0300000000000009000000
1771+0000000000000000000000006e6574207469746c650000001600af0101005000
1772+0100500000000000000000000a0014002c01c800000000000900af0101005000
1773+0000000000000000000000000000000000000000000000001300af0100010000
1774+0100500000000000000000000000000000000000000000001c00af0101005000
1775+f303000000000000000000000000000000000000000000000108b00103000000
1776+ef03000000000000090000000000000000000000000000006e6574207469746c
1777+650000001600b001010050000100500000000000000000000a0014002c01c800
1778+000000000900b001010050000000000000000000000000000000000000000000
1779+000000001300b001000100000100500000000000000000000000000000000000
1780+000000001c00b00101005000f303000000000000000000000000000000000000
1781+000000000108b10103000000ef03000000000000090000000000000000000000
1782+000000006e6574207469746c650000001600b101010050000100500000000000
1783+000000000a0014002c01c800000000000900b101010050000000000000000000
1784+000000000000000000000000000000001300b101000100000100500000000000
1785+000000000000000000000000000000001c00b10101005000f303000000000000
1786+000000000000000000000000000000000108b20103000000ef03000000000000
1787+090000000000000000000000000000006e6574207469746c650000001600b201
1788+010050000100500000000000000000000a0014002c01c800000000000900b201
1789+010050000000000000000000000000000000000000000000000000001300b201
1790+000100000100500000000000000000000000000000000000000000001c00b201
1791+01005000f303000000000000000000000000000000000000000000000108b301
1792+03000000ef03000000000000090000000000000000000000000000006e657420
1793+7469746c650000001600b301010050000100500000000000000000000a001400
1794+2c01c800000000000900b3010100500000000000000000000000000000000000
1795+00000000000000001300b3010001000001005000000000000000000000000000
1796+00000000000000001c00b30101005000f3030000000000000000000000000000
1797+00000000000000000108b40103000000ef030000000000000900000000000000
1798+00000000000000006e6574207469746c650000001600b4010100500001005000
1799+00000000000000000a0014002c01c800000000000900b4010100500000000000
1800+00000000000000000000000000000000000000001300b4010001000001005000
1801+00000000000000000000000000000000000000001c00b40101005000f3030000
1802+00000000000000000000000000000000000000000108b50103000000ef030000
1803+00000000090000000000000000000000000000006e6574207469746c65000000
1804+1600b501010050000100500000000000000000000a0014002c01c80000000000
1805+0900b50101005000000000000000000000000000000000000000000000000000
1806+1300b50100010000010050000000000000000000000000000000000000000000
1807+1c00b50101005000f30300000000000000000000000000000000000000000000
1808+0108b60103000000ef0300000000000009000000000000000000000000000000
1809+6e6574207469746c650000001600b60101005000010050000000000000000000
1810+0a0014002c01c800000000000900b60101005000000000000000000000000000
1811+0000000000000000000000001300b60100010000010050000000000000000000
1812+0000000000000000000000001c00b60101005000f30300000000000000000000
1813+0000000000000000000000000108b70103000000ef0300000000000009000000
1814+0000000000000000000000006e6574207469746c650000001600b70101005000
1815+0100500000000000000000000a0014002c01c800000000000900b70101005000
1816+0000000000000000000000000000000000000000000000001300b70100010000
1817+0100500000000000000000000000000000000000000000001c00b70101005000
1818+f303000000000000000000000000000000000000000000000108b80103000000
1819+ef03000000000000090000000000000000000000000000006e6574207469746c
1820+650000001600b801010050000100500000000000000000000a0014002c01c800
1821+000000000900b801010050000000000000000000000000000000000000000000
1822+000000001300b801000100000100500000000000000000000000000000000000
1823+000000001c00b80101005000f303000000000000000000000000000000000000
1824+000000000108b90103000000ef03000000000000090000000000000000000000
1825+000000006e6574207469746c650000001600b901010050000100500000000000
1826+000000000a0014002c01c800000000000900b901010050000000000000000000
1827+000000000000000000000000000000001300b901000100000100500000000000
1828+000000000000000000000000000000001c00b90101005000f303000000000000
1829+000000000000000000000000000000000108ba0103000000ef03000000000000
1830+090000000000000000000000000000006e6574207469746c650000001600ba01
1831+010050000100500000000000000000000a0014002c01c800000000000900ba01
1832+010050000000000000000000000000000000000000000000000000001300ba01
1833+000100000100500000000000000000000000000000000000000000001c00ba01
1834+01005000f303000000000000000000000000000000000000000000000108bb01
1835+03000000ef03000000000000090000000000000000000000000000006e657420
1836+7469746c650000001600bb01010050000100500000000000000000000a001400
1837+2c01c800000000000900bb010100500000000000000000000000000000000000
1838+00000000000000001300bb010001000001005000000000000000000000000000
1839+00000000000000001c00bb0101005000f3030000000000000000000000000000
1840+00000000000000000108bc0103000000ef030000000000000900000000000000
1841+00000000000000006e6574207469746c650000001600bc010100500001005000
1842+00000000000000000a0014002c01c800000000000900bc010100500000000000
1843+00000000000000000000000000000000000000001300bc010001000001005000
1844+00000000000000000000000000000000000000001c00bc0101005000f3030000
1845+00000000000000000000000000000000000000000108bd0103000000ef030000
1846+00000000090000000000000000000000000000006e6574207469746c65000000
1847+1600bd01010050000100500000000000000000000a0014002c01c80000000000
1848+0900bd0101005000000000000000000000000000000000000000000000000000
1849+1300bd0100010000010050000000000000000000000000000000000000000000
1850+1c00bd0101005000f30300000000000000000000000000000000000000000000
1851+0108be0103000000ef0300000000000009000000000000000000000000000000
1852+6e6574207469746c650000001600be0101005000010050000000000000000000
1853+0a0014002c01c800000000000900be0101005000000000000000000000000000
1854+0000000000000000000000001300be0100010000010050000000000000000000
1855+0000000000000000000000001c00be0101005000f30300000000000000000000
1856+0000000000000000000000000108bf0103000000ef0300000000000009000000
1857+0000000000000000000000006e6574207469746c650000001600bf0101005000
1858+0100500000000000000000000a0014002c01c800000000000900bf0101005000
1859+0000000000000000000000000000000000000000000000001300bf0100010000
1860+0100500000000000000000000000000000000000000000001c00bf0101005000
1861+f303000000000000000000000000000000000000000000000108c00103000000
1862+ef03000000000000090000000000000000000000000000006e6574207469746c
1863+650000001600c001010050000100500000000000000000000a0014002c01c800
1864+000000000900c001010050000000000000000000000000000000000000000000
1865+000000001300c001000100000100500000000000000000000000000000000000
1866+000000001c00c00101005000f303000000000000000000000000000000000000
1867+000000000108c10103000000ef03000000000000090000000000000000000000
1868+000000006e6574207469746c650000001600c101010050000100500000000000
1869+000000000a0014002c01c800000000000900c101010050000000000000000000
1870+000000000000000000000000000000001300c101000100000100500000000000
1871+000000000000000000000000000000001c00c10101005000f303000000000000
1872+000000000000000000000000000000000108c20103000000ef03000000000000
1873+090000000000000000000000000000006e6574207469746c650000001600c201
1874+010050000100500000000000000000000a0014002c01c800000000000900c201
1875+010050000000000000000000000000000000000000000000000000001300c201
1876+000100000100500000000000000000000000000000000000000000001c00c201
1877+01005000f303000000000000000000000000000000000000000000000108c301
1878+03000000ef03000000000000090000000000000000000000000000006e657420
1879+7469746c650000001600c301010050000100500000000000000000000a001400
1880+2c01c800000000000900c3010100500000000000000000000000000000000000
1881+00000000000000001300c3010001000001005000000000000000000000000000
1882+00000000000000001c00c30101005000f3030000000000000000000000000000
1883+00000000000000000108c40103000000ef030000000000000900000000000000
1884+00000000000000006e6574207469746c650000001600c4010100500001005000
1885+00000000000000000a0014002c01c800000000000900c4010100500000000000
1886+00000000000000000000000000000000000000001300c4010001000001005000
1887+00000000000000000000000000000000000000001c00c40101005000f3030000
1888+00000000000000000000000000000000000000000108c50103000000ef030000
1889+00000000090000000000000000000000000000006e6574207469746c65000000
1890+1600c501010050000100500000000000000000000a0014002c01c80000000000
1891+0900c50101005000000000000000000000000000000000000000000000000000
1892+1300c50100010000010050000000000000000000000000000000000000000000
1893+1c00c50101005000f30300000000000000000000000000000000000000000000
1894+0108c60103000000ef0300000000000009000000000000000000000000000000
1895+6e6574207469746c650000001600c60101005000010050000000000000000000
1896+0a0014002c01c800000000000900c60101005000000000000000000000000000
1897+0000000000000000000000001300c60100010000010050000000000000000000
1898+0000000000000000000000001c00c60101005000f30300000000000000000000
1899+0000000000000000000000000108c70103000000ef0300000000000009000000
1900+0000000000000000000000006e6574207469746c650000001600c70101005000
1901+0100500000000000000000000a0014002c01c800000000000900c70101005000
1902+0000000000000000000000000000000000000000000000001300c70100010000
1903+0100500000000000000000000000000000000000000000001c00c70101005000
1904+f303000000000000000000000000000000000000000000000108c80103000000
1905+ef03000000000000090000000000000000000000000000006e6574207469746c
1906+650000001600c801010050000100500000000000000000000a0014002c01c800
1907+000000000900c801010050000000000000000000000000000000000000000000
1908+000000001300c801000100000100500000000000000000000000000000000000
1909+000000001c00c80101005000f303000000000000000000000000000000000000
1910+000000000108c90103000000ef03000000000000090000000000000000000000
1911+000000006e6574207469746c650000001600c901010050000100500000000000
1912+000000000a0014002c01c800000000000900c901010050000000000000000000
1913+000000000000000000000000000000001300c901000100000100500000000000
1914+000000000000000000000000000000001c00c90101005000f303000000000000
1915+000000000000000000000000000000000108ca0103000000ef03000000000000
1916+090000000000000000000000000000006e6574207469746c650000001600ca01
1917+010050000100500000000000000000000a0014002c01c800000000000900ca01
1918+010050000000000000000000000000000000000000000000000000001300ca01
1919+000100000100500000000000000000000000000000000000000000001c00ca01
1920+01005000f303000000000000000000000000000000000000000000000108cb01
1921+03000000ef03000000000000090000000000000000000000000000006e657420
1922+7469746c650000001600cb01010050000100500000000000000000000a001400
1923+2c01c800000000000900cb010100500000000000000000000000000000000000
1924+00000000000000001300cb010001000001005000000000000000000000000000
1925+00000000000000001c00cb0101005000f3030000000000000000000000000000
1926+00000000000000000108cc0103000000ef030000000000000900000000000000
1927+00000000000000006e6574207469746c650000001600cc010100500001005000
1928+00000000000000000a0014002c01c800000000000900cc010100500000000000
1929+00000000000000000000000000000000000000001300cc010001000001005000
1930+00000000000000000000000000000000000000001c00cc0101005000f3030000
1931+00000000000000000000000000000000000000000108cd0103000000ef030000
1932+00000000090000000000000000000000000000006e6574207469746c65000000
1933+1600cd01010050000100500000000000000000000a0014002c01c80000000000
1934+0900cd0101005000000000000000000000000000000000000000000000000000
1935+1300cd0100010000010050000000000000000000000000000000000000000000
1936+1c00cd0101005000f30300000000000000000000000000000000000000000000
1937+0108ce0103000000ef0300000000000009000000000000000000000000000000
1938+6e6574207469746c650000001600ce0101005000010050000000000000000000
1939+0a0014002c01c800000000000900ce0101005000000000000000000000000000
1940+0000000000000000000000001300ce0100010000010050000000000000000000
1941+0000000000000000000000001c00ce0101005000f30300000000000000000000
1942+0000000000000000000000000108cf0103000000ef0300000000000009000000
1943+0000000000000000000000006e6574207469746c650000001600cf0101005000
1944+0100500000000000000000000a0014002c01c800000000000900cf0101005000
1945+0000000000000000000000000000000000000000000000001300cf0100010000
1946+0100500000000000000000000000000000000000000000001c00cf0101005000
1947+f303000000000000000000000000000000000000000000000108d00103000000
1948+ef03000000000000090000000000000000000000000000006e6574207469746c
1949+650000001600d001010050000100500000000000000000000a0014002c01c800
1950+000000000900d001010050000000000000000000000000000000000000000000
1951+000000001300d001000100000100500000000000000000000000000000000000
1952+000000001c00d00101005000f303000000000000000000000000000000000000
1953+000000000108d10103000000ef03000000000000090000000000000000000000
1954+000000006e6574207469746c650000001600d101010050000100500000000000
1955+000000000a0014002c01c800000000000900d101010050000000000000000000
1956+000000000000000000000000000000001300d101000100000100500000000000
1957+000000000000000000000000000000001c00d10101005000f303000000000000
1958+000000000000000000000000000000000108d20103000000ef03000000000000
1959+090000000000000000000000000000006e6574207469746c650000001600d201
1960+010050000100500000000000000000000a0014002c01c800000000000900d201
1961+010050000000000000000000000000000000000000000000000000001300d201
1962+000100000100500000000000000000000000000000000000000000001c00d201
1963+01005000f303000000000000000000000000000000000000000000000108d301
1964+03000000ef03000000000000090000000000000000000000000000006e657420
1965+7469746c650000001600d301010050000100500000000000000000000a001400
1966+2c01c800000000000900d3010100500000000000000000000000000000000000
1967+00000000000000001300d3010001000001005000000000000000000000000000
1968+00000000000000001c00d30101005000f3030000000000000000000000000000
1969+00000000000000000108d40103000000ef030000000000000900000000000000
1970+00000000000000006e6574207469746c650000001600d4010100500001005000
1971+00000000000000000a0014002c01c800000000000900d4010100500000000000
1972+00000000000000000000000000000000000000001300d4010001000001005000
1973+00000000000000000000000000000000000000001c00d40101005000f3030000
1974+00000000000000000000000000000000000000000108d50103000000ef030000
1975+00000000090000000000000000000000000000006e6574207469746c65000000
1976+1600d501010050000100500000000000000000000a0014002c01c80000000000
1977+0900d50101005000000000000000000000000000000000000000000000000000
1978+1300d50100010000010050000000000000000000000000000000000000000000
1979+1c00d50101005000f30300000000000000000000000000000000000000000000
1980+0108d60103000000ef0300000000000009000000000000000000000000000000
1981+6e6574207469746c650000001600d60101005000010050000000000000000000
1982+0a0014002c01c800000000000900d60101005000000000000000000000000000
1983+0000000000000000000000001300d60100010000010050000000000000000000
1984+0000000000000000000000001c00d60101005000f30300000000000000000000
1985+0000000000000000000000000108d70103000000ef0300000000000009000000
1986+0000000000000000000000006e6574207469746c650000001600d70101005000
1987+0100500000000000000000000a0014002c01c800000000000900d70101005000
1988+0000000000000000000000000000000000000000000000001300d70100010000
1989+0100500000000000000000000000000000000000000000001c00d70101005000
1990+f303000000000000000000000000000000000000000000000108d80103000000
1991+ef03000000000000090000000000000000000000000000006e6574207469746c
1992+650000001600d801010050000100500000000000000000000a0014002c01c800
1993+000000000900d801010050000000000000000000000000000000000000000000
1994+000000001300d801000100000100500000000000000000000000000000000000
1995+000000001c00d80101005000f303000000000000000000000000000000000000
1996+000000000108d90103000000ef03000000000000090000000000000000000000
1997+000000006e6574207469746c650000001600d901010050000100500000000000
1998+000000000a0014002c01c800000000000900d901010050000000000000000000
1999+000000000000000000000000000000001300d901000100000100500000000000
2000+000000000000000000000000000000001c00d90101005000f303000000000000
2001+000000000000000000000000000000000108da0103000000ef03000000000000
2002+090000000000000000000000000000006e6574207469746c650000001600da01
2003+010050000100500000000000000000000a0014002c01c800000000000900da01
2004+010050000000000000000000000000000000000000000000000000001300da01
2005+000100000100500000000000000000000000000000000000000000001c00da01
2006+01005000f303000000000000000000000000000000000000000000000108db01
2007+03000000ef03000000000000090000000000000000000000000000006e657420
2008+7469746c650000001600db01010050000100500000000000000000000a001400
2009+2c01c800000000000900db010100500000000000000000000000000000000000
2010+00000000000000001300db010001000001005000000000000000000000000000
2011+00000000000000001c00db0101005000f3030000000000000000000000000000
2012+00000000000000000108dc0103000000ef030000000000000900000000000000
2013+00000000000000006e6574207469746c650000001600dc010100500001005000
2014+00000000000000000a0014002c01c800000000000900dc010100500000000000
2015+00000000000000000000000000000000000000001300dc010001000001005000
2016+00000000000000000000000000000000000000001c00dc0101005000f3030000
2017+00000000000000000000000000000000000000000108dd0103000000ef030000
2018+00000000090000000000000000000000000000006e6574207469746c65000000
2019+1600dd01010050000100500000000000000000000a0014002c01c80000000000
2020+0900dd0101005000000000000000000000000000000000000000000000000000
2021+1300dd0100010000010050000000000000000000000000000000000000000000
2022+1c00dd0101005000f30300000000000000000000000000000000000000000000
2023+0108de0103000000ef0300000000000009000000000000000000000000000000
2024+6e6574207469746c650000001600de0101005000010050000000000000000000
2025+0a0014002c01c800000000000900de0101005000000000000000000000000000
2026+0000000000000000000000001300de0100010000010050000000000000000000
2027+0000000000000000000000001c00de0101005000f30300000000000000000000
2028+0000000000000000000000000108df0103000000ef0300000000000009000000
2029+0000000000000000000000006e6574207469746c650000001600df0101005000
2030+0100500000000000000000000a0014002c01c800000000000900df0101005000
2031+0000000000000000000000000000000000000000000000001300df0100010000
2032+0100500000000000000000000000000000000000000000001c00df0101005000
2033+f303000000000000000000000000000000000000000000000108e00103000000
2034+ef03000000000000090000000000000000000000000000006e6574207469746c
2035+650000001600e001010050000100500000000000000000000a0014002c01c800
2036+000000000900e001010050000000000000000000000000000000000000000000
2037+000000001300e001000100000100500000000000000000000000000000000000
2038+000000001c00e00101005000f303000000000000000000000000000000000000
2039+000000000108e10103000000ef03000000000000090000000000000000000000
2040+000000006e6574207469746c650000001600e101010050000100500000000000
2041+000000000a0014002c01c800000000000900e101010050000000000000000000
2042+000000000000000000000000000000001300e101000100000100500000000000
2043+000000000000000000000000000000001c00e10101005000f303000000000000
2044+000000000000000000000000000000000108e20103000000ef03000000000000
2045+090000000000000000000000000000006e6574207469746c650000001600e201
2046+010050000100500000000000000000000a0014002c01c800000000000900e201
2047+010050000000000000000000000000000000000000000000000000001300e201
2048+000100000100500000000000000000000000000000000000000000001c00e201
2049+01005000f303000000000000000000000000000000000000000000000108e301
2050+03000000ef03000000000000090000000000000000000000000000006e657420
2051+7469746c650000001600e301010050000100500000000000000000000a001400
2052+2c01c800000000000900e3010100500000000000000000000000000000000000
2053+00000000000000001300e3010001000001005000000000000000000000000000
2054+00000000000000001c00e30101005000f3030000000000000000000000000000
2055+00000000000000000108e40103000000ef030000000000000900000000000000
2056+00000000000000006e6574207469746c650000001600e4010100500001005000
2057+00000000000000000a0014002c01c800000000000900e4010100500000000000
2058+00000000000000000000000000000000000000001300e4010001000001005000
2059+00000000000000000000000000000000000000001c00e40101005000f3030000
2060+00000000000000000000000000000000000000000108e50103000000ef030000
2061+00000000090000000000000000000000000000006e6574207469746c65000000
2062+1600e501010050000100500000000000000000000a0014002c01c80000000000
2063+0900e50101005000000000000000000000000000000000000000000000000000
2064+1300e50100010000010050000000000000000000000000000000000000000000
2065+1c00e50101005000f30300000000000000000000000000000000000000000000
2066+0108e60103000000ef0300000000000009000000000000000000000000000000
2067+6e6574207469746c650000001600e60101005000010050000000000000000000
2068+0a0014002c01c800000000000900e60101005000000000000000000000000000
2069+0000000000000000000000001300e60100010000010050000000000000000000
2070+0000000000000000000000001c00e60101005000f30300000000000000000000
2071+0000000000000000000000000108e70103000000ef0300000000000009000000
2072+0000000000000000000000006e6574207469746c650000001600e70101005000
2073+0100500000000000000000000a0014002c01c800000000000900e70101005000
2074+0000000000000000000000000000000000000000000000001300e70100010000
2075+0100500000000000000000000000000000000000000000001c00e70101005000
2076+f303000000000000000000000000000000000000000000000108e80103000000
2077+ef03000000000000090000000000000000000000000000006e6574207469746c
2078+650000001600e801010050000100500000000000000000000a0014002c01c800
2079+000000000900e801010050000000000000000000000000000000000000000000
2080+000000001300e801000100000100500000000000000000000000000000000000
2081+000000001c00e80101005000f303000000000000000000000000000000000000
2082+000000000108e90103000000ef03000000000000090000000000000000000000
2083+000000006e6574207469746c650000001600e901010050000100500000000000
2084+000000000a0014002c01c800000000000900e901010050000000000000000000
2085+000000000000000000000000000000001300e901000100000100500000000000
2086+000000000000000000000000000000001c00e90101005000f303000000000000
2087+000000000000000000000000000000000108ea0103000000ef03000000000000
2088+090000000000000000000000000000006e6574207469746c650000001600ea01
2089+010050000100500000000000000000000a0014002c01c800000000000900ea01
2090+010050000000000000000000000000000000000000000000000000001300ea01
2091+000100000100500000000000000000000000000000000000000000001c00ea01
2092+01005000f303000000000000000000000000000000000000000000000108eb01
2093+03000000ef03000000000000090000000000000000000000000000006e657420
2094+7469746c650000001600eb01010050000100500000000000000000000a001400
2095+2c01c800000000000900eb010100500000000000000000000000000000000000
2096+00000000000000001300eb010001000001005000000000000000000000000000
2097+00000000000000001c00eb0101005000f3030000000000000000000000000000
2098+00000000000000000108ec0103000000ef030000000000000900000000000000
2099+00000000000000006e6574207469746c650000001600ec010100500001005000
2100+00000000000000000a0014002c01c800000000000900ec010100500000000000
2101+00000000000000000000000000000000000000001300ec010001000001005000
2102+00000000000000000000000000000000000000001c00ec0101005000f3030000
2103+00000000000000000000000000000000000000000108ed0103000000ef030000
2104+00000000090000000000000000000000000000006e6574207469746c65000000
2105+1600ed01010050000100500000000000000000000a0014002c01c80000000000
2106+0900ed0101005000000000000000000000000000000000000000000000000000
2107+1300ed0100010000010050000000000000000000000000000000000000000000
2108+1c00ed0101005000f30300000000000000000000000000000000000000000000
2109+0108ee0103000000ef0300000000000009000000000000000000000000000000
2110+6e6574207469746c650000001600ee0101005000010050000000000000000000
2111+0a0014002c01c800000000000900ee0101005000000000000000000000000000
2112+0000000000000000000000001300ee0100010000010050000000000000000000
2113+0000000000000000000000001c00ee0101005000f30300000000000000000000
2114+0000000000000000000000000108ef0103000000ef0300000000000009000000
2115+0000000000000000000000006e6574207469746c650000001600ef0101005000
2116+0100500000000000000000000a0014002c01c800000000000900ef0101005000
2117+0000000000000000000000000000000000000000000000001300ef0100010000
2118+0100500000000000000000000000000000000000000000001c00ef0101005000
2119+f303000000000000000000000000000000000000000000000108f00103000000
2120+ef03000000000000090000000000000000000000000000006e6574207469746c
2121+650000001600f001010050000100500000000000000000000a0014002c01c800
2122+000000000900f001010050000000000000000000000000000000000000000000
2123+000000001300f001000100000100500000000000000000000000000000000000
2124+000000001c00f00101005000f303000000000000000000000000000000000000
2125+000000000108f10103000000ef03000000000000090000000000000000000000
2126+000000006e6574207469746c650000001600f101010050000100500000000000
2127+000000000a0014002c01c800000000000900f101010050000000000000000000
2128+000000000000000000000000000000001300f101000100000100500000000000
2129+000000000000000000000000000000001c00f10101005000f303000000000000
2130+000000000000000000000000000000000108f20103000000ef03000000000000
2131+090000000000000000000000000000006e6574207469746c650000001600f201
2132+010050000100500000000000000000000a0014002c01c800000000000900f201
2133+010050000000000000000000000000000000000000000000000000001300f201
2134+000100000100500000000000000000000000000000000000000000001c00f201
2135+01005000f303000000000000000000000000000000000000000000000108f301
2136+03000000ef03000000000000090000000000000000000000000000006e657420
2137+7469746c650000001600f301010050000100500000000000000000000a001400
2138+2c01c800000000000900f3010100500000000000000000000000000000000000
2139+00000000000000001300f3010001000001005000000000000000000000000000
2140+00000000000000001c00f30101005000f3030000000000000000000000000000
2141+00000000000000000108f40103000000ef030000000000000900000000000000
2142+00000000000000006e6574207469746c650000001600f4010100500001005000
2143+00000000000000000a0014002c01c800000000000900f4010100500000000000
2144+00000000000000000000000000000000000000001300f4010001000001005000
2145+00000000000000000000000000000000000000001c00f40101005000f3030000
2146+00000000000000000000000000000000000000000108f50103000000ef030000
2147+00000000090000000000000000000000000000006e6574207469746c65000000
2148+1600f501010050000100500000000000000000000a0014002c01c80000000000
2149+0900f50101005000000000000000000000000000000000000000000000000000
2150+1300f50100010000010050000000000000000000000000000000000000000000
A internal/adapters/wayland/xwm/unmanaged.go +109 -0
@@ -0,0 +1,109 @@
1+package xwm
2+
3+import (
4+ "bnema.dev/xturbo"
5+ "bnema.dev/xturbo/xproto"
6+)
7+
8+// Override-redirect windows are tracked, never managed: the XWM reads their
9+// window type once at map, pairs them with their wl_surface and reports the
10+// mapped rect to the host in layout coordinates.
11+
12+// maxTypeWords bounds the _NET_WM_WINDOW_TYPE read.
13+const maxTypeWords = 32
14+
15+// requestOverrideType asks for the window type of a freshly mapped override
16+// window; the reply completes its readiness.
17+func (x *XWM) requestOverrideType(s Slot) {
18+ rec := x.tab.rec(s)
19+ seq := xproto.GetProperty(x.conn.Buf(), false, xproto.Window(rec.xid),
20+ xproto.Atom(x.atoms[atomNetWMWindowType]), xproto.AtomAny, 0, maxTypeWords)
21+ x.expect(seq, kindOverrideType, x.tab.tag(s), 0)
22+ x.wake()
23+}
24+
25+func (x *XWM) onOverrideType(s Slot, rec *record, r *xturbo.Reply) {
26+ rec.noInput = false
27+ if rep, ok := xproto.ParseGetPropertyReply(r.Raw); ok && rep.Format() == 32 {
28+ val := rep.Value()
29+ for i := 0; i+4 <= len(val); i += 4 {
30+ switch x.windowType(word(val, i/4)) {
31+ case atomNetWMWindowTypeTooltip, atomNetWMWindowTypeNotification, atomNetWMWindowTypeDND:
32+ rec.noInput = true
33+ }
34+ }
35+ }
36+ x.overrideReady(s)
37+}
38+
39+// overrideReady marks a mapped override window typed and pairs it.
40+func (x *XWM) overrideReady(s Slot) {
41+ rec := x.tab.rec(s)
42+ if rec == nil || rec.kind != kindUnmanaged || !rec.mapped {
43+ return
44+ }
45+ rec.ready = true
46+ x.tryPair(s)
47+ x.overrideMapped(s, rec)
48+}
49+
50+// overrideMapped reports the window once it is mapped, typed and paired.
51+func (x *XWM) overrideMapped(s Slot, rec *record) {
52+ if x.host == nil || rec.ovPaired || !rec.mapped || !rec.ready || rec.surface == 0 {
53+ return
54+ }
55+ rec.ovPaired = true
56+ rec.ov = x.layoutRect(rec)
57+ o := rec.ov
58+ x.host.OverrideMapped(s, rec.xid, rec.surface, int(o.x), int(o.y), int(o.w), int(o.h), rec.noInput)
59+}
60+
61+// overrideUnmapped ends the mapped life of an override window; a remap reads
62+// the type again and pairs with a new surface.
63+func (x *XWM) overrideUnmapped(s Slot, rec *record) {
64+ rec.ready, rec.serial, rec.surface, rec.noInput = false, 0, 0, false
65+ rec.gen++ // a type reply still in flight names a dead tag
66+ if rec.gen == 0 {
67+ rec.gen = 1
68+ }
69+ if rec.ovPaired {
70+ rec.ovPaired = false
71+ x.host.OverrideGone(s)
72+ }
73+}
74+
75+func (x *XWM) layoutRect(rec *record) ovRect {
76+ lx, ly := x.space.ToLayout(int(rec.x), int(rec.y))
77+ return ovRect{int32(lx), int32(ly), int32(rec.w), int32(rec.h)}
78+}
79+
80+// queueOverride schedules a geometry report for s; flushOverrides sends it
81+// once per iteration, only if the layout rect changed.
82+func (x *XWM) queueOverride(s Slot, rec *record) {
83+ if rec.ovQueued || s == 0 {
84+ return
85+ }
86+ if len(x.ovDirty) == cap(x.ovDirty) {
87+ x.flushOverrides()
88+ }
89+ rec.ovQueued = true
90+ x.ovDirty = append(x.ovDirty, s)
91+}
92+
93+func (x *XWM) flushOverrides() {
94+ for _, s := range x.ovDirty {
95+ rec := x.tab.rec(s)
96+ if rec == nil || rec.kind != kindUnmanaged || !rec.ovQueued {
97+ continue
98+ }
99+ rec.ovQueued = false
100+ if !rec.ovPaired {
101+ continue
102+ }
103+ if o := x.layoutRect(rec); o != rec.ov {
104+ rec.ov = o
105+ x.host.OverrideMoved(s, int(o.x), int(o.y), int(o.w), int(o.h))
106+ }
107+ }
108+ x.ovDirty = x.ovDirty[:0]
109+}
A internal/adapters/wayland/xwm/unmanaged_test.go +247 -0
@@ -0,0 +1,247 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "testing"
6+
7+ "bnema.dev/xturbo"
8+ "bnema.dev/xturbo/xproto"
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/stretchr/testify/require"
11+)
12+
13+func layoutOf(os ...ports.OutputPlacement) ports.Layout { return ports.Layout(os) }
14+
15+func rootConfigureNotify(win uint32, x, y int16, w, h uint16) xturbo.Event {
16+ ev := xturbo.Event{Kind: xproto.KindConfigureNotify}
17+ binary.LittleEndian.PutUint32(ev.Raw[4:], peerRoot)
18+ binary.LittleEndian.PutUint32(ev.Raw[8:], win)
19+ binary.LittleEndian.PutUint16(ev.Raw[16:], uint16(x))
20+ binary.LittleEndian.PutUint16(ev.Raw[18:], uint16(y))
21+ binary.LittleEndian.PutUint16(ev.Raw[20:], w)
22+ binary.LittleEndian.PutUint16(ev.Raw[22:], h)
23+ return ev
24+}
25+
26+func (p *xpeer) rootConfigureNotify(win uint32, x, y int16, w, h uint16) {
27+ p.event(22, func(b *[32]byte) {
28+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
29+ binary.LittleEndian.PutUint32(b[8:], win)
30+ binary.LittleEndian.PutUint16(b[16:], uint16(x))
31+ binary.LittleEndian.PutUint16(b[18:], uint16(y))
32+ binary.LittleEndian.PutUint16(b[20:], w)
33+ binary.LittleEndian.PutUint16(b[22:], h)
34+ })
35+}
36+
37+func TestPlaceConvertsLayoutToXCoordinates(t *testing.T) {
38+ x, p, h, _, _ := attached(t)
39+ s := newWin(t, x, p, h, 0x500001)
40+ x.SetLayout(layoutOf(out(-640, 0, 640, 480), out(0, 0, 1920, 1080)))
41+ n := settle(t, x, p)
42+
43+ x.Place(s, -640+10, 20)
44+ settle(t, x, p)
45+ cw := since(p, n, opConfigureWindow)
46+ require.Len(t, cw, 1)
47+ require.EqualValues(t, 10, cw[0].u32(12))
48+ require.EqualValues(t, 20, cw[0].u32(16))
49+}
50+
51+func TestSetLayoutRepositionsPlacedWindows(t *testing.T) {
52+ x, p, h, _, _ := attached(t)
53+ s := newWin(t, x, p, h, 0x500001)
54+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080)))
55+ x.Place(s, 100, 50)
56+ n := settle(t, x, p)
57+
58+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080))) // unchanged: nothing resent
59+ settle(t, x, p)
60+ require.Empty(t, since(p, n, opConfigureWindow))
61+
62+ x.SetLayout(layoutOf(out(-100, 0, 100, 100), out(0, 0, 1920, 1080)))
63+ settle(t, x, p)
64+ cw := since(p, n, opConfigureWindow)
65+ require.Len(t, cw, 1)
66+ require.EqualValues(t, 200, cw[0].u32(12))
67+ require.EqualValues(t, 50, cw[0].u32(16))
68+}
69+
70+func TestSetLayoutWarnsOnceWhenOutputsDropped(t *testing.T) {
71+ x, _, _, _, _ := attached(t)
72+ var l ports.Layout
73+ for i := 0; i < maxRegions+2; i++ {
74+ l = append(l, out(i*100, 0, 100, 100))
75+ }
76+ x.SetLayout(l)
77+ require.True(t, x.warnedSpace)
78+ require.Equal(t, 2, x.space.Dropped())
79+}
80+
81+// overrideWindow creates and maps an override window, answers its type read
82+// and pairs it; the mocked host must expect OverrideMapped beforehand.
83+func overrideWindow(t *testing.T, x *XWM, p *xpeer, win uint32, typ string) Slot {
84+ t.Helper()
85+ if typ != "" {
86+ p.setProp(win, p.atom("_NET_WM_WINDOW_TYPE"), uint32(xproto.AtomAtom), 32, le32(p.atom(typ)))
87+ }
88+ p.createNotify(win, true)
89+ readUntil(t, x, func() bool { return x.tab.lookup(win) != 0 })
90+ s := x.tab.lookup(win)
91+ x.feed(t, p, func() { p.rootConfigureNotify(win, 30, 40, 100, 50) })
92+ x.feed(t, p, func() { p.mapNotify(win) })
93+ return s
94+}
95+
96+func TestUnmanagedMapReadsTypeThenPairsThenReports(t *testing.T) {
97+ x, p, h, _, _ := attached(t)
98+ x.SetLayout(layoutOf(out(-640, 0, 640, 480), out(0, 0, 1920, 1080)))
99+ const win = 0x500009
100+ s := overrideWindow(t, x, p, win, "_NET_WM_WINDOW_TYPE_TOOLTIP")
101+ x.flush()
102+ readUntil(t, x, func() bool { return x.tab.rec(s).ready })
103+ typeReads := 0
104+ for _, q := range p.requests() {
105+ if q.major == opGetProperty && q.u32(4) == win {
106+ typeReads++
107+ }
108+ }
109+ require.Equal(t, 1, typeReads)
110+
111+ h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), -640+30, 40, 100, 50, true).Once()
112+ x.SetSerial(7, 9)
113+ x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) })
114+ require.True(t, x.tab.rec(s).ovPaired)
115+}
116+
117+func TestUnmanagedDefaultTypeTakesInput(t *testing.T) {
118+ x, p, h, _, _ := attached(t)
119+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080)))
120+ const win = 0x500009
121+ s := overrideWindow(t, x, p, win, "_NET_WM_WINDOW_TYPE_POPUP_MENU")
122+ x.SetSerial(7, 9)
123+ h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), 30, 40, 100, 50, false).Once()
124+ x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) })
125+ readUntil(t, x, func() bool { return x.tab.rec(s).ovPaired })
126+}
127+
128+func TestUnmanagedMoveAndLayoutChangeAndUnmap(t *testing.T) {
129+ x, p, h, _, _ := attached(t)
130+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080)))
131+ const win = 0x500009
132+ s := overrideWindow(t, x, p, win, "")
133+ x.SetSerial(7, 9)
134+ h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), 30, 40, 100, 50, false).Once()
135+ x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) })
136+ readUntil(t, x, func() bool { return x.tab.rec(s).ovPaired })
137+
138+ h.EXPECT().OverrideMoved(s, 35, 45, 100, 60).Once()
139+ x.feed(t, p, func() { p.rootConfigureNotify(win, 35, 45, 100, 60) })
140+ x.flush()
141+ x.feed(t, p, func() { p.rootConfigureNotify(win, 35, 45, 100, 60) }) // same rect: no call
142+ x.flush()
143+
144+ h.EXPECT().OverrideMoved(s, -65, 45, 100, 60).Once() // the layout origin moved: same X point, new layout point
145+ x.SetLayout(layoutOf(out(-100, 0, 100, 100), out(0, 0, 1920, 1080)))
146+ x.flush()
147+
148+ h.EXPECT().OverrideGone(s).Once()
149+ x.feed(t, p, func() { p.unmapNotify(win, false) })
150+ require.False(t, x.tab.rec(s).ovPaired)
151+}
152+
153+func TestUnmanagedDestroyReportsGone(t *testing.T) {
154+ x, p, h, _, _ := attached(t)
155+ const win = 0x500009
156+ s := overrideWindow(t, x, p, win, "")
157+ x.SetSerial(7, 9)
158+ h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), 30, 40, 100, 50, false).Once()
159+ x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) })
160+ readUntil(t, x, func() bool { return x.tab.rec(s).ovPaired })
161+ h.EXPECT().OverrideGone(s).Once()
162+ x.feed(t, p, func() { p.destroyNotify(win) })
163+ require.Zero(t, x.tab.lookup(win))
164+}
165+
166+func TestFocusInIgnoresUnmanagedAndGrabModes(t *testing.T) {
167+ x, p, h, _, _ := attached(t)
168+ sa := newWin(t, x, p, h, 0x500001)
169+ newWin(t, x, p, h, 0x500002)
170+ x.Focus(sa)
171+ p.createNotify(0x500009, true)
172+ readUntil(t, x, func() bool { return x.tab.lookup(0x500009) != 0 })
173+ n := settle(t, x, p)
174+
175+ focusIn := func(win uint32, mode byte) {
176+ p.event(9, func(e *[32]byte) {
177+ binary.LittleEndian.PutUint32(e[4:], win)
178+ e[8] = mode
179+ })
180+ }
181+ x.feed(t, p, func() { focusIn(0x500009, 0) })
182+ x.feed(t, p, func() { focusIn(0x500002, byte(xproto.NotifyModeGrab)) })
183+ x.feed(t, p, func() { focusIn(0x500002, byte(xproto.NotifyModeUngrab)) })
184+ require.False(t, x.refocus)
185+ settle(t, x, p)
186+ require.Empty(t, since(p, n, opSetInputFocus))
187+
188+ x.feed(t, p, func() { focusIn(0x500002, 0) })
189+ settle(t, x, p)
190+ require.Len(t, since(p, n, opSetInputFocus), 1, "a normal FocusIn on another managed window is still denied")
191+}
192+
193+func TestWMChangeStateIgnored(t *testing.T) {
194+ x, p, h, _, _ := attached(t)
195+ newWin(t, x, p, h, 0x500001)
196+ n := settle(t, x, p)
197+ x.feed(t, p, func() { p.clientMessage(0x500001, p.atom("WM_CHANGE_STATE"), 3) })
198+ settle(t, x, p)
199+ require.Empty(t, since(p, n, opConfigureWindow)) // the mocked host sees no call either
200+}
201+
202+func TestUnmanagedGeometryAllocations(t *testing.T) {
203+ x, p, _, _, _ := attached(t)
204+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080)))
205+ s := x.insert(0x500009)
206+ r := x.tab.rec(s)
207+ r.kind, r.mapped, r.ready, r.ovPaired = kindUnmanaged, true, true, true
208+ r.x, r.y, r.w, r.h = 30, 40, 100, 50
209+ r.ov = x.layoutRect(r)
210+ sinkPeer(t, x, p)
211+ a := rootConfigureNotify(0x500009, 31, 41, 100, 50)
212+ b := rootConfigureNotify(0x500009, 30, 40, 100, 50) // back to the reported rect
213+ allocs := testing.AllocsPerRun(1000, func() {
214+ x.onEvent(&a)
215+ x.onEvent(&b)
216+ x.flush()
217+ })
218+ require.Zero(t, allocs)
219+ require.True(t, r.ovPaired)
220+}
221+
222+func TestFocusInUnmanagedAllocations(t *testing.T) {
223+ x, p, _, _, _ := attached(t)
224+ s := x.insert(0x500009)
225+ x.tab.rec(s).kind = kindUnmanaged
226+ sinkPeer(t, x, p)
227+ ev := xturbo.Event{Kind: xproto.KindFocusIn}
228+ binary.LittleEndian.PutUint32(ev.Raw[4:], 0x500009)
229+ grab := ev
230+ grab.Raw[8] = byte(xproto.NotifyModeGrab)
231+ allocs := testing.AllocsPerRun(1000, func() {
232+ x.onEvent(&ev)
233+ x.onEvent(&grab)
234+ x.flush()
235+ })
236+ require.Zero(t, allocs)
237+ require.False(t, x.refocus)
238+}
239+
240+func TestDropWindowClearsQueuedOverride(t *testing.T) {
241+ x, _, _, _, _ := attached(t)
242+ s := x.insert(0x500009)
243+ r := x.tab.rec(s)
244+ r.kind, r.ovQueued = kindUnmanaged, true
245+ x.dropWindow(s)
246+ require.False(t, r.ovQueued)
247+}
A internal/adapters/wayland/xwm/xpeer_test.go +594 -0
@@ -0,0 +1,594 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "io"
6+ "os"
7+ "sync"
8+ "sync/atomic"
9+ "testing"
10+ "time"
11+
12+ "golang.org/x/sys/unix"
13+)
14+
15+// Opcodes the peer understands.
16+const (
17+ opCreateWindow = 1
18+ opChangeWindowAttributes = 2
19+ opGetWindowAttributes = 3
20+ opMapWindow = 8
21+ opDeleteProperty = 19
22+ opConfigureWindow = 12
23+ opGetGeometry = 14
24+ opQueryTree = 15
25+ opInternAtom = 16
26+ opChangeProperty = 18
27+ opGetProperty = 20
28+ opSetSelectionOwner = 22
29+ opSendEvent = 25
30+ opSetInputFocus = 42
31+ opGetInputFocus = 43
32+ opQueryExtension = 98
33+ opKillClient = 113
34+)
35+
36+const (
37+ peerRoot = 0x100
38+ peerXIDBase = 0x200000
39+ peerXIDMask = 0x1fffff
40+ peerMaxWords = 0x100000
41+)
42+
43+type xpeerExt struct{ major, firstEvent, firstError uint8 }
44+
45+// xpeerReq is one request the peer received.
46+type xpeerReq struct {
47+ seq uint16
48+ major uint8
49+ minor uint8
50+ raw []byte // whole request, header included
51+}
52+
53+func (r xpeerReq) u32(off int) uint32 { return binary.LittleEndian.Uint32(r.raw[off:]) }
54+
55+type xpeerProp struct {
56+ typ uint32
57+ format uint8
58+ data []byte
59+}
60+
61+type xpeerGeom struct {
62+ x, y int16
63+ w, h, border uint16
64+}
65+
66+// xpeer is a scripted X server speaking the real wire protocol over a
67+// socketpair: setup, extension queries, atoms, properties, geometry, window
68+// tree. It records every request. It is a protocol peer, not a test double of
69+// a project interface.
70+type xpeer struct {
71+ t *testing.T
72+ f *os.File
73+
74+ mu sync.Mutex
75+ seq uint16
76+ reqs []xpeerReq
77+ exts map[string]xpeerExt
78+ atoms map[string]uint32
79+ nextAtom uint32
80+ props map[uint32]map[uint32]xpeerProp
81+ geoms map[uint32]xpeerGeom
82+ attrs map[uint32][2]byte // window -> {overrideRedirect, mapState}
83+ children []uint32
84+ denyRoot bool // answer the root event-mask request with BadAccess
85+ pids map[uint32]uint32 // XRes QueryClientIds answers, by window
86+ bad map[uint32]bool // windows whose GetProperty/GetGeometry fail with BadWindow
87+ hold bool // keep replies until release
88+ held [][]byte
89+ tap []byte // everything written to the client, when recording
90+ record bool
91+
92+ gate atomic.Pointer[chan struct{}] // closed = reading allowed
93+ sbuf [4096]byte
94+ sink atomic.Bool // discard requests without recording or allocating
95+ done chan struct{}
96+}
97+
98+func fullPeerExts() map[string]xpeerExt {
99+ return map[string]xpeerExt{
100+ "BIG-REQUESTS": {133, 0, 0},
101+ "XC-MISC": {136, 0, 0},
102+ "Composite": {142, 0, 0},
103+ "XFIXES": {138, 87, 140},
104+ "X-Resource": {144, 0, 0},
105+ }
106+}
107+
108+// startXPeer returns the client end of a socketpair, to give to Boot, and the
109+// peer serving the other end.
110+func startXPeer(t *testing.T, exts map[string]xpeerExt) (int, *xpeer) {
111+ t.Helper()
112+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
113+ if err != nil {
114+ t.Fatal(err)
115+ }
116+ p := &xpeer{
117+ t: t, f: os.NewFile(uintptr(fds[1]), "xpeer"),
118+ exts: exts, atoms: map[string]uint32{}, nextAtom: 1000,
119+ props: map[uint32]map[uint32]xpeerProp{}, geoms: map[uint32]xpeerGeom{}, attrs: map[uint32][2]byte{},
120+ pids: map[uint32]uint32{}, bad: map[uint32]bool{},
121+ done: make(chan struct{}),
122+ }
123+ open := make(chan struct{})
124+ close(open)
125+ p.gate.Store(&open)
126+ t.Cleanup(func() {
127+ p.resume() // a paused peer would never see the close
128+ p.f.Close()
129+ <-p.done
130+ })
131+ go p.serve()
132+ return fds[0], p
133+}
134+
135+func (p *xpeer) setupReply() []byte {
136+ vendor := []byte("xpeer\x00\x00\x00")
137+ b := make([]byte, 40+len(vendor)+40)
138+ b[0], b[2] = 1, 11
139+ binary.LittleEndian.PutUint16(b[6:], uint16((len(b)-8)/4))
140+ binary.LittleEndian.PutUint32(b[12:], peerXIDBase)
141+ binary.LittleEndian.PutUint32(b[16:], peerXIDMask)
142+ binary.LittleEndian.PutUint16(b[24:], 5)
143+ b[28] = 1
144+ copy(b[40:], vendor)
145+ scr := b[40+len(vendor):]
146+ binary.LittleEndian.PutUint32(scr, peerRoot)
147+ binary.LittleEndian.PutUint32(scr[32:], 0x22) // root visual
148+ scr[38] = 24 // root depth
149+ return b
150+}
151+
152+func (p *xpeer) serve() {
153+ defer close(p.done)
154+ var hdr [12]byte
155+ if _, err := io.ReadFull(p.f, hdr[:]); err != nil {
156+ return
157+ }
158+ // Auth name and data are empty (NewConn sends none).
159+ if _, err := p.f.Write(p.setupReply()); err != nil {
160+ return
161+ }
162+ for {
163+ <-*p.gate.Load()
164+ if p.sink.Load() {
165+ if !p.discard() {
166+ return
167+ }
168+ continue
169+ }
170+ var h [4]byte
171+ if _, err := io.ReadFull(p.f, h[:]); err != nil {
172+ return
173+ }
174+ raw := h[:]
175+ n := int(binary.LittleEndian.Uint16(h[2:])) * 4
176+ if n == 0 { // BIG-REQUESTS header
177+ var l [4]byte
178+ if _, err := io.ReadFull(p.f, l[:]); err != nil {
179+ return
180+ }
181+ raw = append(raw, l[:]...)
182+ n = int(binary.LittleEndian.Uint32(l[:])) * 4
183+ }
184+ rest := make([]byte, n-len(raw))
185+ if _, err := io.ReadFull(p.f, rest); err != nil {
186+ return
187+ }
188+ raw = append(append([]byte(nil), raw...), rest...)
189+ p.mu.Lock()
190+ p.seq++
191+ r := xpeerReq{p.seq, h[0], h[1], raw}
192+ p.reqs = append(p.reqs, r)
193+ p.answer(r, raw[len(raw)-len(rest):])
194+ p.mu.Unlock()
195+ }
196+}
197+
198+// discard reads and drops one request without allocating. It reports false
199+// when the connection ended.
200+func (p *xpeer) discard() bool {
201+ if _, err := io.ReadFull(p.f, p.sbuf[:4]); err != nil {
202+ return false
203+ }
204+ n := int(binary.LittleEndian.Uint16(p.sbuf[2:])) * 4
205+ left := n - 4
206+ if n == 0 {
207+ if _, err := io.ReadFull(p.f, p.sbuf[:4]); err != nil {
208+ return false
209+ }
210+ left = int(binary.LittleEndian.Uint32(p.sbuf[:4]))*4 - 8
211+ }
212+ for left > 0 {
213+ m, err := p.f.Read(p.sbuf[:min(left, len(p.sbuf))])
214+ if err != nil {
215+ return false
216+ }
217+ left -= m
218+ }
219+ return true
220+}
221+
222+// write sends bytes to the client. The caller holds p.mu.
223+func (p *xpeer) write(b []byte) {
224+ if p.record {
225+ p.tap = append(p.tap, b...)
226+ }
227+ _, _ = p.f.Write(b)
228+}
229+
230+func (p *xpeer) reply(data uint8, body []byte) {
231+ words := (max(len(body), 24) - 24 + 3) / 4
232+ b := make([]byte, 32+4*words)
233+ b[0], b[1] = 1, data
234+ binary.LittleEndian.PutUint16(b[2:], p.seq)
235+ binary.LittleEndian.PutUint32(b[4:], uint32(words))
236+ copy(b[8:], body)
237+ if p.hold {
238+ p.held = append(p.held, b)
239+ return
240+ }
241+ p.write(b)
242+}
243+
244+// release sends the held replies, in request order as a real server does.
245+func (p *xpeer) release() {
246+ p.mu.Lock()
247+ defer p.mu.Unlock()
248+ p.hold = false
249+ for _, b := range p.held {
250+ p.write(b)
251+ }
252+ p.held = nil
253+}
254+
255+// holdReplies keeps the replies of the following requests back.
256+func (p *xpeer) holdReplies() {
257+ p.mu.Lock()
258+ p.hold = true
259+ p.mu.Unlock()
260+}
261+
262+func (p *xpeer) errorPacket(code, major uint8, bad uint32) {
263+ b := make([]byte, 32)
264+ b[1] = code
265+ binary.LittleEndian.PutUint16(b[2:], p.seq)
266+ binary.LittleEndian.PutUint32(b[4:], bad)
267+ b[10] = major
268+ p.write(b)
269+}
270+
271+func (p *xpeer) extName(major uint8) string {
272+ for name, e := range p.exts {
273+ if e.major == major {
274+ return name
275+ }
276+ }
277+ return ""
278+}
279+
280+func le32(v ...uint32) []byte {
281+ b := make([]byte, 0, 4*len(v))
282+ for _, x := range v {
283+ b = binary.LittleEndian.AppendUint32(b, x)
284+ }
285+ return b
286+}
287+
288+// answer runs with p.mu held.
289+func (p *xpeer) answer(r xpeerReq, body []byte) {
290+ switch {
291+ case r.major == opQueryExtension:
292+ e, ok := p.exts[string(body[4:4+binary.LittleEndian.Uint16(body)])]
293+ var present uint8
294+ if ok {
295+ present = 1
296+ }
297+ p.reply(0, []byte{present, e.major, e.firstEvent, e.firstError})
298+ case r.major == opInternAtom:
299+ name := string(body[4 : 4+binary.LittleEndian.Uint16(body)])
300+ a, ok := p.atoms[name]
301+ if !ok {
302+ p.nextAtom++
303+ a = p.nextAtom
304+ p.atoms[name] = a
305+ }
306+ p.reply(0, le32(a))
307+ case r.major == opGetInputFocus:
308+ p.reply(1, le32(0))
309+ case r.major == opQueryTree:
310+ b := make([]byte, 24+4*len(p.children))
311+ binary.LittleEndian.PutUint32(b[0:], peerRoot)
312+ binary.LittleEndian.PutUint16(b[8:], uint16(len(p.children)))
313+ for i, c := range p.children {
314+ binary.LittleEndian.PutUint32(b[24+4*i:], c)
315+ }
316+ p.reply(0, b)
317+ case r.major == opGetWindowAttributes:
318+ a := p.attrs[binary.LittleEndian.Uint32(body)]
319+ b := make([]byte, 36) // reply length 3
320+ b[18] = a[1] // map_state is byte 26 of the reply
321+ b[19] = a[0] // override_redirect, byte 27
322+ p.reply(0, b)
323+ case r.major == opGetGeometry && p.bad[binary.LittleEndian.Uint32(body)]:
324+ p.errorPacket(9, r.major, binary.LittleEndian.Uint32(body)) // BadDrawable
325+ case r.major == opGetProperty && p.bad[binary.LittleEndian.Uint32(body)]:
326+ p.errorPacket(3, r.major, binary.LittleEndian.Uint32(body)) // BadWindow
327+ case r.major == opConfigureWindow:
328+ g := p.geoms[binary.LittleEndian.Uint32(body)]
329+ mask, vals := binary.LittleEndian.Uint16(body[4:]), body[8:]
330+ for bit, dst := 0, []any{&g.x, &g.y, &g.w, &g.h, &g.border}; bit < 5; bit++ {
331+ if mask&(1<<bit) == 0 {
332+ continue
333+ }
334+ v := uint16(binary.LittleEndian.Uint32(vals))
335+ vals = vals[4:]
336+ switch d := dst[bit].(type) {
337+ case *int16:
338+ *d = int16(v)
339+ case *uint16:
340+ *d = v
341+ }
342+ }
343+ p.geoms[binary.LittleEndian.Uint32(body)] = g
344+ case r.major == opGetGeometry:
345+ g := p.geoms[binary.LittleEndian.Uint32(body)]
346+ b := make([]byte, 24)
347+ binary.LittleEndian.PutUint32(b[0:], peerRoot)
348+ binary.LittleEndian.PutUint16(b[4:], uint16(g.x))
349+ binary.LittleEndian.PutUint16(b[6:], uint16(g.y))
350+ binary.LittleEndian.PutUint16(b[8:], g.w)
351+ binary.LittleEndian.PutUint16(b[10:], g.h)
352+ binary.LittleEndian.PutUint16(b[12:], g.border)
353+ p.reply(24, b)
354+ case r.major == opChangeProperty:
355+ win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:])
356+ format, n := body[12], int(binary.LittleEndian.Uint32(body[16:]))
357+ if p.props[win] == nil {
358+ p.props[win] = map[uint32]xpeerProp{}
359+ }
360+ p.props[win][prop] = xpeerProp{
361+ typ: binary.LittleEndian.Uint32(body[8:]), format: format,
362+ data: append([]byte(nil), body[20:20+n*int(format)/8]...),
363+ }
364+ case r.major == opDeleteProperty:
365+ delete(p.props[binary.LittleEndian.Uint32(body)], binary.LittleEndian.Uint32(body[4:]))
366+ case r.major == opGetProperty:
367+ win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:])
368+ v, ok := p.props[win][prop]
369+ b := make([]byte, 24+(len(v.data)+3)&^3)
370+ if ok && v.format != 0 {
371+ binary.LittleEndian.PutUint32(b[0:], v.typ)
372+ binary.LittleEndian.PutUint32(b[8:], uint32(len(v.data)/(int(v.format)/8)))
373+ copy(b[24:], v.data)
374+ }
375+ p.reply(v.format, b)
376+ case r.major == opChangeWindowAttributes:
377+ const cwEventMask = 1 << 11
378+ if p.denyRoot && binary.LittleEndian.Uint32(body) == peerRoot && binary.LittleEndian.Uint32(body[4:])&cwEventMask != 0 {
379+ p.errorPacket(10, opChangeWindowAttributes, peerRoot) // BadAccess
380+ }
381+ case r.major >= 128:
382+ switch p.extName(r.major) {
383+ case "BIG-REQUESTS":
384+ p.reply(0, le32(peerMaxWords))
385+ case "XC-MISC":
386+ if r.minor == 1 {
387+ p.reply(0, le32(0, 1))
388+ }
389+ case "Composite", "XFIXES":
390+ if r.minor == 0 {
391+ p.reply(0, le32(5, 4))
392+ }
393+ case "X-Resource":
394+ switch r.minor {
395+ case 0:
396+ p.reply(0, []byte{1, 0, 2, 0})
397+ case 4: // QueryClientIds: one spec, answered when a pid is scripted
398+ pid, ok := p.pids[r.u32(8)]
399+ if !ok {
400+ p.reply(0, le32(0))
401+ break
402+ }
403+ b := make([]byte, 40)
404+ binary.LittleEndian.PutUint32(b[0:], 1)
405+ binary.LittleEndian.PutUint32(b[24:], r.u32(8))
406+ binary.LittleEndian.PutUint32(b[28:], 2) // LocalClientPID
407+ binary.LittleEndian.PutUint32(b[32:], 4)
408+ binary.LittleEndian.PutUint32(b[36:], pid)
409+ p.reply(0, b)
410+ }
411+ }
412+ }
413+}
414+
415+// requests returns a snapshot of every request received after setup.
416+func (p *xpeer) requests() []xpeerReq {
417+ p.mu.Lock()
418+ defer p.mu.Unlock()
419+ return append([]xpeerReq(nil), p.reqs...)
420+}
421+
422+// waitReq waits for a request satisfying match among those after the first
423+// skip requests, and returns it with the new count.
424+func (p *xpeer) waitReq(skip int, match func(xpeerReq) bool) (xpeerReq, int) {
425+ p.t.Helper()
426+ deadline := time.Now().Add(5 * time.Second)
427+ for time.Now().Before(deadline) {
428+ reqs := p.requests()
429+ for i := skip; i < len(reqs); i++ {
430+ if match(reqs[i]) {
431+ return reqs[i], i + 1
432+ }
433+ }
434+ time.Sleep(time.Millisecond)
435+ }
436+ p.t.Fatal("xpeer: expected request never arrived")
437+ return xpeerReq{}, 0
438+}
439+
440+func (p *xpeer) atom(name string) uint32 {
441+ p.mu.Lock()
442+ defer p.mu.Unlock()
443+ return p.atoms[name]
444+}
445+
446+func (p *xpeer) prop(win uint32, atom string) (xpeerProp, bool) {
447+ p.mu.Lock()
448+ defer p.mu.Unlock()
449+ v, ok := p.props[win][p.atoms[atom]]
450+ return v, ok
451+}
452+
453+// event sends a 32-byte core event stamped with the current sequence number.
454+func (p *xpeer) event(code uint8, fill func(b *[32]byte)) {
455+ p.mu.Lock()
456+ defer p.mu.Unlock()
457+ var b [32]byte
458+ b[0] = code
459+ binary.LittleEndian.PutUint16(b[2:], p.seq)
460+ if fill != nil {
461+ fill(&b)
462+ }
463+ p.write(b[:])
464+}
465+
466+func (p *xpeer) createNotify(win uint32, override bool) {
467+ p.event(16, func(b *[32]byte) {
468+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
469+ binary.LittleEndian.PutUint32(b[8:], win)
470+ binary.LittleEndian.PutUint16(b[16:], 100)
471+ binary.LittleEndian.PutUint16(b[18:], 50)
472+ if override {
473+ b[22] = 1
474+ }
475+ })
476+}
477+
478+func (p *xpeer) destroyNotify(win uint32) {
479+ p.event(17, func(b *[32]byte) {
480+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
481+ binary.LittleEndian.PutUint32(b[8:], win)
482+ })
483+}
484+
485+// pause makes the peer stop reading after the request it is serving.
486+func (p *xpeer) pause() {
487+ g := make(chan struct{})
488+ p.gate.Store(&g)
489+}
490+
491+// resume lets the peer read again.
492+func (p *xpeer) resume() {
493+ g := *p.gate.Load()
494+ open := make(chan struct{})
495+ close(open)
496+ p.gate.Store(&open)
497+ select {
498+ case <-g:
499+ default:
500+ close(g)
501+ }
502+}
503+
504+// set mutates the peer's script under its lock.
505+func (p *xpeer) set(fn func(p *xpeer)) {
506+ p.mu.Lock()
507+ defer p.mu.Unlock()
508+ fn(p)
509+}
510+
511+// hangup closes the server end; the client reads EOF.
512+func (p *xpeer) hangup() {
513+ if rc, err := p.f.SyscallConn(); err == nil {
514+ _ = rc.Control(func(fd uintptr) { _ = unix.Shutdown(int(fd), unix.SHUT_RDWR) })
515+ }
516+}
517+
518+// setProp scripts a window property for GetProperty.
519+func (p *xpeer) setProp(win, atom, typ uint32, format uint8, data []byte) {
520+ p.mu.Lock()
521+ defer p.mu.Unlock()
522+ if p.props[win] == nil {
523+ p.props[win] = map[uint32]xpeerProp{}
524+ }
525+ p.props[win][atom] = xpeerProp{typ: typ, format: format, data: data}
526+}
527+
528+func (p *xpeer) mapRequest(win uint32) {
529+ p.event(20, func(b *[32]byte) {
530+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
531+ binary.LittleEndian.PutUint32(b[8:], win)
532+ })
533+}
534+
535+func (p *xpeer) mapNotify(win uint32) {
536+ p.event(19, func(b *[32]byte) {
537+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
538+ binary.LittleEndian.PutUint32(b[8:], win)
539+ })
540+}
541+
542+func (p *xpeer) unmapNotify(win uint32, synthetic bool) {
543+ p.event(18, func(b *[32]byte) {
544+ if synthetic {
545+ b[0] |= 0x80
546+ }
547+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
548+ binary.LittleEndian.PutUint32(b[8:], win)
549+ })
550+}
551+
552+func (p *xpeer) propertyNotify(win, atom uint32, deleted bool) {
553+ p.event(28, func(b *[32]byte) {
554+ binary.LittleEndian.PutUint32(b[4:], win)
555+ binary.LittleEndian.PutUint32(b[8:], atom)
556+ if deleted {
557+ b[16] = 1
558+ }
559+ })
560+}
561+
562+func (p *xpeer) reparentNotify(win, parent uint32) {
563+ p.event(21, func(b *[32]byte) {
564+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
565+ binary.LittleEndian.PutUint32(b[8:], win)
566+ binary.LittleEndian.PutUint32(b[12:], parent)
567+ })
568+}
569+
570+func (p *xpeer) clientMessage(win, typ uint32, data ...uint32) {
571+ p.event(33, func(b *[32]byte) {
572+ b[1] = 32
573+ binary.LittleEndian.PutUint32(b[4:], win)
574+ binary.LittleEndian.PutUint32(b[8:], typ)
575+ for i, d := range data {
576+ binary.LittleEndian.PutUint32(b[12+4*i:], d)
577+ }
578+ })
579+}
580+
581+func (p *xpeer) focusIn(win uint32) {
582+ p.event(9, func(b *[32]byte) { binary.LittleEndian.PutUint32(b[4:], win) })
583+}
584+
585+func (p *xpeer) configureNotify(win uint32) {
586+ p.event(22, func(b *[32]byte) {
587+ binary.LittleEndian.PutUint32(b[4:], win)
588+ binary.LittleEndian.PutUint32(b[8:], win)
589+ binary.LittleEndian.PutUint16(b[20:], 10)
590+ binary.LittleEndian.PutUint16(b[22:], 20)
591+ binary.LittleEndian.PutUint16(b[24:], 300)
592+ binary.LittleEndian.PutUint16(b[26:], 200)
593+ })
594+}
A internal/adapters/wayland/xwm/xwm.go +451 -0
@@ -0,0 +1,451 @@
1+// Package xwm is the built-in X11 window manager for Xwayland: it speaks the
2+// X protocol over the -wm socket with xturbo and reports managed windows to
3+// the Wayland server through the unexported host interface.
4+//
5+// Threading: Boot runs on the starter goroutine and blocks. The XWM it returns
6+// is then handed to the display goroutine, which owns it exclusively; nothing
7+// in this package is locked.
8+package xwm
9+
10+import (
11+ "context"
12+ "encoding/binary"
13+ "errors"
14+ "fmt"
15+ "io"
16+ "os"
17+
18+ "bnema.dev/xturbo"
19+ "bnema.dev/xturbo/composite"
20+ "bnema.dev/xturbo/res"
21+ "bnema.dev/xturbo/xfixes"
22+ "bnema.dev/xturbo/xproto"
23+ "github.com/bnema/purego-libwayland/server"
24+ "github.com/bnema/zerowrap"
25+ "golang.org/x/sys/unix"
26+)
27+
28+var (
29+ // ErrWMExists is returned by Boot when another client already redirects
30+ // the root window's children.
31+ ErrWMExists = errors.New("xwm: another window manager owns the root window")
32+ // ErrNoComposite is returned by Boot when the X server lacks Composite.
33+ ErrNoComposite = errors.New("xwm: X server has no Composite extension")
34+)
35+
36+// Stats are connection and event counters, for diagnostics.
37+type Stats struct {
38+ Events, Replies, Errors, Unattributed, Stale, RingFull, LegacyID, UnmanagedSeen uint64
39+}
40+
41+// Version request ids, carried in the first Expect argument of kindVersion.
42+const (
43+ versionComposite uint32 = iota + 1
44+ versionXFixes
45+ versionRes
46+)
47+
48+const unpairedCap = 64
49+
50+// XWM manages the windows of one Xwayland. Display goroutine only, after Boot.
51+type XWM struct {
52+ conn *xturbo.Conn
53+ log zerowrap.Logger
54+ root uint32
55+ wmWin uint32
56+ atoms [atomCount]uint32
57+ tab *table
58+ h xturbo.Handler
59+ stats Stats
60+
61+ // attachment
62+ host host
63+ src fdSource
64+ post func(func())
65+ flushFn func()
66+
67+ posted, writable, closed bool
68+ bootErr error
69+ warnedRing, warnedFull bool
70+ warnedUnpaired bool
71+
72+ scratch []byte // encode buffer for 32-bit property values, 4 bytes per slot
73+
74+ // Shared by the map, configure and focus flows.
75+ dirty []Slot // slots with a pending configure, capacity == table size
76+ clients []uint32 // _NET_CLIENT_LIST, map order
77+ stacking []uint32 // _NET_CLIENT_LIST_STACKING, bottom to top
78+ active Slot // slot named by _NET_ACTIVE_WINDOW, 0 = none
79+ refocus bool // a FocusIn was denied; focus is re-issued in flush
80+ listsDirty uint8 // client lists to rewrite in flush
81+ unpaired [unpairedCap]struct{ serial, surface uint64 }
82+ unpairedN int
83+
84+ space *Space // layout <-> X coordinates, shared with the host
85+ warnedSpace bool // more outputs than the Space holds, logged once
86+ ovDirty []Slot // mapped unmanaged slots whose geometry changed, capacity == table size
87+}
88+
89+func newXWM(conn *xturbo.Conn, log zerowrap.Logger) *XWM {
90+ x := &XWM{conn: conn, log: log, root: conn.Root(), tab: newTable(initialSlots), space: new(Space)}
91+ x.h = xturbo.Handler{OnEvent: x.onEvent, OnReply: x.onReply, OnError: x.onError}
92+ x.fit()
93+ return x
94+}
95+
96+// fit sizes the per-slot buffers to the table; called after every insert.
97+func (x *XWM) fit() {
98+ n := len(x.tab.recs)
99+ if cap(x.dirty) < n {
100+ x.dirty = append(make([]Slot, 0, n), x.dirty...)
101+ x.clients = append(make([]uint32, 0, n), x.clients...)
102+ x.ovDirty = append(make([]Slot, 0, n), x.ovDirty...)
103+ x.stacking = append(make([]uint32, 0, n), x.stacking...)
104+ x.scratch = make([]byte, 0, 4*n)
105+ }
106+}
107+
108+// insert adds a record for xid; 0 means the table is full.
109+func (x *XWM) insert(xid uint32) Slot {
110+ s := x.tab.insert(xid)
111+ if s == 0 {
112+ if !x.warnedFull {
113+ x.warnedFull = true
114+ x.log.Warn().Int("windows", x.tab.n).Msg("X11 window table full, window ignored")
115+ }
116+ return 0
117+ }
118+ x.fit()
119+ return s
120+}
121+
122+// expect registers seq, tolerating a full ring: the reply then arrives
123+// unattributed and is dropped.
124+func (x *XWM) expect(seq xturbo.Seq, kind uint16, a0, a1 uint32) {
125+ if seq == 0 {
126+ return
127+ }
128+ if err := x.conn.Expect(seq, kind, a0, a1, 0, 0); err != nil {
129+ x.stats.RingFull++
130+ if !x.warnedRing {
131+ x.warnedRing = true
132+ x.log.Warn().Err(err).Msg("X11 request not tracked")
133+ }
134+ }
135+}
136+
137+// setProp32 queues ChangeProperty (replace, format 32) for vals.
138+func (x *XWM) setProp32(win uint32, prop int, typ xproto.Atom, vals []uint32) {
139+ n := 4 * len(vals)
140+ if cap(x.scratch) < n {
141+ x.scratch = make([]byte, 0, n)
142+ }
143+ p := x.scratch[:n]
144+ for i, v := range vals {
145+ binary.LittleEndian.PutUint32(p[4*i:], v)
146+ }
147+ xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(win),
148+ xproto.Atom(x.atoms[prop]), typ, 32, uint32(len(vals)), p)
149+}
150+
151+// Boot connects to the X server on wmFD and becomes its window manager. It
152+// blocks (about 25 s at worst) and must not run on the display goroutine: the
153+// handshake needs Xwayland, which needs the display goroutine to serve its
154+// Wayland requests. It takes ownership of wmFD and closes it on failure.
155+// Boot gives up promptly when ctx ends (the Xwayland process died or the run
156+// was cancelled). log is scoped by the caller with logging.For(ctx, "xwm").
157+func Boot(ctx context.Context, wmFD int, log zerowrap.Logger) (*XWM, error) {
158+ // The watcher shuts the socket down when ctx ends, which wakes any blocked
159+ // handshake, Sync or InternAtoms. It holds its own duplicate so a shutdown
160+ // can never reach a descriptor number reused after Boot closed wmFD.
161+ var watch *os.File
162+ if dup, err := unix.FcntlInt(uintptr(wmFD), unix.F_DUPFD_CLOEXEC, 0); err == nil {
163+ watch = os.NewFile(uintptr(dup), "xwm abort")
164+ }
165+ stopWatch := func() {}
166+ if watch != nil {
167+ quit, joined := make(chan struct{}), make(chan struct{})
168+ go func() {
169+ defer close(joined)
170+ select {
171+ case <-ctx.Done():
172+ if rc, err := watch.SyscallConn(); err == nil {
173+ _ = rc.Control(func(fd uintptr) { _ = unix.Shutdown(int(fd), unix.SHUT_RDWR) })
174+ }
175+ case <-quit:
176+ }
177+ _ = watch.Close()
178+ }()
179+ stopWatch = func() { close(quit); <-joined }
180+ }
181+ x, err := boot(wmFD, log)
182+ stopWatch()
183+ if err == nil && ctx.Err() != nil {
184+ x.CloseConn()
185+ x, err = nil, ctx.Err()
186+ }
187+ if err != nil && ctx.Err() != nil {
188+ err = fmt.Errorf("%w (%w)", err, ctx.Err())
189+ }
190+ return x, err
191+}
192+
193+func boot(wmFD int, log zerowrap.Logger) (*XWM, error) {
194+ conn, err := xturbo.NewConn(wmFD)
195+ if err != nil {
196+ return nil, fmt.Errorf("xwm: connect: %w", err)
197+ }
198+ x := newXWM(conn, log)
199+ if err := x.boot(); err != nil {
200+ _ = conn.Close()
201+ return nil, err
202+ }
203+ return x, nil
204+}
205+
206+func (x *XWM) boot() error {
207+ conn := x.conn
208+ ext := conn.Ext()
209+ if !ext.Info(xturbo.ExtComposite).Present {
210+ return ErrNoComposite
211+ }
212+ var visual, depth uint32
213+ if it := xproto.NewSetupIter(conn.SetupBytes(), 1); it.Next() {
214+ if roots := it.Value().Roots(); roots.Next() {
215+ visual, depth = uint32(roots.Value().RootVisual()), uint32(roots.Value().RootDepth())
216+ }
217+ }
218+ x.log.Info().Uint32("root", x.root).Uint32("visual", visual).Uint32("depth", depth).
219+ Bool("xfixes", ext.Info(xturbo.ExtXFixes).Present).
220+ Bool("res", ext.Info(xturbo.ExtRes).Present).Msg("X server")
221+
222+ if err := conn.InternAtoms(atomNames[:], false, x.atoms[:]); err != nil {
223+ return fmt.Errorf("xwm: intern atoms: %w", err)
224+ }
225+ xid, err := conn.NewXID()
226+ if err != nil {
227+ return fmt.Errorf("xwm: allocate window id: %w", err)
228+ }
229+ x.wmWin = xid
230+
231+ b := conn.Buf()
232+ x.expect(composite.QueryVersion(b, 0, 4), kindVersion, versionComposite, 0)
233+ if ext.Info(xturbo.ExtXFixes).Present {
234+ x.expect(xfixes.QueryVersion(b, 6, 0), kindVersion, versionXFixes, 0)
235+ }
236+ if ext.Info(xturbo.ExtRes).Present {
237+ x.expect(res.QueryVersion(b, 1, 2), kindVersion, versionRes, 0)
238+ }
239+
240+ root := xproto.Window(x.root)
241+ var rootMask xproto.ChangeWindowAttributesValues
242+ rootMask.SetEventMask(xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange)
243+ x.expect(xproto.ChangeWindowAttributes(b, root, &rootMask), kindRootMask, 0, 0)
244+ // Only one client may select SubstructureRedirect on the root: wait for
245+ // the answer before touching anything another window manager owns.
246+ if err := b.Err(); err != nil {
247+ return fmt.Errorf("xwm: encode root mask: %w", err)
248+ }
249+ if err := conn.Sync(&x.h); err != nil {
250+ return fmt.Errorf("xwm: root mask: %w", err)
251+ }
252+ if x.bootErr != nil {
253+ return x.bootErr
254+ }
255+ composite.RedirectSubwindows(b, root, composite.RedirectManual)
256+
257+ var wv xproto.CreateWindowValues
258+ wv.SetEventMask(0)
259+ xproto.CreateWindow(b, 0, xproto.Window(x.wmWin), root, -1, -1, 1, 1, 0, xproto.WindowClassInputOutput, 0, &wv)
260+ name := []byte("neferwl")
261+ xproto.ChangeProperty(b, xproto.PropModeReplace, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomNetWMName]),
262+ xproto.Atom(x.atoms[atomUTF8String]), 8, uint32(len(name)), name)
263+ x.setProp32(x.root, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin})
264+ x.setProp32(x.wmWin, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin})
265+ // CurrentTime and no MANAGER announcement: this Xwayland is private to
266+ // neferwl and nobody waits on these selections.
267+ xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomWMS0]), 0)
268+ xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomNetWMCMS0]), 0)
269+
270+ supported := [...]int{
271+ atomNetWMState, atomNetActiveWindow, atomNetWMStateFocused,
272+ atomNetWMStateModal, atomNetWMStateFullscreen, atomNetClientList,
273+ atomNetClientListStacking, atomNetSupportingWMCheck, atomNetWMName, atomNetWMPID,
274+ atomNetWMWindowType,
275+ }
276+ var list [len(supported)]uint32
277+ for i, a := range supported {
278+ list[i] = x.atoms[a]
279+ }
280+ x.setProp32(x.root, atomNetSupported, xproto.AtomAtom, list[:])
281+ x.setProp32(x.root, atomNetActiveWindow, xproto.AtomWindow, []uint32{0})
282+ x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, nil)
283+ x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, nil)
284+ xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.WindowNone, 0)
285+ if err := b.Err(); err != nil {
286+ return fmt.Errorf("xwm: encode setup requests: %w", err)
287+ }
288+ if err := conn.Sync(&x.h); err != nil {
289+ return fmt.Errorf("xwm: root setup: %w", err)
290+ }
291+ if x.bootErr != nil {
292+ return x.bootErr
293+ }
294+
295+ // Windows that exist already (Xwayland restarted under a live client set).
296+ x.expect(xproto.QueryTree(b, root), kindTree, 0, 0)
297+ if err := conn.Sync(&x.h); err != nil {
298+ return fmt.Errorf("xwm: query tree: %w", err)
299+ }
300+ return x.bootErr
301+}
302+
303+// Attach registers the X socket with the display and hands the windows found
304+// at boot to h. Display goroutine only.
305+func (x *XWM) Attach(d *server.Display, h host) error {
306+ src, err := d.AddFDRW(x.conn.Fd(), x.onReadable, x.onWritable)
307+ if err != nil {
308+ return fmt.Errorf("xwm: watch X socket: %w", err)
309+ }
310+ x.attach(src, h, d.Post)
311+ return nil
312+}
313+
314+// attach is Attach over its seams: the event source and the display's Post.
315+func (x *XWM) attach(src fdSource, h host, post func(func())) {
316+ x.src, x.host, x.post = src, h, post
317+ x.flushFn = func() {
318+ x.posted = false
319+ x.flush()
320+ }
321+ for i := range x.tab.recs {
322+ r := &x.tab.recs[i]
323+ if r.kind == kindUnmanaged && r.mapped && !r.ready { // mapped before Attach
324+ x.requestOverrideType(Slot(i + 1))
325+ }
326+ if r.kind != kindManaged || r.id != 0 {
327+ continue
328+ }
329+ s := Slot(i + 1)
330+ x.adopt(s)
331+ if r.mapping { // MapRequest or viewable before Attach
332+ r.mapping = false
333+ x.beginMap(s)
334+ }
335+ }
336+ x.flush()
337+}
338+
339+// Close detaches from the display, drops every window from the host and closes
340+// the X connection. Idempotent.
341+func (x *XWM) Close() {
342+ if x.closed {
343+ return
344+ }
345+ x.closed = true
346+ s := x.stats
347+ x.log.Info().Uint64("events", s.Events).Uint64("replies", s.Replies).Uint64("errors", s.Errors).
348+ Uint64("unattributed", s.Unattributed).Uint64("stale", s.Stale).Uint64("ring_full", s.RingFull).
349+ Uint64("legacy_id", s.LegacyID).Uint64("unmanaged_seen", s.UnmanagedSeen).Msg("X11 window management closed")
350+ if x.src != nil {
351+ x.src.Remove()
352+ x.src = nil
353+ }
354+ if x.host != nil {
355+ for i := range x.tab.recs {
356+ r := &x.tab.recs[i]
357+ switch {
358+ case r.kind == kindManaged && r.id != 0:
359+ r.id = 0
360+ x.host.Dissociate(Slot(i+1), true)
361+ case r.kind == kindUnmanaged && r.ovPaired:
362+ r.ovPaired = false
363+ x.host.OverrideGone(Slot(i + 1))
364+ }
365+ }
366+ }
367+ x.unpairedN = 0
368+ _ = x.conn.Close()
369+}
370+
371+// CloseConn closes the connection of an XWM that was never attached.
372+func (x *XWM) CloseConn() {
373+ if !x.closed {
374+ x.closed = true
375+ _ = x.conn.Close()
376+ }
377+}
378+
379+// fail ends window management after an unrecoverable connection error. Xwayland
380+// keeps running; its windows no longer map.
381+func (x *XWM) fail(err error) {
382+ if x.closed {
383+ return
384+ }
385+ if errors.Is(err, io.EOF) || errors.Is(err, xturbo.ErrClosed) {
386+ x.log.Info().Err(err).Msg("X11 window management stopped: Xwayland closed the connection")
387+ } else {
388+ x.log.Warn().Err(err).Msg("X11 window management stopped")
389+ }
390+ x.Close()
391+}
392+
393+// Stats returns the counters; Close logs them.
394+func (x *XWM) Stats() Stats { return x.stats }
395+
396+func (x *XWM) onReadable() {
397+ if x.closed {
398+ return
399+ }
400+ if err := x.conn.Dispatch(&x.h); err != nil {
401+ x.fail(err)
402+ return
403+ }
404+ x.flush()
405+}
406+
407+func (x *XWM) onWritable() { x.flush() }
408+
409+// wake schedules one flush at the end of the current display iteration.
410+func (x *XWM) wake() {
411+ if x.posted || x.closed || x.post == nil {
412+ return
413+ }
414+ x.posted = true
415+ x.post(x.flushFn)
416+}
417+
418+// flush sends what the handlers queued: pending configures, an XID prefetch,
419+// then the socket, arming POLLOUT only while it is full.
420+func (x *XWM) flush() {
421+ if x.closed {
422+ return
423+ }
424+ x.flushConfigures()
425+ if x.conn.XIDsLow() {
426+ _ = x.conn.PrefetchXIDs()
427+ }
428+ err := x.conn.Flush()
429+ switch {
430+ case err == nil:
431+ if x.writable {
432+ x.setWritable(false)
433+ }
434+ case errors.Is(err, xturbo.ErrWouldBlock):
435+ if !x.writable {
436+ x.setWritable(true)
437+ }
438+ default:
439+ x.fail(err)
440+ }
441+}
442+
443+func (x *XWM) setWritable(on bool) {
444+ x.writable = on
445+ if x.src == nil {
446+ return
447+ }
448+ if err := x.src.SetWritable(on); err != nil {
449+ x.fail(err)
450+ }
451+}
A internal/adapters/wayland/xwm/xwm_test.go +381 -0
@@ -0,0 +1,381 @@
1+package xwm
2+
3+import (
4+ "context"
5+ "encoding/binary"
6+ "os"
7+ "strings"
8+ "testing"
9+ "time"
10+
11+ "bnema.dev/xturbo/xproto"
12+ "github.com/bnema/neferwl/internal/ports"
13+ "github.com/bnema/zerowrap"
14+ "github.com/rs/zerolog"
15+ "github.com/stretchr/testify/mock"
16+ "github.com/stretchr/testify/require"
17+)
18+
19+func testLog() zerowrap.Logger { return zerowrap.Logger{Logger: zerolog.Nop()} }
20+
21+func bootPeer(t *testing.T, mutate func(p *xpeer)) (*XWM, *xpeer) {
22+ t.Helper()
23+ fd, p := startXPeer(t, fullPeerExts())
24+ if mutate != nil {
25+ p.set(mutate)
26+ }
27+ x, err := Boot(context.Background(), fd, testLog())
28+ require.NoError(t, err)
29+ t.Cleanup(x.CloseConn)
30+ return x, p
31+}
32+
33+func atomsOf(p xpeerProp) []uint32 {
34+ out := make([]uint32, len(p.data)/4)
35+ for i := range out {
36+ out[i] = binary.LittleEndian.Uint32(p.data[4*i:])
37+ }
38+ return out
39+}
40+
41+func TestBootRequests(t *testing.T) {
42+ x, p := bootPeer(t, nil)
43+ reqs := p.requests()
44+
45+ first := -1
46+ for i, r := range reqs {
47+ if r.major == opInternAtom {
48+ first = i
49+ break
50+ }
51+ }
52+ require.NotEqual(t, -1, first)
53+ for i := 0; i < atomCount; i++ {
54+ r := reqs[first+i]
55+ require.EqualValues(t, opInternAtom, r.major, i)
56+ n := int(binary.LittleEndian.Uint16(r.raw[4:]))
57+ require.Equal(t, atomNames[i], string(r.raw[8:8+n]), i)
58+ require.Equal(t, p.atom(atomNames[i]), x.atoms[i])
59+ }
60+
61+ // Root mask, then composite redirect, then the WM window and ownership.
62+ var rootMask, redirect, create, s0, cm, focus int
63+ rootMask, redirect, create, s0, cm, focus = -1, -1, -1, -1, -1, -1
64+ wm := x.wmWin
65+ for i := first + atomCount; i < len(reqs); i++ {
66+ r := reqs[i]
67+ switch {
68+ case r.major == opChangeWindowAttributes && r.u32(4) == peerRoot:
69+ rootMask = i
70+ require.EqualValues(t, 1<<11, r.u32(8))
71+ want := uint32(xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange)
72+ require.Equal(t, want, r.u32(12))
73+ case r.major == 142 && r.minor == 2:
74+ redirect = i
75+ require.EqualValues(t, peerRoot, r.u32(4))
76+ require.EqualValues(t, 1, r.raw[8], "manual")
77+ case r.major == opCreateWindow:
78+ create = i
79+ require.Equal(t, wm, r.u32(4))
80+ case r.major == opSetSelectionOwner && r.u32(8) == p.atom("WM_S0"):
81+ s0 = i
82+ require.Equal(t, wm, r.u32(4))
83+ case r.major == opSetSelectionOwner && r.u32(8) == p.atom("_NET_WM_CM_S0"):
84+ cm = i
85+ case r.major == opSetInputFocus:
86+ focus = i
87+ require.EqualValues(t, 0, r.u32(4), "focus None")
88+ }
89+ }
90+ for name, i := range map[string]int{"rootMask": rootMask, "redirect": redirect, "create": create, "WM_S0": s0, "CM_S0": cm, "focus": focus} {
91+ require.NotEqual(t, -1, i, name)
92+ }
93+ require.Less(t, rootMask, redirect)
94+ require.Less(t, redirect, create)
95+ require.Less(t, create, s0)
96+
97+ sup, ok := p.prop(peerRoot, "_NET_SUPPORTED")
98+ require.True(t, ok)
99+ var names []string
100+ byAtom := map[uint32]string{}
101+ for n, a := range p.atoms {
102+ byAtom[a] = n
103+ }
104+ for _, a := range atomsOf(sup) {
105+ names = append(names, byAtom[a])
106+ }
107+ require.Equal(t, []string{
108+ "_NET_WM_STATE", "_NET_ACTIVE_WINDOW", "_NET_WM_STATE_FOCUSED",
109+ "_NET_WM_STATE_MODAL", "_NET_WM_STATE_FULLSCREEN", "_NET_CLIENT_LIST",
110+ "_NET_CLIENT_LIST_STACKING", "_NET_SUPPORTING_WM_CHECK", "_NET_WM_NAME", "_NET_WM_PID",
111+ "_NET_WM_WINDOW_TYPE",
112+ }, names)
113+ require.NotContains(t, names, "_NET_WM_MOVERESIZE")
114+ require.NotContains(t, names, "_NET_WM_STATE_MAXIMIZED_VERT")
115+
116+ check, ok := p.prop(peerRoot, "_NET_SUPPORTING_WM_CHECK")
117+ require.True(t, ok)
118+ require.Equal(t, []uint32{wm}, atomsOf(check))
119+ check, ok = p.prop(wm, "_NET_SUPPORTING_WM_CHECK")
120+ require.True(t, ok)
121+ require.Equal(t, []uint32{wm}, atomsOf(check))
122+ name, ok := p.prop(wm, "_NET_WM_NAME")
123+ require.True(t, ok)
124+ require.Equal(t, "neferwl", string(name.data))
125+ active, ok := p.prop(peerRoot, "_NET_ACTIVE_WINDOW")
126+ require.True(t, ok)
127+ require.Equal(t, []uint32{0}, atomsOf(active))
128+ for _, l := range []string{"_NET_CLIENT_LIST", "_NET_CLIENT_LIST_STACKING"} {
129+ v, ok := p.prop(peerRoot, l)
130+ require.True(t, ok, l)
131+ require.Empty(t, v.data, l)
132+ }
133+}
134+
135+func TestBootWMExists(t *testing.T) {
136+ fd, p := startXPeer(t, fullPeerExts())
137+ p.set(func(p *xpeer) { p.denyRoot = true })
138+ x, err := Boot(context.Background(), fd, testLog())
139+ require.ErrorIs(t, err, ErrWMExists)
140+ require.Nil(t, x)
141+ // Nothing the running window manager owns was touched.
142+ for _, r := range p.requests() {
143+ require.NotEqual(t, uint8(opSetSelectionOwner), r.major)
144+ if r.major == opChangeProperty {
145+ require.NotEqual(t, uint32(peerRoot), r.u32(4))
146+ }
147+ }
148+}
149+
150+func TestBootNoComposite(t *testing.T) {
151+ exts := fullPeerExts()
152+ delete(exts, "Composite")
153+ fd, _ := startXPeer(t, exts)
154+ _, err := Boot(context.Background(), fd, testLog())
155+ require.ErrorIs(t, err, ErrNoComposite)
156+}
157+
158+func TestBootOptionalExtensions(t *testing.T) {
159+ exts := fullPeerExts()
160+ delete(exts, "XFIXES")
161+ delete(exts, "X-Resource")
162+ fd, _ := startXPeer(t, exts)
163+ x, err := Boot(context.Background(), fd, testLog())
164+ require.NoError(t, err)
165+ x.CloseConn()
166+}
167+
168+func TestBootConnectFailure(t *testing.T) {
169+ fd, p := startXPeer(t, fullPeerExts())
170+ p.hangup()
171+ _, err := Boot(context.Background(), fd, testLog())
172+ require.Error(t, err)
173+}
174+
175+func TestBootExistingWindows(t *testing.T) {
176+ x, p := bootPeer(t, func(p *xpeer) {
177+ p.children = []uint32{0x400001, 0x400002}
178+ p.attrs[0x400002] = [2]byte{1, 2} // override-redirect, viewable
179+ })
180+ _ = p
181+ require.NotZero(t, x.tab.lookup(0x400001))
182+ require.NotZero(t, x.tab.lookup(0x400002))
183+ require.Equal(t, kindManaged, x.tab.rec(x.tab.lookup(0x400001)).kind)
184+ require.Equal(t, kindUnmanaged, x.tab.rec(x.tab.lookup(0x400002)).kind)
185+}
186+
187+// readUntil dispatches the socket until cond holds.
188+func readUntil(t *testing.T, x *XWM, cond func() bool) {
189+ t.Helper()
190+ deadline := time.Now().Add(5 * time.Second)
191+ for !cond() {
192+ if time.Now().After(deadline) {
193+ t.Fatal("condition never held")
194+ }
195+ x.onReadable()
196+ time.Sleep(time.Millisecond)
197+ }
198+}
199+
200+func newPost() (post func(func()), runs *[]func()) {
201+ var q []func()
202+ return func(f func()) { q = append(q, f) }, &q
203+}
204+
205+func TestAttachAnnouncesWindowsFoundAtBoot(t *testing.T) {
206+ x, _ := bootPeer(t, func(p *xpeer) {
207+ p.children = []uint32{0x400001, 0x400002}
208+ p.attrs[0x400002] = [2]byte{1, 2}
209+ })
210+ h := newMockhost(t)
211+ src := newMockfdSource(t)
212+ h.EXPECT().Created(x.tab.lookup(0x400001), uint32(0x400001)).Return(ports.WindowID(7)).Once()
213+ post, _ := newPost()
214+ x.attach(src, h, post)
215+ require.Equal(t, ports.WindowID(7), x.tab.rec(x.tab.lookup(0x400001)).id)
216+}
217+
218+func attached(t *testing.T) (*XWM, *xpeer, *mockhost, *mockfdSource, *[]func()) {
219+ t.Helper()
220+ x, p := bootPeer(t, nil)
221+ h := newMockhost(t)
222+ src := newMockfdSource(t)
223+ post, runs := newPost()
224+ x.attach(src, h, post)
225+ return x, p, h, src, runs
226+}
227+
228+func TestCreateAndDestroyManaged(t *testing.T) {
229+ x, p, h, src, _ := attached(t)
230+ const win = 0x500001
231+ h.EXPECT().Created(mock.Anything, uint32(win)).Return(ports.WindowID(3)).Once()
232+ p.createNotify(win, false)
233+ readUntil(t, x, func() bool { return x.tab.lookup(win) != 0 })
234+ s := x.tab.lookup(win)
235+ require.Equal(t, kindManaged, x.tab.rec(s).kind)
236+ require.Equal(t, ports.WindowID(3), x.tab.rec(s).id)
237+
238+ // The window is watched for property and focus changes.
239+ req, _ := p.waitReq(0, func(r xpeerReq) bool { return r.major == opChangeWindowAttributes && r.u32(4) == win })
240+ require.EqualValues(t, xproto.EventMaskPropertyChange|xproto.EventMaskFocusChange, req.u32(12))
241+
242+ h.EXPECT().Dissociate(s, true).Once()
243+ p.destroyNotify(win)
244+ readUntil(t, x, func() bool { return x.tab.lookup(win) == 0 })
245+ require.Zero(t, x.tab.n)
246+ _ = src
247+}
248+
249+// The X server reports the XWM's own check window to it like any other
250+// child of the root; it is never adopted.
251+func TestOwnWindowNeverAdopted(t *testing.T) {
252+ x, p, _, _, _ := attached(t) // the mock fails the test on any host call
253+ p.createNotify(x.wmWin, false)
254+ const marker = 0x500003
255+ p.createNotify(marker, true)
256+ readUntil(t, x, func() bool { return x.tab.lookup(marker) != 0 })
257+ require.Zero(t, x.tab.lookup(x.wmWin))
258+}
259+
260+func TestOverrideRedirectNeverReachesHost(t *testing.T) {
261+ x, p, _, _, _ := attached(t) // the mock fails the test on any host call
262+ p.createNotify(0x500002, true)
263+ readUntil(t, x, func() bool { return x.tab.lookup(0x500002) != 0 })
264+ require.Equal(t, kindUnmanaged, x.tab.rec(x.tab.lookup(0x500002)).kind)
265+ require.EqualValues(t, 1, x.Stats().UnmanagedSeen)
266+ p.destroyNotify(0x500002)
267+ readUntil(t, x, func() bool { return x.tab.lookup(0x500002) == 0 })
268+}
269+
270+func TestCloseDissociatesEverything(t *testing.T) {
271+ x, p, h, src, _ := attached(t)
272+ for _, win := range []uint32{0x500001, 0x500002} {
273+ h.EXPECT().Created(mock.Anything, win).Return(ports.WindowID(win)).Once()
274+ p.createNotify(win, false)
275+ }
276+ p.createNotify(0x500003, true)
277+ readUntil(t, x, func() bool { return x.tab.lookup(0x500003) != 0 })
278+ h.EXPECT().Dissociate(x.tab.lookup(0x500001), true).Once()
279+ h.EXPECT().Dissociate(x.tab.lookup(0x500002), true).Once()
280+ src.EXPECT().Remove().Once()
281+ x.Close()
282+ x.Close() // idempotent
283+ require.True(t, x.closed)
284+ require.Equal(t, -1, x.conn.Fd())
285+}
286+
287+func TestEOFClosesXWM(t *testing.T) {
288+ x, p, h, src, _ := attached(t)
289+ h.EXPECT().Created(mock.Anything, uint32(0x500001)).Return(ports.WindowID(1)).Once()
290+ p.createNotify(0x500001, false)
291+ readUntil(t, x, func() bool { return x.tab.lookup(0x500001) != 0 })
292+ h.EXPECT().Dissociate(x.tab.lookup(0x500001), true).Once()
293+ src.EXPECT().Remove().Once()
294+ p.hangup()
295+ readUntil(t, x, func() bool { return x.closed })
296+}
297+
298+func TestWakeCoalescesFlush(t *testing.T) {
299+ x, _, _, _, runs := attached(t)
300+ x.wake()
301+ x.wake()
302+ x.wake()
303+ require.Len(t, *runs, 1)
304+ (*runs)[0]()
305+ x.wake()
306+ require.Len(t, *runs, 2)
307+}
308+
309+func TestWriteBackpressure(t *testing.T) {
310+ x, p, _, src, _ := attached(t)
311+ p.pause()
312+ b := x.conn.Buf()
313+ big := make([]byte, 64<<10)
314+ for i := 0; i < 64; i++ { // 4 MiB: more than any socket buffer
315+ xproto.ChangeProperty(b, xproto.PropModeReplace, xproto.Window(x.root), xproto.AtomWmName, xproto.AtomString, 8, uint32(len(big)), big)
316+ }
317+ src.EXPECT().SetWritable(true).Return(nil).Once()
318+ x.flush()
319+ require.True(t, x.writable)
320+ x.flush() // still blocked: SetWritable is not called again
321+ x.onWritable()
322+ require.True(t, x.writable)
323+
324+ drained := false
325+ src.EXPECT().SetWritable(false).RunAndReturn(func(bool) error { drained = true; return nil }).Once()
326+ p.resume()
327+ deadline := time.Now().Add(10 * time.Second)
328+ for !drained {
329+ require.True(t, time.Now().Before(deadline), "never drained")
330+ x.onWritable()
331+ time.Sleep(time.Millisecond)
332+ }
333+ require.False(t, x.writable)
334+}
335+
336+func TestFlushWithNothingQueuedSendsNothing(t *testing.T) {
337+ x, p, _, _, _ := attached(t)
338+ n := len(p.requests())
339+ x.flush()
340+ time.Sleep(20 * time.Millisecond)
341+ require.Len(t, p.requests(), n, "nothing queued, nothing sent")
342+}
343+
344+// The XWM never blocks after Boot: NewXID, Sync and InternAtoms are startup
345+// only, so they may appear in production code only inside boot().
346+func TestNoBlockingCallsOutsideBoot(t *testing.T) {
347+ files, err := os.ReadDir(".")
348+ require.NoError(t, err)
349+ for _, f := range files {
350+ name := f.Name()
351+ if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
352+ continue
353+ }
354+ src, err := os.ReadFile(name)
355+ require.NoError(t, err)
356+ text := string(src)
357+ lo, hi := 0, 0
358+ if name == "xwm.go" {
359+ lo = strings.Index(text, "func (x *XWM) boot() error")
360+ hi = strings.Index(text, "// Attach registers")
361+ require.Greater(t, hi, lo)
362+ }
363+ for _, call := range []string{".NewXID(", ".Sync(", ".InternAtoms("} {
364+ for _, at := range allIndex(text, call) {
365+ require.True(t, name == "xwm.go" && at > lo && at < hi, "%s: %s outside boot", name, call)
366+ }
367+ }
368+ }
369+}
370+
371+func allIndex(s, sub string) []int {
372+ var out []int
373+ for off := 0; ; {
374+ i := strings.Index(s[off:], sub)
375+ if i < 0 {
376+ return out
377+ }
378+ out = append(out, off+i)
379+ off += i + len(sub)
380+ }
381+}
A internal/adapters/xwayland/builtin.go +216 -0
@@ -0,0 +1,216 @@
1+package xwayland
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "os"
8+ "os/exec"
9+ "strconv"
10+ "strings"
11+ "syscall"
12+ "time"
13+
14+ "golang.org/x/sys/unix"
15+)
16+
17+type builtinStarter struct{}
18+
19+func (builtinStarter) start(ctx context.Context, d *Display, stderr *logWriter) error {
20+ // Files own all setup descriptors until Start succeeds and Attach takes
21+ // the Wayland socket. This also unwinds partial setup failures.
22+ var files []*os.File
23+ defer func() {
24+ for _, f := range files {
25+ if f != nil {
26+ f.Close()
27+ }
28+ }
29+ }()
30+ pair := func(name string) (int, error) {
31+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
32+ if err != nil {
33+ return 0, err
34+ }
35+ i := len(files)
36+ files = append(files, os.NewFile(uintptr(fds[0]), name+" parent"), os.NewFile(uintptr(fds[1]), name+" child"))
37+ return i, nil
38+ }
39+ wl, err := pair("wayland")
40+ if err != nil {
41+ return err
42+ }
43+ wm, err := pair("wm")
44+ if err != nil {
45+ return err
46+ }
47+ var pipe [2]int
48+ if err := unix.Pipe2(pipe[:], unix.O_CLOEXEC); err != nil {
49+ return err
50+ }
51+ rd := os.NewFile(uintptr(pipe[0]), "displayfd read")
52+ wr := os.NewFile(uintptr(pipe[1]), "displayfd write")
53+ files = append(files, rd, wr)
54+ args := []string{d.Name(), "-rootless"}
55+ for i := range d.sockets {
56+ args = append(args, "-listenfd", strconv.Itoa(3+i))
57+ }
58+ wlChild := 3 + len(d.sockets)
59+ args = append(args, "-wm", strconv.Itoa(wlChild+1), "-displayfd", strconv.Itoa(wlChild+2))
60+ cmd := exec.Command(d.opts.Binary, args...)
61+ for _, e := range d.opts.Env {
62+ if !strings.HasPrefix(e, "WAYLAND_DISPLAY=") && !strings.HasPrefix(e, "WAYLAND_SOCKET=") {
63+ cmd.Env = append(cmd.Env, e)
64+ }
65+ }
66+ cmd.Env = append(cmd.Env, "WAYLAND_SOCKET="+strconv.Itoa(wlChild))
67+ cmd.ExtraFiles = append(append([]*os.File(nil), d.sockets...), files[wl+1], files[wm+1], wr)
68+ cmd.Stderr = stderr
69+ cmd.WaitDelay = 2 * time.Second
70+ cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
71+ if err := cmd.Start(); err != nil {
72+ return err
73+ }
74+ files[wl+1].Close()
75+ files[wm+1].Close()
76+ wr.Close()
77+ d.opts.Log.Info().Str("display", d.Name()).Int("pid", cmd.Process.Pid).Msg("Xwayland started")
78+ // Detach the Wayland descriptor from os.File ownership before handing it
79+ // to the display (including its finalizers).
80+ if d.opts.Attach != nil {
81+ // Dup transfers an independent descriptor; closing the File wrapper below
82+ // cannot race with the display's ownership of the transferred descriptor.
83+ var wlFD int
84+ wlFD, err = unix.FcntlInt(uintptr(files[wl].Fd()), unix.F_DUPFD_CLOEXEC, 0)
85+ if err == nil {
86+ d.opts.Attach(wlFD)
87+ }
88+ }
89+ files[wl].Close()
90+ done := make(chan error, 1)
91+ procGone := make(chan struct{})
92+ go func() { done <- cmd.Wait(); close(procGone) }()
93+ if err != nil {
94+ stopGroup(cmd, done)
95+ return err
96+ }
97+ exited, readyErr := waitReady(ctx, pipe[0], done)
98+ if readyErr != nil {
99+ err := readyErr
100+ if errors.Is(err, errReadinessTimeout) {
101+ d.opts.Log.Warn().Str("display", d.Name()).Msg("Xwayland readiness timed out")
102+ }
103+ // Join Wait unless readiness already observed process exit.
104+ if !exited {
105+ stopGroup(cmd, done)
106+ }
107+ return err
108+ }
109+ rd.Close()
110+ d.opts.Log.Info().Str("display", d.Name()).Msg("Xwayland ready")
111+ if d.opts.AttachWM != nil {
112+ // The WM socket is bootstrapped only now: Xwayland serves it after its
113+ // Wayland round trips, which the display goroutine must be free to answer.
114+ wmFD, err := unix.FcntlInt(uintptr(files[wm].Fd()), unix.F_DUPFD_CLOEXEC, 0)
115+ if err != nil {
116+ stopGroup(cmd, done)
117+ return err
118+ }
119+ // The context ends with the process or the caller's context, so a
120+ // bootstrap against a dead or silent Xwayland aborts at once.
121+ wmCtx, cancel := context.WithCancel(ctx)
122+ watched := make(chan struct{})
123+ go func() {
124+ defer close(watched)
125+ select {
126+ case <-procGone:
127+ cancel()
128+ case <-wmCtx.Done():
129+ }
130+ }()
131+ stop := d.opts.AttachWM(wmCtx, wmFD)
132+ cancel()
133+ <-watched
134+ if stop != nil {
135+ // Runs before start returns so the restart loop never overlaps two XWMs.
136+ defer stop()
137+ }
138+ }
139+ select {
140+ case err := <-done:
141+ return err
142+ case <-ctx.Done():
143+ stopGroup(cmd, done)
144+ return ctx.Err()
145+ }
146+}
147+
148+var errReadinessTimeout = errors.New("xwayland readiness timed out")
149+
150+// waitReady uses bounded poll waits rather than a pipe reader goroutine.
151+// The result reports whether cmd.Wait was already joined.
152+func waitReady(ctx context.Context, fd int, done <-chan error) (bool, error) {
153+ deadline := time.Now().Add(10 * time.Second)
154+ var buf [32]byte
155+ n := 0
156+ for {
157+ select {
158+ case err := <-done:
159+ if err == nil {
160+ err = errors.New("xwayland exited before readiness")
161+ }
162+ return true, err
163+ default:
164+ }
165+ if ctx.Err() != nil {
166+ return false, ctx.Err()
167+ }
168+ if time.Now().After(deadline) {
169+ return false, errReadinessTimeout
170+ }
171+ // A closed pipe also covers process exit, including exec's stderr delay.
172+ p := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
173+ _, err := unix.Poll(p, 50)
174+ if errors.Is(err, unix.EINTR) {
175+ continue
176+ }
177+ if err != nil {
178+ return false, err
179+ }
180+ if p[0].Revents == 0 {
181+ continue
182+ }
183+ if p[0].Revents&unix.POLLIN != 0 {
184+ count, err := unix.Read(fd, buf[n:])
185+ if errors.Is(err, unix.EINTR) {
186+ continue
187+ }
188+ if err != nil {
189+ return false, err
190+ }
191+ n += count
192+ if n > 0 && buf[n-1] == '\n' {
193+ if _, err := strconv.Atoi(strings.TrimSpace(string(buf[:n]))); err != nil {
194+ return false, fmt.Errorf("xwayland displayfd: %w", err)
195+ }
196+ return false, nil
197+ }
198+ if n == len(buf) {
199+ return false, errors.New("invalid Xwayland displayfd response")
200+ }
201+ }
202+ if p[0].Revents&(unix.POLLHUP|unix.POLLERR|unix.POLLNVAL) != 0 {
203+ return false, errors.New("xwayland exited before readiness")
204+ }
205+ }
206+}
207+
208+func stopGroup(cmd *exec.Cmd, done <-chan error) {
209+ _ = unix.Kill(-cmd.Process.Pid, unix.SIGTERM)
210+ select {
211+ case <-done:
212+ case <-time.After(2 * time.Second):
213+ _ = unix.Kill(-cmd.Process.Pid, unix.SIGKILL)
214+ <-done
215+ }
216+}
A internal/adapters/xwayland/starter_mock_test.go +110 -0
@@ -0,0 +1,110 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package xwayland
6+
7+import (
8+ "context"
9+
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// newMockstarter creates a new instance of mockstarter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func newMockstarter(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *mockstarter {
19+ if helper, ok := t.(interface{ Helper() }); ok {
20+ helper.Helper()
21+ }
22+
23+ mock := &mockstarter{}
24+ mock.Mock.Test(t)
25+
26+ t.Cleanup(func() {
27+ if helper, ok := t.(interface{ Helper() }); ok {
28+ helper.Helper()
29+ }
30+ mock.AssertExpectations(t)
31+ })
32+
33+ return mock
34+}
35+
36+// mockstarter is an autogenerated mock type for the starter type
37+type mockstarter struct {
38+ mock.Mock
39+}
40+
41+type mockstarter_Expecter struct {
42+ mock *mock.Mock
43+}
44+
45+func (_m *mockstarter) EXPECT() *mockstarter_Expecter {
46+ return &mockstarter_Expecter{mock: &_m.Mock}
47+}
48+
49+// start provides a mock function for the type mockstarter
50+func (_mock *mockstarter) start(ctx context.Context, d *Display, stderr *logWriter) error {
51+ ret := _mock.Called(ctx, d, stderr)
52+
53+ if len(ret) == 0 {
54+ panic("no return value specified for start")
55+ }
56+
57+ var r0 error
58+ if returnFunc, ok := ret.Get(0).(func(context.Context, *Display, *logWriter) error); ok {
59+ r0 = returnFunc(ctx, d, stderr)
60+ } else {
61+ r0 = ret.Error(0)
62+ }
63+ return r0
64+}
65+
66+// mockstarter_start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'start'
67+type mockstarter_start_Call struct {
68+ *mock.Call
69+}
70+
71+// start is a helper method to define mock.On call
72+// - ctx context.Context
73+// - d *Display
74+// - stderr *logWriter
75+func (_e *mockstarter_Expecter) start(ctx any, d any, stderr any) *mockstarter_start_Call {
76+ return &mockstarter_start_Call{Call: _e.mock.On("start", ctx, d, stderr)}
77+}
78+
79+func (_c *mockstarter_start_Call) Run(run func(ctx context.Context, d *Display, stderr *logWriter)) *mockstarter_start_Call {
80+ _c.Call.Run(func(args mock.Arguments) {
81+ var arg0 context.Context
82+ if args[0] != nil {
83+ arg0 = args[0].(context.Context)
84+ }
85+ var arg1 *Display
86+ if args[1] != nil {
87+ arg1 = args[1].(*Display)
88+ }
89+ var arg2 *logWriter
90+ if args[2] != nil {
91+ arg2 = args[2].(*logWriter)
92+ }
93+ run(
94+ arg0,
95+ arg1,
96+ arg2,
97+ )
98+ })
99+ return _c
100+}
101+
102+func (_c *mockstarter_start_Call) Return(err error) *mockstarter_start_Call {
103+ _c.Call.Return(err)
104+ return _c
105+}
106+
107+func (_c *mockstarter_start_Call) RunAndReturn(run func(ctx context.Context, d *Display, stderr *logWriter) error) *mockstarter_start_Call {
108+ _c.Call.Return(run)
109+ return _c
110+}
M internal/adapters/xwayland/xwayland.go +46 -18
@@ -1,6 +1,5 @@
1-// Package xwayland runs X11 clients through xwayland-satellite, started on
2-// demand: neferwl owns the X11 display sockets and starts the satellite
3-// when the first X11 client connects, and again after it exits.
1+// Package xwayland starts an X11 backend on demand. NeferWL owns the display
2+// sockets and starts the selected backend when the first client connects.
43 package xwayland
54
65 import (
@@ -17,15 +16,26 @@ import (
1716 "syscall"
1817 "time"
1918
19+ "github.com/bnema/neferwl/internal/ports"
2020 "github.com/bnema/zerowrap"
2121 "golang.org/x/sys/unix"
2222 )
2323
2424 // Options configures the X11 display.
2525 type Options struct {
26- // Binary is the xwayland-satellite executable.
26+ Backend ports.XwaylandBackend
27+ // Attach takes ownership of the Wayland descriptor right after the backend
28+ // starts.
29+ Attach func(wlFD int)
30+ // AttachWM takes ownership of the window manager descriptor once the
31+ // backend is ready. It may block to bootstrap the X11 connection; ctx ends
32+ // when the backend exits or the run is cancelled, and the bootstrap must
33+ // then give up. The returned stop (nil for none) is called before the run
34+ // ends, on backend exit or context cancellation.
35+ AttachWM func(ctx context.Context, wmFD int) (stop func())
36+ // Binary is the selected backend executable.
2737 Binary string
28- // Env is the satellite's environment; it must hold WAYLAND_DISPLAY.
38+ // Env is the backend environment; satellite requires WAYLAND_DISPLAY.
2939 Env []string
3040 // Dir holds the X11 sockets and TmpDir the lock files: /tmp/.X11-unix
3141 // and /tmp outside tests.
@@ -35,8 +45,9 @@ type Options struct {
3545 Log zerowrap.Logger
3646 }
3747
38-// Display is an X11 display served by xwayland-satellite.
48+// Display is an on-demand X11 display.
3949 type Display struct {
50+ starter starter
4051 opts Options
4152 n int
4253 sockets []*os.File // unix socket, then the abstract one
@@ -81,7 +92,10 @@ func Open(opts Options) (*Display, error) {
8192 _ = os.Remove(lock)
8293 return nil, fmt.Errorf("write X11 lock: %w", err)
8394 }
84- d := &Display{opts: opts, n: n, lock: lock, socket: filepath.Join(opts.Dir, fmt.Sprintf("X%d", n))}
95+ d := &Display{starter: satelliteStarter{}, opts: opts, n: n, lock: lock, socket: filepath.Join(opts.Dir, fmt.Sprintf("X%d", n))}
96+ if opts.Backend == ports.XwaylandBuiltin {
97+ d.starter = builtinStarter{}
98+ }
8599 if err := d.listen(); err != nil {
86100 last = err
87101 d.Close()
@@ -212,15 +226,15 @@ func (d *Display) Run(ctx context.Context) error {
212226 return err
213227 }
214228 start := time.Now()
215- stderr := &logWriter{log: d.opts.Log}
216- err := d.serve(ctx, stderr)
229+ stderr := &logWriter{log: d.opts.Log, backend: d.backendName()}
230+ err := d.starter.start(ctx, d, stderr)
217231 if ctx.Err() != nil {
218232 return nil
219233 }
220234 if err != nil {
221- d.opts.Log.Warn().Err(err).Strs("stderr", stderr.tail()).Str("display", d.Name()).Msg("xwayland-satellite exited")
235+ d.opts.Log.Warn().Err(err).Strs("stderr", stderr.tail()).Str("display", d.Name()).Msg(d.backendName() + " exited")
222236 } else {
223- d.opts.Log.Info().Str("display", d.Name()).Msg("xwayland-satellite exited")
237+ d.opts.Log.Info().Str("display", d.Name()).Msg(d.backendName() + " exited")
224238 }
225239 // Clients the satellite never accepted would wake us at once: a
226240 // satellite that cannot start must not respawn in a loop.
@@ -261,8 +275,14 @@ func (d *Display) waitClient(ctx context.Context, wake int) error {
261275 return ctx.Err()
262276 }
263277
264-// serve runs the satellite on the sockets until it exits or ctx ends.
265-func (d *Display) serve(ctx context.Context, stderr *logWriter) error {
278+// starter runs a backend on the reserved sockets until it exits or ctx ends.
279+type starter interface {
280+ start(ctx context.Context, d *Display, stderr *logWriter) error
281+}
282+
283+type satelliteStarter struct{}
284+
285+func (satelliteStarter) start(ctx context.Context, d *Display, stderr *logWriter) error {
266286 args := []string{d.Name()}
267287 for i := range d.sockets {
268288 args = append(args, "-listenfd", strconv.Itoa(3+i))
@@ -318,10 +338,11 @@ func (d *Display) dropPending() {
318338 // logWriter logs the satellite's stderr line by line and keeps the last
319339 // lines for the exit log; exec copies it from a pipe on its own goroutine.
320340 type logWriter struct {
321- mu sync.Mutex
322- log zerowrap.Logger
323- line []byte
324- last []string
341+ backend string
342+ mu sync.Mutex
343+ log zerowrap.Logger
344+ line []byte
345+ last []string
325346 }
326347
327348 // tail is the last lines written.
@@ -342,7 +363,7 @@ func (w *logWriter) Write(p []byte) (int, error) {
342363 }
343364 if i > 0 {
344365 line := string(w.line[:i])
345- w.log.Debug().Str("line", line).Msg("xwayland-satellite")
366+ w.log.Debug().Str("line", line).Msg(w.backend)
346367 if w.last = append(w.last, line); len(w.last) > 5 {
347368 w.last = w.last[1:]
348369 }
@@ -354,3 +375,10 @@ func (w *logWriter) Write(p []byte) (int, error) {
354375 }
355376 return len(p), nil
356377 }
378+
379+func (d *Display) backendName() string {
380+ if d.opts.Backend == ports.XwaylandBuiltin {
381+ return "Xwayland"
382+ }
383+ return "xwayland-satellite"
384+}
M internal/adapters/xwayland/xwayland_test.go +207 -0
@@ -11,6 +11,8 @@ import (
1111 "time"
1212
1313 "github.com/bnema/neferwl/internal/adapters/logging"
14+ "github.com/bnema/neferwl/internal/ports"
15+ "github.com/stretchr/testify/mock"
1416 "golang.org/x/sys/unix"
1517 )
1618
@@ -270,3 +272,208 @@ func TestRunKillsSatelliteGroup(t *testing.T) {
270272 }
271273 }
272274 }
275+
276+func TestStarterSelection(t *testing.T) {
277+ for _, backend := range []ports.XwaylandBackend{ports.XwaylandSatellite, ports.XwaylandBuiltin} {
278+ opts := testOptions(t, "true")
279+ opts.Backend = backend
280+ d, err := Open(opts)
281+ if err != nil {
282+ t.Fatal(err)
283+ }
284+ defer d.Close()
285+ switch d.starter.(type) {
286+ case satelliteStarter:
287+ if backend != ports.XwaylandSatellite {
288+ t.Fatal("satellite selected")
289+ }
290+ case builtinStarter:
291+ if backend != ports.XwaylandBuiltin {
292+ t.Fatal("builtin selected")
293+ }
294+ default:
295+ t.Fatalf("unexpected starter %T", d.starter)
296+ }
297+ }
298+}
299+
300+func TestRunCallsStarter(t *testing.T) {
301+ d, err := Open(testOptions(t, "unused"))
302+ if err != nil {
303+ t.Fatal(err)
304+ }
305+ defer d.Close()
306+ ctx, cancel := context.WithCancel(t.Context())
307+ defer cancel()
308+ m := newMockstarter(t)
309+ d.starter = m
310+ m.EXPECT().start(ctx, d, mock.Anything).RunAndReturn(func(_ context.Context, got *Display, stderr *logWriter) error {
311+ if got != d || stderr == nil {
312+ t.Error("starter arguments")
313+ }
314+ cancel()
315+ return ctx.Err()
316+ }).Once()
317+ done := make(chan error, 1)
318+ go func() { done <- d.Run(ctx) }()
319+ unix.Close(dial(t, d.socket))
320+ select {
321+ case err := <-done:
322+ if err != nil {
323+ t.Fatal(err)
324+ }
325+ case <-time.After(3 * time.Second):
326+ t.Fatal("Run did not stop")
327+ }
328+}
329+
330+func TestWaitReady(t *testing.T) {
331+ for _, tt := range []struct {
332+ name, data string
333+ wantErr bool
334+ }{
335+ {"ready", "12\n", false}, {"invalid", "bad\n", true}, {"closed", "", true},
336+ } {
337+ t.Run(tt.name, func(t *testing.T) {
338+ var p [2]int
339+ if err := unix.Pipe2(p[:], unix.O_CLOEXEC); err != nil {
340+ t.Fatal(err)
341+ }
342+ defer unix.Close(p[0])
343+ if tt.data != "" {
344+ if _, err := unix.Write(p[1], []byte(tt.data)); err != nil {
345+ t.Fatal(err)
346+ }
347+ }
348+ unix.Close(p[1])
349+ joined, err := waitReady(t.Context(), p[0], make(chan error))
350+ if joined || (err != nil) != tt.wantErr {
351+ t.Fatalf("joined=%v err=%v", joined, err)
352+ }
353+ })
354+ }
355+ t.Run("exit", func(t *testing.T) {
356+ done := make(chan error, 1)
357+ done <- errors.New("exit")
358+ joined, err := waitReady(t.Context(), -1, done)
359+ if !joined || err == nil {
360+ t.Fatal(joined, err)
361+ }
362+ })
363+ t.Run("cancel", func(t *testing.T) {
364+ ctx, cancel := context.WithCancel(t.Context())
365+ cancel()
366+ joined, err := waitReady(ctx, -1, make(chan error))
367+ if joined || !errors.Is(err, context.Canceled) {
368+ t.Fatal(joined, err)
369+ }
370+ })
371+}
372+
373+// readyScript stands in for Xwayland: after a short delay it creates a marker
374+// file and reports readiness on the -displayfd descriptor when ready is true,
375+// then runs tail.
376+func readyScript(t *testing.T, dir string, ready bool, tail string) (bin, marker string) {
377+ t.Helper()
378+ bin, marker = filepath.Join(dir, "xwayland"), filepath.Join(dir, "ready")
379+ script := "#!/bin/sh\nwhile [ $# -gt 0 ]; do [ \"$1\" = -displayfd ] && fd=$2; shift; done\n" +
380+ "sleep 0.3\n"
381+ if ready {
382+ script += ": > " + marker + "\neval \"echo 1 >&$fd\"\n"
383+ }
384+ script += tail + "\n"
385+ if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
386+ t.Fatal(err)
387+ }
388+ return bin, marker
389+}
390+
391+func runBuiltin(t *testing.T, ctx context.Context, ready bool, tail string, attachWM func(ctx context.Context, wmFD int)) (events []string) {
392+ t.Helper()
393+ opts := testOptions(t, "")
394+ opts.Backend = ports.XwaylandBuiltin
395+ var marker string
396+ opts.Binary, marker = readyScript(t, opts.TmpDir, ready, tail)
397+ exists := func() bool { _, err := os.Stat(marker); return err == nil }
398+ opts.Attach = func(wlFD int) {
399+ if exists() {
400+ t.Error("Attach ran after readiness")
401+ }
402+ events = append(events, "attach")
403+ unix.Close(wlFD)
404+ }
405+ opts.AttachWM = func(ctx context.Context, wmFD int) func() {
406+ if !exists() {
407+ t.Error("AttachWM ran before readiness")
408+ }
409+ events = append(events, "attachWM")
410+ attachWM(ctx, wmFD)
411+ return func() { events = append(events, "stop") }
412+ }
413+ d, err := Open(opts)
414+ if err != nil {
415+ t.Fatal(err)
416+ }
417+ defer d.Close()
418+ _ = d.starter.start(ctx, d, &logWriter{log: opts.Log, backend: d.backendName()})
419+ return append(events, "returned")
420+}
421+
422+// AttachWM runs only after -displayfd readiness; stop runs before start
423+// returns, whether the process exits or the context ends.
424+func TestBuiltinAttachWMAfterReadiness(t *testing.T) {
425+ for name, tc := range map[string]struct {
426+ tail string
427+ cancel bool
428+ }{"process exit": {tail: "exit 0"}, "context end": {tail: "exec sleep 30", cancel: true}} {
429+ t.Run(name, func(t *testing.T) {
430+ ctx, cancel := context.WithCancel(t.Context())
431+ defer cancel()
432+ events := runBuiltin(t, ctx, true, tc.tail, func(_ context.Context, wmFD int) {
433+ if _, err := unix.FcntlInt(uintptr(wmFD), unix.F_GETFD, 0); err != nil {
434+ t.Errorf("wmFD not open: %v", err)
435+ }
436+ unix.Close(wmFD)
437+ if tc.cancel {
438+ go func() { time.Sleep(200 * time.Millisecond); cancel() }()
439+ }
440+ })
441+ if got := strings.Join(events, ","); got != "attach,attachWM,stop,returned" {
442+ t.Fatalf("events %s", got)
443+ }
444+ })
445+ }
446+}
447+
448+// A process that exits before reporting readiness never reaches AttachWM.
449+func TestBuiltinNoAttachWMWithoutReadiness(t *testing.T) {
450+ events := runBuiltin(t, t.Context(), false, "exit 1", func(context.Context, int) {})
451+ if got := strings.Join(events, ","); got != "attach,returned" {
452+ t.Fatalf("events %s", got)
453+ }
454+}
455+
456+// The AttachWM context ends when the process dies, so a bootstrap against a
457+// dead Xwayland gives up at once instead of waiting out its handshake.
458+func TestBuiltinAttachWMContextEndsWithProcess(t *testing.T) {
459+ ended := make(chan error, 1)
460+ start := time.Now()
461+ events := runBuiltin(t, t.Context(), true, "sleep 0.3", func(ctx context.Context, wmFD int) {
462+ defer unix.Close(wmFD)
463+ select {
464+ case <-ctx.Done():
465+ ended <- ctx.Err()
466+ case <-time.After(5 * time.Second):
467+ ended <- nil
468+ }
469+ })
470+ if err := <-ended; err == nil {
471+ t.Fatal("AttachWM context did not end with the process")
472+ }
473+ if time.Since(start) > 3*time.Second {
474+ t.Fatalf("took %v", time.Since(start))
475+ }
476+ if got := strings.Join(events, ","); got != "attach,attachWM,stop,returned" {
477+ t.Fatalf("events %s", got)
478+ }
479+}
M internal/app/app.go +13 -6
@@ -5,6 +5,7 @@ import (
55 "context"
66 "fmt"
77 "io"
8+ "os/exec"
89 "time"
910
1011 "github.com/bnema/neferwl/internal/adapters/logging"
@@ -188,18 +189,24 @@ func logScenes(log zerowrap.Logger, set []ports.Scene) {
188189 }
189190 }
190191
191-// openXwayland reserves an X11 display for xwayland-satellite, or returns
192+// openXwayland reserves an X11 display for the selected backend, or returns
192193 // nil when X11 is off or unavailable: the session runs without it.
193-func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.Logger) *xwayland.Display {
194- bin := opts.Config.Xwayland
195- if bin == "" || opts.NoXwayland {
194+func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.Logger, attach func(int), attachWM func(context.Context, int) func()) *xwayland.Display {
195+ cfg := opts.Config.Xwayland
196+ bin := cfg.Binary
197+ if cfg.Backend == ports.XwaylandOff || opts.NoXwayland {
196198 return nil
197199 }
198- if !xwayland.Supported(ctx, bin) {
200+ if cfg.Backend == ports.XwaylandBuiltin {
201+ if _, err := exec.LookPath(bin); err != nil {
202+ log.Warn().Err(err).Str("binary", bin).Msg("Xwayland not found: X11 apps disabled")
203+ return nil
204+ }
205+ } else if !xwayland.Supported(ctx, bin) {
199206 log.Warn().Str("binary", bin).Msg("xwayland-satellite 0.7 or later not found: X11 apps disabled")
200207 return nil
201208 }
202- d, err := xwayland.Open(xwayland.Options{Binary: bin, Env: env, Dir: "/tmp/.X11-unix", TmpDir: "/tmp", Abstract: true, Log: log})
209+ d, err := xwayland.Open(xwayland.Options{Backend: cfg.Backend, Attach: attach, AttachWM: attachWM, Binary: bin, Env: env, Dir: "/tmp/.X11-unix", TmpDir: "/tmp", Abstract: true, Log: log})
203210 if err != nil {
204211 log.Warn().Err(err).Msg("X11 display disabled")
205212 return nil
M internal/app/session.go +1 -1
@@ -152,7 +152,7 @@ func assembleSession(ctx context.Context, opts Options) (s *session, err error)
152152 s.statePath = statePath
153153 s.childEnv = append(s.childEnv, statefile.Env+"="+statePath)
154154 }
155- if s.xdisplay = openXwayland(ctx, opts, s.childEnv, logging.For(ctx, "xwayland")); s.xdisplay != nil {
155+ if s.xdisplay = openXwayland(ctx, opts, s.childEnv, logging.For(ctx, "xwayland"), s.server.AttachXwayland, s.server.AttachXWM); s.xdisplay != nil {
156156 s.childEnv = append(s.childEnv, "DISPLAY="+s.xdisplay.Name())
157157 }
158158 s.child = launcher.New(s.childEnv, s.security, logging.For(ctx, "launcher"))
A internal/app/testdata/x11probe/main.go +232 -0
@@ -0,0 +1,232 @@
1+// Command x11probe is a minimal X11 client for the Xwayland end-to-end test.
2+//
3+// It opens one window filled with solid green, prints "mapped WxH" after the
4+// first ConfigureNotify and "focus" on each FocusIn, and exits 0 when the
5+// window manager sends WM_DELETE_WINDOW.
6+//
7+// With -override it also maps a magenta override-redirect window after the
8+// main one is mapped (prints "override mapped") and unmaps it after
9+// -override-for (prints "override unmapped"). With -override-dismiss-on-focusout
10+// it unmaps it instead when the main window loses focus (prints "dismissed").
11+package main
12+
13+import (
14+ "encoding/binary"
15+ "flag"
16+ "fmt"
17+ "os"
18+ "strconv"
19+ "strings"
20+ "time"
21+
22+ "golang.org/x/sys/unix"
23+
24+ "bnema.dev/xturbo"
25+ "bnema.dev/xturbo/shape"
26+ "bnema.dev/xturbo/xproto"
27+)
28+
29+const (
30+ green = 0x00ff00
31+ magenta = 0xff00ff
32+)
33+
34+func main() {
35+ if err := run(); err != nil {
36+ fmt.Fprintln(os.Stderr, "x11probe:", err)
37+ os.Exit(1)
38+ }
39+}
40+
41+func run() error {
42+ class := flag.String("class", "X11Probe", "WM_CLASS class (the instance is x11probe)")
43+ title := flag.String("title", "probe", "_NET_WM_NAME")
44+ size := flag.String("size", "300x200", "window size WxH")
45+ sleep := flag.Duration("sleep", 0, "wait before mapping the window")
46+ override := flag.Bool("override", false, "map a magenta override-redirect window after the main one")
47+ overrideAt := flag.String("override-at", "100,50", "override window position X,Y in root coordinates")
48+ overrideSize := flag.String("override-size", "120x80", "override window size WxH")
49+ overrideFor := flag.Duration("override-for", 0, "unmap the override window after this delay (0: never)")
50+ overrideType := flag.String("override-type", "menu", "override window type: menu or tooltip")
51+ overrideShape := flag.Bool("override-shape", false, "give the override window an empty input shape")
52+ dismiss := flag.Bool("override-dismiss-on-focusout", false, "unmap the override window when the main window loses focus")
53+ flag.Parse()
54+ w, h, ok := parseSize(*size)
55+ if !ok {
56+ return fmt.Errorf("bad -size %q", *size)
57+ }
58+
59+ c, err := xturbo.Dial("") // $DISPLAY
60+ if err != nil {
61+ return err
62+ }
63+ defer c.Close()
64+
65+ var atoms [7]uint32
66+ if err := c.InternAtoms([]string{"WM_PROTOCOLS", "WM_DELETE_WINDOW", "_NET_WM_NAME", "UTF8_STRING",
67+ "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", "_NET_WM_WINDOW_TYPE_TOOLTIP"}, false, atoms[:]); err != nil {
68+ return err
69+ }
70+ protocols, deleteWin, netWMName, utf8 := xproto.Atom(atoms[0]), xproto.Atom(atoms[1]), xproto.Atom(atoms[2]), xproto.Atom(atoms[3])
71+ typeAtom, typeMenu, typeTooltip := xproto.Atom(atoms[4]), atoms[5], atoms[6]
72+
73+ b := c.Buf()
74+ xwin, err := c.NewXID()
75+ if err != nil {
76+ return err
77+ }
78+ xgc, err := c.NewXID()
79+ if err != nil {
80+ return err
81+ }
82+ win, gc := xproto.Window(xwin), xproto.Gcontext(xgc)
83+
84+ var v xproto.CreateWindowValues
85+ v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify | xproto.EventMaskFocusChange)
86+ xproto.CreateWindow(b, 0, win, xproto.Window(c.Root()), 0, 0, uint16(w), uint16(h), 0, xproto.WindowClassInputOutput, 0, &v)
87+
88+ wmClass := []byte("x11probe\x00" + *class + "\x00")
89+ xproto.ChangeProperty(b, xproto.PropModeReplace, win, xproto.AtomWmClass, xproto.AtomString, 8, uint32(len(wmClass)), wmClass)
90+ name := []byte(*title)
91+ xproto.ChangeProperty(b, xproto.PropModeReplace, win, netWMName, utf8, 8, uint32(len(name)), name)
92+ var del [4]byte
93+ binary.LittleEndian.PutUint32(del[:], uint32(deleteWin))
94+ xproto.ChangeProperty(b, xproto.PropModeReplace, win, protocols, xproto.AtomAtom, 32, 1, del[:])
95+
96+ var gv xproto.CreateGCValues
97+ gv.SetForeground(green)
98+ xproto.CreateGC(b, gc, xproto.Drawable(win), &gv)
99+ if err := c.Flush(); err != nil {
100+ return err
101+ }
102+ time.Sleep(*sleep)
103+ xproto.MapWindow(b, win)
104+
105+ // The override-redirect window is created once the main window is mapped.
106+ var ovWin xproto.Window
107+ var ovAt, ovSz [2]int
108+ if *override {
109+ var ok1, ok2 bool
110+ x, y, found := strings.Cut(*overrideAt, ",")
111+ ovAt[0], ovAt[1] = atoi(x), atoi(y)
112+ ok1 = found
113+ ovSz[0], ovSz[1], ok2 = parseSize(*overrideSize)
114+ if !ok1 || !ok2 || (*overrideType != "menu" && *overrideType != "tooltip") {
115+ return fmt.Errorf("bad -override-at/-override-size/-override-type")
116+ }
117+ }
118+ var ovDeadline time.Time // zero: no timed unmap pending
119+ unmapOverride := func(msg string) {
120+ if ovWin == 0 {
121+ return
122+ }
123+ xproto.UnmapWindow(b, ovWin)
124+ ovWin = 0
125+ ovDeadline = time.Time{}
126+ fmt.Println(msg)
127+ }
128+ mapOverride := func() error {
129+ id, err := c.NewXID()
130+ if err != nil {
131+ return err
132+ }
133+ ovWin = xproto.Window(id)
134+ var ov xproto.CreateWindowValues
135+ ov.SetBackgroundPixel(magenta).SetOverrideRedirect(true)
136+ xproto.CreateWindow(b, 0, ovWin, xproto.Window(c.Root()), int16(ovAt[0]), int16(ovAt[1]), uint16(ovSz[0]), uint16(ovSz[1]), 0, xproto.WindowClassInputOutput, 0, &ov)
137+ typ := typeMenu
138+ if *overrideType == "tooltip" {
139+ typ = typeTooltip
140+ }
141+ var tb [4]byte
142+ binary.LittleEndian.PutUint32(tb[:], typ)
143+ xproto.ChangeProperty(b, xproto.PropModeReplace, ovWin, typeAtom, xproto.AtomAtom, 32, 1, tb[:])
144+ if *overrideShape {
145+ shape.Rectangles(b, shape.SoSet, shape.SkInput, 0, ovWin, 0, 0, nil)
146+ }
147+ xproto.MapWindow(b, ovWin)
148+ fmt.Println("override mapped")
149+ if *overrideFor > 0 {
150+ ovDeadline = time.Now().Add(*overrideFor)
151+ }
152+ return nil
153+ }
154+ var ovErr error
155+
156+ mapped, done := false, false
157+ cw, ch := uint16(w), uint16(h) // current size, tracked from ConfigureNotify
158+ hd := &xturbo.Handler{
159+ OnEvent: func(ev *xturbo.Event) {
160+ switch ev.Kind {
161+ case xproto.KindExpose:
162+ xproto.PolyFillRectangle(b, xproto.Drawable(win), gc, []xproto.Rectangle{{Width: cw, Height: ch}})
163+ case xproto.KindConfigureNotify:
164+ e := xproto.ConfigureNotifyOf(ev)
165+ if e.Window() != win {
166+ break
167+ }
168+ cw, ch = e.Width(), e.Height()
169+ if !mapped {
170+ mapped = true
171+ fmt.Printf("mapped %dx%d\n", cw, ch)
172+ if *override {
173+ ovErr = mapOverride()
174+ }
175+ }
176+ case xproto.KindFocusIn:
177+ fmt.Println("focus")
178+ case xproto.KindFocusOut:
179+ if *dismiss && xproto.FocusOutOf(ev).Event() == win {
180+ unmapOverride("dismissed")
181+ }
182+ case xproto.KindClientMessage:
183+ e := xproto.ClientMessageOf(ev)
184+ if e.Type() == protocols && e.Format() == 32 && e.Data().Data32()[0] == uint32(deleteWin) {
185+ done = true
186+ }
187+ }
188+ },
189+ OnError: func(e *xturbo.XError) { fmt.Fprintln(os.Stderr, "x11probe:", e) },
190+ }
191+ for !done {
192+ events := int16(unix.POLLIN)
193+ if err := c.Flush(); err == xturbo.ErrWouldBlock {
194+ events |= unix.POLLOUT
195+ } else if err != nil {
196+ return err
197+ }
198+ timeout := -1
199+ if !ovDeadline.IsZero() {
200+ timeout = max(int(time.Until(ovDeadline).Milliseconds()), 0)
201+ }
202+ fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}}
203+ if _, err := unix.Poll(fds, timeout); err != nil && err != unix.EINTR {
204+ return err
205+ }
206+ if !ovDeadline.IsZero() && !time.Now().Before(ovDeadline) {
207+ unmapOverride("override unmapped")
208+ }
209+ if err := c.Dispatch(hd); err != nil {
210+ return err
211+ }
212+ if ovErr != nil {
213+ return ovErr
214+ }
215+ }
216+ return nil
217+}
218+
219+func parseSize(s string) (w, h int, ok bool) {
220+ ws, hs, found := strings.Cut(s, "x")
221+ if !found {
222+ return 0, 0, false
223+ }
224+ w, err1 := strconv.Atoi(ws)
225+ h, err2 := strconv.Atoi(hs)
226+ return w, h, err1 == nil && err2 == nil && w > 0 && h > 0 && w < 0xffff && h < 0xffff
227+}
228+
229+func atoi(s string) int {
230+ n, _ := strconv.Atoi(s)
231+ return n
232+}
A internal/app/xwayland_test.go +394 -0
@@ -0,0 +1,394 @@
1+//go:build xwayland
2+
3+// End-to-end test of the built-in XWM: a real Xwayland and a real X11 client
4+// (testdata/x11probe) run against a headless NeferWL. It needs Xwayland on
5+// PATH and a GPU, so it is excluded from `go test ./...` and CI. Run it with
6+// `make e2e-xwayland`.
7+package app
8+
9+import (
10+ "context"
11+ "image"
12+ "image/png"
13+ "io"
14+ "os"
15+ "os/exec"
16+ "path/filepath"
17+ "regexp"
18+ "strings"
19+ "sync"
20+ "testing"
21+ "time"
22+
23+ "github.com/bnema/zerowrap"
24+
25+ "github.com/bnema/neferwl/internal/adapters/config"
26+ "github.com/bnema/neferwl/internal/adapters/statefile"
27+ "github.com/bnema/neferwl/internal/ports"
28+)
29+
30+const (
31+ xwaylandTimeout = 10 * time.Second
32+ xwaylandW, xwaylandH = 640, 360
33+)
34+
35+var displayRe = regexp.MustCompile(`"DISPLAY":"(:\d+)"`)
36+
37+// probe is a running x11probe.
38+type probe struct {
39+ cmd *exec.Cmd
40+ out *lockedBuffer
41+ waited chan error
42+}
43+
44+// xwaylandEnv is a running headless compositor with the built-in XWM.
45+type xwaylandEnv struct {
46+ bin, display, statePath, shots string
47+ script *io.PipeWriter
48+}
49+
50+// startXwayland runs the compositor on one headless output and waits for its
51+// X11 display. It skips the test without Xwayland and stops everything when
52+// the test ends.
53+func startXwayland(t *testing.T) *xwaylandEnv {
54+ t.Helper()
55+ if _, err := exec.LookPath("Xwayland"); err != nil {
56+ t.Skip("Xwayland not on PATH")
57+ }
58+ // The compositor must not read or write the live session's state.
59+ for _, k := range []string{"NEFERWL_STATE", "NEFERWL_SLOT"} {
60+ if v, ok := os.LookupEnv(k); ok {
61+ os.Unsetenv(k)
62+ t.Cleanup(func() { os.Setenv(k, v) })
63+ }
64+ }
65+ runtime := t.TempDir()
66+ t.Setenv("XDG_RUNTIME_DIR", runtime)
67+ t.Setenv("XDG_STATE_HOME", t.TempDir())
68+ shots := t.TempDir()
69+ bin := buildProbe(t)
70+
71+ script, scriptW := io.Pipe()
72+ logs := &lockedBuffer{}
73+ ctx, cancel := context.WithCancel(zerowrap.WithCtx(context.Background(), zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: logs})))
74+ cfg := config.Defaults()
75+ cfg.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandBuiltin, Binary: "Xwayland"}
76+ done := make(chan error, 1)
77+ go func() {
78+ done <- Run(ctx, Options{
79+ Backend: "headless", NoTerminal: true, Config: cfg, Script: script,
80+ ScreenshotDir: shots, Sizes: [][2]int{{xwaylandW, xwaylandH}},
81+ })
82+ }()
83+ t.Cleanup(func() {
84+ if t.Failed() {
85+ t.Logf("compositor log:\n%s", logs.String())
86+ }
87+ cancel()
88+ _ = scriptW.Close()
89+ select {
90+ case err := <-done:
91+ if err != nil {
92+ t.Errorf("Run: %v", err)
93+ }
94+ case <-time.After(5 * time.Second):
95+ t.Error("Run did not stop")
96+ }
97+ })
98+
99+ var display string
100+ until(t, "X11 display in the log", func() bool {
101+ if m := displayRe.FindStringSubmatch(logs.String()); m != nil {
102+ display = m[1]
103+ return true
104+ }
105+ return false
106+ })
107+ statePath, err := statefile.Path(runtime, "wayland-1")
108+ if err != nil {
109+ t.Fatal(err)
110+ }
111+ return &xwaylandEnv{bin: bin, display: display, statePath: statePath, shots: shots, script: scriptW}
112+}
113+
114+// TestXwaylandProbe: an X11 client is managed by the built-in XWM. It shows
115+// up in the state with its class and pid, is drawn, tiles next to a second
116+// one, receives focus and is closed by the close-window bind.
117+func TestXwaylandProbe(t *testing.T) {
118+ e := startXwayland(t)
119+ bin, display, statePath, shots, scriptW := e.bin, e.display, e.statePath, e.shots, e.script
120+ first := startProbe(t, bin, display, "-title", "one")
121+ until(t, "probe one mapped", func() bool { return strings.Contains(first.out.String(), "mapped ") })
122+
123+ var st statefile.State
124+ until(t, "one visible X11Probe window", func() bool {
125+ st = readState(statePath)
126+ return len(st.Windows) == 1 && st.Windows[0].AppID == "X11Probe" && st.Windows[0].Visible
127+ })
128+ if w := st.Windows[0]; w.PID != first.cmd.Process.Pid {
129+ t.Errorf("window pid %d, want %d", w.PID, first.cmd.Process.Pid)
130+ }
131+
132+ // The client draws green and the compositor shows it.
133+ id := st.Windows[0].ID
134+ until(t, "green window in latest.png", func() bool {
135+ r, ok := tileRect(readState(statePath), id)
136+ return ok && greenShare(filepath.Join(shots, "latest.png"), r) >= 0.5
137+ })
138+
139+ // Focus round trip: the compositor focused the window, the client saw it.
140+ until(t, "probe one focus", func() bool { return strings.Contains(first.out.String(), "focus") })
141+
142+ // A second probe tiles beside the first.
143+ second := startProbe(t, bin, display, "-title", "two")
144+ until(t, "two tiled windows", func() bool {
145+ st = readState(statePath)
146+ if len(st.Windows) != 2 {
147+ return false
148+ }
149+ for _, w := range st.Windows {
150+ if w.AppID != "X11Probe" || !w.Visible || w.Floating {
151+ return false
152+ }
153+ }
154+ return true
155+ })
156+ until(t, "probe two focus", func() bool { return strings.Contains(second.out.String(), "focus") })
157+
158+ // The close-window bind asks the focused (second) probe to close.
159+ if _, err := io.WriteString(scriptW, "key Super+q\n"); err != nil {
160+ t.Fatal(err)
161+ }
162+ select {
163+ case err := <-second.waited:
164+ if err != nil {
165+ t.Errorf("probe two exited with %v, want 0", err)
166+ }
167+ case <-time.After(2 * time.Second):
168+ t.Fatal("probe two did not exit within 2s of Super+q")
169+ }
170+ until(t, "one window left", func() bool {
171+ st = readState(statePath)
172+ return len(st.Windows) == 1 && st.Windows[0].PID == first.cmd.Process.Pid
173+ })
174+ select {
175+ case err := <-first.waited:
176+ t.Fatalf("probe one exited (%v) though only the focused probe was closed", err)
177+ default:
178+ }
179+}
180+
181+// buildProbe builds testdata/x11probe.
182+func buildProbe(t *testing.T) string {
183+ t.Helper()
184+ bin := filepath.Join(t.TempDir(), "x11probe")
185+ build := exec.Command("go", "build", "-o", bin, "./testdata/x11probe")
186+ build.Env = append(os.Environ(), "CGO_ENABLED=0")
187+ if out, err := build.CombinedOutput(); err != nil {
188+ t.Fatalf("build x11probe: %v: %s", err, out)
189+ }
190+ return bin
191+}
192+
193+// startProbe starts x11probe on display, with X11 as its only display
194+// server, and stops it when the test ends.
195+func startProbe(t *testing.T, bin, display string, args ...string) *probe {
196+ t.Helper()
197+ p := &probe{out: &lockedBuffer{}, waited: make(chan error, 1)}
198+ p.cmd = exec.Command(bin, args...)
199+ p.cmd.Env = append(envWithout("DISPLAY", "WAYLAND_DISPLAY", "NEFERWL_STATE", "NEFERWL_SLOT"), "DISPLAY="+display)
200+ p.cmd.Stdout = p.out
201+ stderr := &lockedBuffer{}
202+ p.cmd.Stderr = stderr
203+ if err := p.cmd.Start(); err != nil {
204+ t.Fatal(err)
205+ }
206+ var exited sync.Once
207+ go func() { p.waited <- p.cmd.Wait() }()
208+ t.Cleanup(func() {
209+ exited.Do(func() {
210+ _ = p.cmd.Process.Kill()
211+ select {
212+ case <-p.waited:
213+ case <-time.After(2 * time.Second):
214+ }
215+ })
216+ if s := stderr.String(); s != "" {
217+ t.Logf("x11probe stderr: %s", s)
218+ }
219+ })
220+ return p
221+}
222+
223+func envWithout(keys ...string) []string {
224+ var env []string
225+next:
226+ for _, kv := range os.Environ() {
227+ for _, k := range keys {
228+ if strings.HasPrefix(kv, k+"=") {
229+ continue next
230+ }
231+ }
232+ env = append(env, kv)
233+ }
234+ return env
235+}
236+
237+// until polls cond every 20 ms and fails the test after xwaylandTimeout.
238+func until(t *testing.T, what string, cond func() bool) {
239+ t.Helper()
240+ for deadline := time.Now().Add(xwaylandTimeout); !cond(); time.Sleep(20 * time.Millisecond) {
241+ if time.Now().After(deadline) {
242+ t.Fatalf("timeout waiting for %s", what)
243+ }
244+ }
245+}
246+
247+// readState returns the current state file, or an empty state while it does
248+// not exist or is being replaced.
249+func readState(path string) statefile.State {
250+ st, err := statefile.Read(path)
251+ if err != nil {
252+ return statefile.State{}
253+ }
254+ return st
255+}
256+
257+// tileRect finds the rectangle of window id in the tiled columns.
258+func tileRect(st statefile.State, id uint64) (image.Rectangle, bool) {
259+ for _, ws := range st.Workspaces {
260+ for _, c := range ws.Columns {
261+ for _, w := range c.Windows {
262+ if w.ID == id {
263+ return image.Rect(w.X, w.Y, w.X+w.Width, w.Y+w.Height), true
264+ }
265+ }
266+ }
267+ }
268+ return image.Rectangle{}, false
269+}
270+
271+// greenShare is the fraction of pixels of r in the PNG at path that are
272+// clearly green; 0 when the image cannot be read yet.
273+func greenShare(path string, r image.Rectangle) float64 {
274+ f, err := os.Open(path)
275+ if err != nil {
276+ return 0
277+ }
278+ defer f.Close()
279+ img, err := png.Decode(f)
280+ if err != nil {
281+ return 0
282+ }
283+ r = r.Intersect(img.Bounds())
284+ if r.Empty() {
285+ return 0
286+ }
287+ n := 0
288+ for y := r.Min.Y; y < r.Max.Y; y++ {
289+ for x := r.Min.X; x < r.Max.X; x++ {
290+ cr, cg, cb, _ := img.At(x, y).RGBA()
291+ if cg>>8 > 200 && cr>>8 < 60 && cb>>8 < 60 {
292+ n++
293+ }
294+ }
295+ }
296+ return float64(n) / float64(r.Dx()*r.Dy())
297+}
298+
299+// magentaShare is the fraction of pixels of r in the PNG at path that are
300+// clearly magenta; 0 when the image cannot be read yet.
301+func magentaShare(path string, r image.Rectangle) float64 {
302+ f, err := os.Open(path)
303+ if err != nil {
304+ return 0
305+ }
306+ defer f.Close()
307+ img, err := png.Decode(f)
308+ if err != nil {
309+ return 0
310+ }
311+ r = r.Intersect(img.Bounds())
312+ if r.Empty() {
313+ return 0
314+ }
315+ n := 0
316+ for y := r.Min.Y; y < r.Max.Y; y++ {
317+ for x := r.Min.X; x < r.Max.X; x++ {
318+ cr, cg, cb, _ := img.At(x, y).RGBA()
319+ if cr>>8 > 200 && cg>>8 < 60 && cb>>8 > 200 {
320+ n++
321+ }
322+ }
323+ }
324+ return float64(n) / float64(r.Dx()*r.Dy())
325+}
326+
327+// TestXwaylandOverride: an override-redirect window (a menu) of an X11 client
328+// is drawn above the toplevel at its X position, is not a window in the
329+// state, leaves keyboard focus on the toplevel and vanishes when unmapped.
330+func TestXwaylandOverride(t *testing.T) {
331+ e := startXwayland(t)
332+ // Probe unmaps the menu 3 s after mapping it; the first checks run well
333+ // inside that window.
334+ p := startProbe(t, e.bin, e.display, "-title", "menu", "-override", "-override-at", "100,50",
335+ "-override-size", "120x80", "-override-for", "3s")
336+ until(t, "override mapped", func() bool { return strings.Contains(p.out.String(), "override mapped") })
337+ shot := filepath.Join(e.shots, "latest.png")
338+ box := image.Rect(100, 50, 220, 130) // output-local: one output at the X origin
339+ until(t, "magenta menu in latest.png", func() bool { return magentaShare(shot, box) >= 0.9 })
340+
341+ var st statefile.State
342+ until(t, "focused X11Probe toplevel", func() bool {
343+ st = readState(e.statePath)
344+ return len(st.Windows) >= 1 && st.Window != nil
345+ })
346+ if len(st.Windows) != 1 || st.Windows[0].AppID != "X11Probe" {
347+ t.Fatalf("state windows = %+v, want only the X11Probe toplevel", st.Windows)
348+ }
349+ id := st.Windows[0].ID
350+ if st.Window.ID != id {
351+ t.Errorf("focused window = %+v, want the toplevel %d", st.Window, id)
352+ }
353+ until(t, "probe focus", func() bool { return strings.Contains(p.out.String(), "focus") })
354+
355+ // The menu is above the toplevel: its box is magenta, not green.
356+ if g := greenShare(shot, box); g > 0.1 {
357+ t.Errorf("green share in menu box = %.2f, menu is not above the toplevel", g)
358+ }
359+ until(t, "override unmapped", func() bool { return strings.Contains(p.out.String(), "override unmapped") })
360+ until(t, "menu gone from latest.png", func() bool {
361+ return magentaShare(shot, box) == 0 && greenShare(shot, box) >= 0.9
362+ })
363+ if st := readState(e.statePath); st.Window == nil || st.Window.ID != id {
364+ t.Errorf("focus after the menu closed = %+v, want %d", st.Window, id)
365+ }
366+}
367+
368+// TestXwaylandOverrideDismiss: the menu owner sees FocusOut on the toplevel
369+// when core focuses another window, which is how toolkits close their menus.
370+func TestXwaylandOverrideDismiss(t *testing.T) {
371+ e := startXwayland(t)
372+ p := startProbe(t, e.bin, e.display, "-title", "menu", "-override", "-override-dismiss-on-focusout")
373+ until(t, "override mapped", func() bool { return strings.Contains(p.out.String(), "override mapped") })
374+ until(t, "probe focus", func() bool { return strings.Contains(p.out.String(), "focus") })
375+ // A second X11 window takes the focus away from the menu owner.
376+ startProbe(t, e.bin, e.display, "-title", "other")
377+ until(t, "menu dismissed on FocusOut", func() bool { return strings.Contains(p.out.String(), "dismissed") })
378+ box := image.Rect(100, 50, 220, 130)
379+ until(t, "menu gone from latest.png", func() bool {
380+ return magentaShare(filepath.Join(e.shots, "latest.png"), box) == 0
381+ })
382+}
383+
384+// TestXwaylandOverrideShape: a menu with an empty input shape is still drawn
385+// and still not a window.
386+func TestXwaylandOverrideShape(t *testing.T) {
387+ e := startXwayland(t)
388+ p := startProbe(t, e.bin, e.display, "-title", "menu", "-override", "-override-shape", "-override-type", "tooltip")
389+ until(t, "override mapped", func() bool { return strings.Contains(p.out.String(), "override mapped") })
390+ until(t, "magenta tooltip in latest.png", func() bool {
391+ return magentaShare(filepath.Join(e.shots, "latest.png"), image.Rect(100, 50, 220, 130)) >= 0.9
392+ })
393+ until(t, "only the toplevel in state", func() bool { return len(readState(e.statePath).Windows) == 1 })
394+}
M internal/core/client.go +8 -0
@@ -70,6 +70,14 @@ func (c *Core) clientEvent(ctx context.Context, ev ports.ClientEvent) (publish,
7070 if _, w := c.screenOf(v.ID); w != nil {
7171 w.SetModal(v.ID, v.Modal)
7272 }
73+ case ports.UnmanagedMapped, ports.UnmanagedMoved, ports.UnmanagedUnmapped:
74+ changed, err := c.unmanagedEvent(ctx, v)
75+ if err != nil {
76+ return false, true
77+ }
78+ if !changed {
79+ return false, false
80+ }
7381 case ports.WindowUnmapped:
7482 if c.unmapClient(ctx, v) != nil {
7583 return false, true
M internal/core/core.go +16 -5
@@ -103,9 +103,11 @@ type Core struct {
103103 binds map[binding]Action
104104 pressed map[string]bool
105105 configures configures
106- pointer WindowID
107- grab WindowID
108- buttons map[uint32]bool
106+ // placed is the last position (X, Y of the Rect) sent in a PlaceWindow, per window.
107+ placed map[WindowID]Rect
108+ pointer WindowID
109+ grab WindowID
110+ buttons map[uint32]bool
109111 // drag is the pointer drag in progress (drag.go); swallow holds the
110112 // buttons whose release no client must see. mods are the modifiers
111113 // held, cmdMod the one keyboard.cmd names. lastButton is the latest
@@ -144,7 +146,11 @@ type Core struct {
144146 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.
145147 popups map[WindowID]*popupState
146148 popupOrder []WindowID
147- keyboard keyboard
149+ // unmanaged are the mapped override-redirect windows, unmanagedOrder
150+ // stacks them, bottom to top (unmanaged.go).
151+ unmanaged map[WindowID]*unmanagedState
152+ unmanagedOrder []WindowID
153+ keyboard keyboard
148154 // activity is when core last told wayland about user input
149155 // (ports.UserActivity), sent at most once per ActivityInterval.
150156 activity time.Time
@@ -417,7 +423,7 @@ func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) {
417423 return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")
418424 }
419425 // A placeholder screen holds windows until the first output arrives.
420- c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, opts: opts, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures(), ping: make(chan chan struct{}), out: newOutbox()}
426+ c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, unmanaged: map[WindowID]*unmanagedState{}, unmanagedOrder: make([]WindowID, 0, 32), ch: ch, opts: opts, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures(), placed: map[WindowID]Rect{}, ping: make(chan chan struct{}), out: newOutbox()}
421427 c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}
422428 if err := c.apply(cfg); err != nil {
423429 return nil, err
@@ -994,6 +1000,11 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
9941000 }
9951001 sc := c.screens[i]
9961002 lx, ly := x-float64(sc.x), y-float64(sc.y)
1003+ // Unmanaged windows are over everything, popups included; a locked
1004+ // pointer keeps its focus (a mouse-look game).
1005+ if id, px, py := c.unmanagedAt(sc, lx, ly); id != 0 {
1006+ return id, px, py
1007+ }
9971008 // Layer popups are over everything; window popups are over the windows
9981009 // only, under the top and overlay layers (as drawn).
9991010 if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
M internal/core/core_test.go +4 -0
@@ -38,6 +38,10 @@ func command(t *testing.T, ch <-chan ports.ClientCommand) ports.ClientCommand {
3838 for {
3939 switch v := receive(t, ch); v.(type) {
4040 case nil, ports.SetOutputs, ports.UserActivity:
41+ case ports.PlaceWindow:
42+ // Only Positioned (X11) windows are placed; every scenario
43+ // using this helper maps xdg windows.
44+ t.Fatalf("unexpected %+v", v)
4145 default:
4246 return v
4347 }
M internal/core/datadrag.go +1 -1
@@ -101,7 +101,7 @@ func dataDragHitChanged(ev ports.ClientEvent) bool {
101101 case ports.LayerChanged, ports.InputRegionChanged, ports.WindowMapped,
102102 ports.WindowResized, ports.PopupRequest, ports.PopupMapped,
103103 ports.OutputPower, ports.WindowParent, ports.WindowModal,
104- ports.WindowUnmapped, ports.PointerWarp, ports.WindowMoveRequest,
104+ ports.WindowUnmapped, ports.UnmanagedMapped, ports.UnmanagedMoved, ports.UnmanagedUnmapped, ports.PointerWarp, ports.WindowMoveRequest,
105105 ports.WindowFullscreenRequest, ports.WorkspaceActivate, ports.WindowActivate:
106106 return true
107107 default:
A internal/core/place_test.go +80 -0
@@ -0,0 +1,80 @@
1+package core_test
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/core"
8+ "github.com/bnema/neferwl/internal/ports"
9+)
10+
11+// places collects the PlaceWindow commands queued so far.
12+func places(commands chan ports.ClientCommand) (out []ports.PlaceWindow) {
13+ for len(commands) > 0 {
14+ if v, ok := (<-commands).(ports.PlaceWindow); ok {
15+ out = append(out, v)
16+ }
17+ }
18+ return out
19+}
20+
21+func TestPlaceWindow(t *testing.T) {
22+ cfg := altCmdDefaults()
23+ cfg.Border.Width = 0
24+ cfg.Layout.Gaps = 8
25+ client := make(chan ports.ClientEvent, 16)
26+ input := make(chan ports.InputEvent, 16)
27+ output := make(chan ports.OutputEvent, 8)
28+ commands := make(chan ports.ClientCommand, 256)
29+ scenes := make(chan []ports.Scene, 1)
30+ c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{})
31+ if err != nil {
32+ t.Fatal(err)
33+ }
34+ ctx, cancel := context.WithCancel(context.Background())
35+ defer cancel()
36+ go func() { _ = c.Run(ctx) }()
37+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
38+ scene(t, scenes)
39+
40+ // An xdg window is never placed.
41+ client <- ports.WindowMapped{ID: 1}
42+ scene(t, scenes)
43+ if got := places(commands); len(got) != 0 {
44+ t.Fatal(got)
45+ }
46+
47+ // A positioned window is placed once on map.
48+ client <- ports.WindowMapped{ID: 2, Positioned: true}
49+ scene(t, scenes)
50+ first := places(commands)
51+ if len(first) != 1 || first[0].ID != 2 {
52+ t.Fatal(first)
53+ }
54+
55+ // The same layout sends nothing more.
56+ client <- ports.WindowMapped{ID: 1}
57+ scene(t, scenes)
58+ if got := places(commands); len(got) != 0 {
59+ t.Fatal(got)
60+ }
61+
62+ // A position change sends it again, for the positioned window only.
63+ client <- ports.WindowMapped{ID: 3}
64+ scene(t, scenes)
65+ moved := places(commands)
66+ if len(moved) != 1 || moved[0].ID != 2 || (moved[0].X == first[0].X && moved[0].Y == first[0].Y) {
67+ t.Fatal(first, moved)
68+ }
69+
70+ // Unmap prunes it: mapping again places from nothing.
71+ client <- ports.WindowUnmapped{ID: 2}
72+ scene(t, scenes)
73+ places(commands)
74+ client <- ports.WindowMapped{ID: 2, Positioned: true}
75+ scene(t, scenes)
76+ again := places(commands)
77+ if len(again) != 1 || again[0].ID != 2 {
78+ t.Fatal(again)
79+ }
80+}
M internal/core/scene.go +12 -1
@@ -20,7 +20,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
2020 // layout is what is drawn; settled (same indexes) is where the
2121 // windows are going, and alone sizes the configures.
2222 layout, settled := sc.shown, sc.settledLayout
23- scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Radius: c.cfg.Border.Radius, Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)+1), Layers: shownLayers(sc)}
23+ scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Radius: c.cfg.Border.Radius, Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)+len(c.unmanagedOrder)+1), Layers: shownLayers(sc)}
2424 if !sc.mon.previewing() {
2525 // Tiles stop at the panels; the overview lays out previews over
2626 // the whole output.
@@ -116,8 +116,19 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
116116 }
117117 c.configures.mark(v)
118118 }
119+ if !p.Hidden && p.Preview == 0 && c.windows.lookup(p.ID).client.Positioned {
120+ cr := c.clientRect(ps)
121+ at := Rect{X: sc.x + cr.X, Y: sc.y + cr.Y}
122+ if old, ok := c.placed[p.ID]; !ok || old != at {
123+ if err := c.command(ctx, ports.PlaceWindow{ID: p.ID, X: at.X, Y: at.Y}); err != nil {
124+ return ports.Scene{}, err
125+ }
126+ c.placed[p.ID] = at
127+ }
128+ }
119129 }
120130 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
131+ scene.Windows = c.sceneUnmanaged(scene.Windows, sc)
121132 if icon, ok := c.dataDragIconOn(i, sc); ok {
122133 // The drag icon is over everything, popups and layers included.
123134 scene.Windows = append(scene.Windows, icon)
M internal/core/transition.go +1 -0
@@ -453,6 +453,7 @@ func (c *Core) unmapWindow(v ports.WindowUnmapped) {
453453 // The window is unmapped: a toplevel that maps again keeps its ID and
454454 // must be configured from nothing (a leaving entry keeps no configure).
455455 c.configures.forget(v.ID)
456+ delete(c.placed, v.ID)
456457 sc, w := c.screenOf(v.ID)
457458 if sc == nil {
458459 return
A internal/core/unmanaged.go +117 -0
@@ -0,0 +1,117 @@
1+package core
2+
3+import (
4+ "context"
5+ "slices"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// unmanagedState is a mapped override-redirect X11 window: an absolute layout
11+// rect chosen by the client, no parent, never keyboard-focused. rect is in
12+// global logical coordinates.
13+type unmanagedState struct {
14+ id WindowID
15+ rect Rect
16+ noInput bool
17+}
18+
19+// unmanagedEvent applies an unmanaged window event. changed is true when the
20+// scene or the pointer target may differ.
21+func (c *Core) unmanagedEvent(ctx context.Context, ev ports.ClientEvent) (changed bool, err error) {
22+ switch v := ev.(type) {
23+ case ports.UnmanagedMapped:
24+ r := Rect{X: v.X, Y: v.Y, W: v.Width, H: v.Height}
25+ if u := c.unmanaged[v.ID]; u != nil {
26+ if u.rect == r && u.noInput == v.NoInput {
27+ return false, nil
28+ }
29+ u.rect, u.noInput = r, v.NoInput
30+ } else {
31+ c.unmanaged[v.ID] = &unmanagedState{id: v.ID, rect: r, noInput: v.NoInput}
32+ c.unmanagedOrder = append(c.unmanagedOrder, v.ID)
33+ }
34+ case ports.UnmanagedMoved:
35+ u := c.unmanaged[v.ID]
36+ r := Rect{X: v.X, Y: v.Y, W: v.Width, H: v.Height}
37+ if u == nil || u.rect == r {
38+ return false, nil
39+ }
40+ u.rect = r
41+ case ports.UnmanagedUnmapped:
42+ if c.unmanaged[v.ID] == nil {
43+ return false, nil
44+ }
45+ delete(c.unmanaged, v.ID)
46+ c.unmanagedOrder = slices.DeleteFunc(c.unmanagedOrder, func(id WindowID) bool { return id == v.ID })
47+ c.windows.drop(v.ID)
48+ if err := c.dataDragGone(ctx, v.ID); err != nil {
49+ return false, err
50+ }
51+ // rehit does nothing while a button grab or drag is held: do not
52+ // leave the pointer and grab on the dead window.
53+ if c.grab == v.ID {
54+ c.grab = 0
55+ }
56+ if c.pointer == v.ID {
57+ c.pointer = 0
58+ if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil {
59+ return false, err
60+ }
61+ }
62+ }
63+ // The window under a still pointer may have changed.
64+ return true, c.rehit(ctx)
65+}
66+
67+// unmanagedScreen is the screen showing u: the one containing its origin, or
68+// the one the pointer clamp would pick when the origin is in a hole.
69+func (c *Core) unmanagedScreen(u *unmanagedState) *screen {
70+ x, y := float64(u.rect.X), float64(u.rect.Y)
71+ i := c.screenAt(x, y)
72+ if i < 0 {
73+ cx, cy := c.clampPointer(x, y, x, y)
74+ i = c.screenAt(cx, cy)
75+ }
76+ if i < 0 {
77+ return nil
78+ }
79+ return c.screens[i]
80+}
81+
82+// sceneUnmanaged appends the unmanaged windows shown on sc, bottom to top,
83+// drawn as popups over the layers. They are hidden while sc previews.
84+func (c *Core) sceneUnmanaged(out []ports.SceneWindow, sc *screen) []ports.SceneWindow {
85+ if len(c.unmanagedOrder) == 0 || sc.mon.previewing() {
86+ return out
87+ }
88+ for _, id := range c.unmanagedOrder {
89+ u := c.unmanaged[id]
90+ if u == nil || c.unmanagedScreen(u) != sc {
91+ continue
92+ }
93+ r := u.rect
94+ r.X, r.Y = r.X-sc.x, r.Y-sc.y
95+ out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true, OverLayers: true})
96+ }
97+ return out
98+}
99+
100+// unmanagedAt is the topmost unmanaged window taking the pointer at the
101+// screen-local point. None while the pointer is locked or the screen previews.
102+func (c *Core) unmanagedAt(sc *screen, lx, ly float64) (WindowID, float64, float64) {
103+ if len(c.unmanagedOrder) == 0 || c.constraint.Mode == ports.ConstraintLock || sc.mon.previewing() {
104+ return 0, 0, 0
105+ }
106+ for i := len(c.unmanagedOrder) - 1; i >= 0; i-- {
107+ u := c.unmanaged[c.unmanagedOrder[i]]
108+ if u == nil || u.noInput || c.unmanagedScreen(u) != sc {
109+ continue
110+ }
111+ x, y := lx-float64(u.rect.X-sc.x), ly-float64(u.rect.Y-sc.y)
112+ if x >= 0 && x < float64(u.rect.W) && y >= 0 && y < float64(u.rect.H) && c.acceptsInput(u.id, x, y) {
113+ return u.id, x, y
114+ }
115+ }
116+ return 0, 0, 0
117+}
A internal/core/unmanaged_test.go +145 -0
@@ -0,0 +1,145 @@
1+package core
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+func unmanagedScene(t *testing.T, c *Core, i int) ports.Scene {
11+ t.Helper()
12+ s, err := c.sceneFor(context.Background(), i, c.screens[i], nil, nil)
13+ if err != nil {
14+ t.Fatal(err)
15+ }
16+ return s
17+}
18+
19+func mapUnmanaged(t *testing.T, c *Core, id WindowID, x, y, w, h int, noInput bool) {
20+ t.Helper()
21+ c.clientEvent(context.Background(), ports.UnmanagedMapped{ID: id, X: x, Y: y, Width: w, Height: h, NoInput: noInput})
22+}
23+
24+func TestUnmanagedScene(t *testing.T) {
25+ c := publishRig(t, 2)
26+ mapUnmanaged(t, c, 100, 310, 20, 50, 40, false)
27+ mapUnmanaged(t, c, 101, 5, 5, 10, 10, true)
28+ b := unmanagedScene(t, c, 1)
29+ last := b.Windows[len(b.Windows)-1]
30+ if last.ID != 100 || last.Rect != (Rect{X: 10, Y: 20, W: 50, H: 40}) || !last.Popup || !last.OverLayers {
31+ t.Fatalf("screen B top window %+v", last)
32+ }
33+ a := unmanagedScene(t, c, 0)
34+ if got := a.Windows[len(a.Windows)-1]; got.ID != 101 || got.Rect.X != 5 {
35+ t.Fatalf("screen A top window %+v", got)
36+ }
37+ c.clientEvent(context.Background(), ports.UnmanagedMoved{ID: 100, X: 20, Y: 20, Width: 50, Height: 40})
38+ if got := unmanagedScene(t, c, 0).Windows; got[len(got)-2].ID != 100 || got[len(got)-2].Rect.X != 20 {
39+ t.Fatalf("moved window not on screen A: %+v", got)
40+ }
41+ c.clientEvent(context.Background(), ports.UnmanagedUnmapped{ID: 100})
42+ c.clientEvent(context.Background(), ports.UnmanagedUnmapped{ID: 101})
43+ if len(c.unmanagedOrder) != 0 || len(c.unmanaged) != 0 {
44+ t.Fatal("state left after unmap")
45+ }
46+}
47+
48+func TestUnmanagedHiddenWhilePreviewing(t *testing.T) {
49+ c := publishRig(t, 1)
50+ mapUnmanaged(t, c, 100, 10, 10, 50, 40, false)
51+ c.cur().mon.ov.open = true
52+ if !c.cur().mon.previewing() {
53+ t.Fatal("setup: not previewing")
54+ }
55+ for _, w := range unmanagedScene(t, c, 0).Windows {
56+ if w.ID == 100 {
57+ t.Fatal("unmanaged window drawn in the overview")
58+ }
59+ }
60+ if id, _, _ := c.hit(20, 20); id == 100 {
61+ t.Fatal("unmanaged window hit in the overview")
62+ }
63+}
64+
65+func TestUnmanagedHit(t *testing.T) {
66+ c := publishRig(t, 2)
67+ mapUnmanaged(t, c, 100, 310, 20, 50, 40, false)
68+ mapUnmanaged(t, c, 101, 320, 30, 10, 10, false)
69+ mapUnmanaged(t, c, 102, 340, 30, 10, 10, true)
70+ if id, x, y := c.hit(325, 35); id != 101 || x != 5 || y != 5 {
71+ t.Fatalf("topmost: %d %v %v", id, x, y)
72+ }
73+ if id, _, _ := c.hit(345, 35); id != 100 {
74+ t.Fatalf("noInput must be skipped: %d", id)
75+ }
76+ c.windows.setRegion(ports.InputRegionChanged{ID: 100, Rects: []Rect{{X: 0, Y: 0, W: 5, H: 5}}})
77+ if id, _, _ := c.hit(345, 35); id == 100 {
78+ t.Fatal("outside the input region")
79+ }
80+ c.constraint.Mode = ports.ConstraintLock
81+ if id, _, _ := c.hit(325, 35); id == 101 {
82+ t.Fatal("locked pointer must not move to an unmanaged window")
83+ }
84+ c.constraint.Mode = 0
85+ c.security.Protected = true
86+ if id, _, _ := c.hit(325, 35); id == 101 {
87+ t.Fatal("protected session hit an unmanaged window")
88+ }
89+}
90+
91+func unmanagedRig(t testing.TB, n int) *Core {
92+ c := publishRig(t, 1)
93+ for i := range n {
94+ c.clientEvent(context.Background(), ports.UnmanagedMapped{ID: WindowID(100 + i), X: 10 * i, Y: 10, Width: 30, Height: 30})
95+ }
96+ return c
97+}
98+
99+func publishOnce(t testing.TB, c *Core) {
100+ if err := c.publish(context.Background()); err != nil {
101+ t.Fatal(err)
102+ }
103+ <-c.ch.Scenes
104+}
105+
106+// TestUnmanagedPublishAllocations: three unmanaged windows cost no more than
107+// none (the scene window slice is presized for them).
108+func TestUnmanagedPublishAllocations(t *testing.T) {
109+ base, with := unmanagedRig(t, 0), unmanagedRig(t, 3)
110+ publishOnce(t, base)
111+ publishOnce(t, with)
112+ a := testing.AllocsPerRun(50, func() { publishOnce(t, base) })
113+ b := testing.AllocsPerRun(50, func() { publishOnce(t, with) })
114+ if b > a {
115+ t.Fatalf("publish allocs with unmanaged = %v, without = %v", b, a)
116+ }
117+}
118+
119+func TestUnmanagedHitAllocations(t *testing.T) {
120+ c := unmanagedRig(t, 3)
121+ if n := testing.AllocsPerRun(100, func() { c.hit(25, 20); c.hit(250, 150) }); n != 0 {
122+ t.Fatalf("hit allocs = %v", n)
123+ }
124+}
125+
126+func TestUnmanagedUnmapDuringGrabClearsPointer(t *testing.T) {
127+ c := publishRig(t, 1)
128+ mapUnmanaged(t, c, 100, 10, 10, 50, 40, false)
129+ cmds := make(chan ports.ClientCommand, 64)
130+ c.ch.Commands = cmds
131+ c.pointer, c.grab = 100, 100
132+ c.clientEvent(context.Background(), ports.UnmanagedUnmapped{ID: 100})
133+ if c.pointer == 100 || c.grab != 0 {
134+ t.Fatalf("pointer %d grab %d left on the dead window", c.pointer, c.grab)
135+ }
136+ var focus bool
137+ for len(cmds) > 0 {
138+ if f, ok := (<-cmds).(ports.PointerFocus); ok && f == (ports.PointerFocus{}) {
139+ focus = true
140+ }
141+ }
142+ if !focus {
143+ t.Fatal("pointer focus not reset")
144+ }
145+}
M internal/ports/client_commands.go +10 -0
@@ -47,6 +47,16 @@ type ConfigureWindow struct {
4747
4848 func (ConfigureWindow) clientCommand() {}
4949
50+// PlaceWindow carries core → wayland the position of a Positioned window's
51+// client rect, in layout logical coordinates (output origin included). It
52+// follows the ConfigureWindow of a frame and is sent once per change.
53+type PlaceWindow struct {
54+ ID WindowID
55+ X, Y int
56+}
57+
58+func (PlaceWindow) clientCommand() {}
59+
5060 // OutputPlacement is one output in the global layout. X, Y, Width and
5161 // Height are logical (after Transform: they swap for 90/270);
5262 // physical = logical × Scale. Info.Width and Info.Height are the physical
M internal/ports/client_events.go +28 -0
@@ -20,6 +20,9 @@ type WindowMapped struct {
2020 // xdg-foreign), 0 when none. A dialog of a fullscreen window shows over
2121 // it.
2222 Parent WindowID
23+ // Positioned windows are placed by core: it sends PlaceWindow with the
24+ // client rect origin. Set only by the X11 host, false for xdg.
25+ Positioned bool
2326 // Remap is set when the toplevel mapped before and was unmapped (a tray
2427 // app restored): window rules only place its first map.
2528 Remap bool
@@ -49,6 +52,31 @@ type WindowUnmapped struct{ ID WindowID }
4952
5053 func (WindowUnmapped) clientEvent() {}
5154
55+// UnmanagedMapped carries wayland → core that an override-redirect X11 window
56+// shows. The rect is its place in the layout (logical coordinates). NoInput
57+// windows (tooltips, drag icons, notifications) never take the pointer.
58+type UnmanagedMapped struct {
59+ ID WindowID
60+ X, Y, Width, Height int
61+ NoInput bool
62+}
63+
64+func (UnmanagedMapped) clientEvent() {}
65+
66+// UnmanagedMoved carries wayland → core a move and/or resize of a mapped
67+// unmanaged window.
68+type UnmanagedMoved struct {
69+ ID WindowID
70+ X, Y, Width, Height int
71+}
72+
73+func (UnmanagedMoved) clientEvent() {}
74+
75+// UnmanagedUnmapped carries wayland → core that an unmanaged window is gone.
76+type UnmanagedUnmapped struct{ ID WindowID }
77+
78+func (UnmanagedUnmapped) clientEvent() {}
79+
5280 // ShortcutsInhibit carries wayland → core that a window asks for (Active)
5381 // or stops asking for all keys, compositor binds included, while it has
5482 // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1).
M internal/ports/config.go +17 -3
@@ -101,9 +101,8 @@ type Config struct {
101101 Command []string
102102 AutoOpen string
103103 }
104- // Xwayland is the xwayland-satellite binary serving X11 clients; empty
105- // disables X11.
106- Xwayland string
104+ // Xwayland selects the X11 launcher.
105+ Xwayland XwaylandConfig
107106 // Startup are commands run once when the session starts, in order.
108107 // One startup line may hold several, split by SplitCommands.
109108 Startup [][]string
@@ -298,3 +297,18 @@ type WindowRule struct {
298297 // (1 to 16; 0 leaves it automatic).
299298 Cells int
300299 }
300+
301+// XwaylandBackend selects how X11 clients are served.
302+type XwaylandBackend uint8
303+
304+const (
305+ XwaylandOff XwaylandBackend = iota
306+ XwaylandSatellite
307+ XwaylandBuiltin
308+)
309+
310+// XwaylandConfig selects the launcher and its executable.
311+type XwaylandConfig struct {
312+ Backend XwaylandBackend
313+ Binary string
314+}

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