#21 WIP: feat(xwayland): add built-in Xwayland launcher

state
draft
branch
feat/xwayland-builtin → main
opened
bnema ·
updated
CommitSubjectAuthorDateSignatureStatus
8545a93Merge branch 'fix/hdr-sdr-plane' into feat/xwayland-builtinbriceGPG Verified
8342bf4Merge branch 'fix/x11-fullscreen-restore' into feat/xwayland-builtinbriceGPG Verified
7f7b429fix(drm): compose SDR buffers on HDR outputs until the plane pipeline is verifiedbriceGPG Verified
61f9ab3fix(core): restore fullscreen when focus returns to a window that left itbriceGPG Verified
41da69cfix(core): honour the fullscreen state a window maps withbriceGPG Verified
5eb9885test(xwm): make sinkPeer deterministicbriceGPG Verified
13ff6acfix(xwm): announce scale 1 to X11 clients while Xwayland runs at logical sizebriceGPG Verified
f2f4074test(xwm): measure the Wayland-to-X alloc path through onSelectionRequestbriceGPG Verified
04b38e5feat(xwm): answer TIMESTAMP and list it in TARGETSbriceGPG Verified
6168668fix(xwm): harden selection transfers on close, late replies and theme truncationbriceGPG Verified
8fb84bfrefactor(xwm): replace addFD/after function seams with a mockery-generated loop interfacebriceGPG Verified
07dab5dfix(xwm): never adopt the XSETTINGS window as a client windowbriceGPG Verified
a50e31efix(xwm): drop stale TARGETS replies after Wayland takes ownershipbriceGPG Verified
4afca7adocs(xwayland): clipboard bridge, XSETTINGS and root cursorbriceGPG Verified
def433btest(app): e2e clipboard bridge, XSETTINGS and root cursor on the built-in XWMbriceGPG Verified
3e19081Merge branch 'feat/x10' into feat/xwayland-builtinbriceGPG Verified
d7ef3dbMerge branch 'feat/x9' into feat/xwayland-builtinbriceGPG Verified
9bbf554feat(xwm): XSETTINGS manager, xwayland.xsettings key and root cursorbriceGPG Verified
46fd2a1Merge branch 'feat/x8' into feat/xwayland-builtinbriceGPG Verified
1f67695feat(wayland): Wayland to X text selection with INCR and X-owned clipboard sourcesbriceGPG Verified
3df83c3docs(xwayland): describe fullscreen, keyboard grab, output hint and steam_app rulesbriceGPG Verified
8c66911feat(xwm): X to Wayland text selection transfers with INCR, buffer pool and timeoutsbriceGPG Verified
ad55fe8feat(xwm): host timer seam After for selection timeoutsbriceGPG Verified
41edcc9feat(xwm): selection window, XFixes ownership events and TARGETS probebriceGPG Verified
d71e4a4test(app): cover X11 fullscreen, keyboard grab, output hint and class rules end to endbriceGPG Verified
43a1ce7test(wayland): wait for the output change before counting surface entersbriceGPG Verified
1b0bfc7fix(core): keep rule workspaces ahead of the X11 output hint and read the keyboard grab once per syncbriceGPG Verified
56a0abdMerge branch 'feat/x8-pointer' into feat/x8briceGPG Verified
b77ab13Merge branch 'feat/x8-grab' into feat/x8briceGPG Verified
224cf30feat(xwm): place output-sized X11 windows on their output, read bypass hint, steam_app rulesbriceGPG Verified
c5b7312docs(xwayland): state that builtin shows menus, popups and tooltipsbriceGPG Verified
864c005test(wayland): cover X11 pointer lock, confine, relative motion and single-output enterbriceGPG Verified
65eceabfeat(wayland): serve zwp_xwayland_keyboard_grab as a fullscreen shortcuts inhibitorbriceGPG Verified
47739datest(app): cover override-redirect X11 windows end to endbriceGPG Verified
dc722bbrefactor(xwm): share one Space with the wayland server and drop dead codebriceGPG Verified
8ab1d07fix(core): clear pointer and grab when an unmanaged window unmapsbriceGPG Verified
b3f6b3afeat(wayland): host override-redirect X11 windows as unmanaged windowsbriceGPG Verified
27b8b9eMerge branch 'feat/x7-xdgoutput' into feat/x7briceGPG Verified
83719a6Merge branch 'feat/x7-core' into feat/x7briceGPG Verified
7ef926ffeat(xwm): convert Place through Space and track override-redirect windowsbriceGPG Verified
5a832c8feat(xwm): add Space mapping layout coordinates to X coordinatesbriceGPG Verified
305c2c8feat(wayland): private X-space xdg-output manager for XwaylandbriceGPG Verified
bf24250feat(core): place, draw and hit-test unmanaged X11 windowsbriceGPG Verified
58a7346refactor(xwm): log counters on close, drop the test-only Slot lookup and state atoms in present tensebriceGPG Verified
80258b6docs(xwayland): describe the built-in window manager and the xwayland e2e checkbriceGPG Verified
b64a207test(app): add headless Xwayland end-to-end test with an x11probe clientbriceGPG Verified
ace7dd1fix(wayland): forward X11 pointer constraints and modal changes, remap on surface replacement, clear dangling parentsbriceGPG Verified
24f47ebfeat(wayland): integrate the built-in XWM with the serverbriceGPG Verified
e1de5a5test(xwm): replace the sinkPeer sleep with a peer lock barrierbriceGPG Verified
9439437fix(xwm): stop _NET_WM_STATE feedback, honour pre-ready geometry, drop stale map repliesbriceGPG Verified
3ccf649Merge branch 'feat/x6-configure' into feat/x6-xwmbriceGPG Verified
ce78abcfeat(xwm): add map flow, property decoding, serial pairing and reparent handlingbriceGPG Verified
5021c6ctest(xwm): fix staticcheck findings in configure testsbriceGPG Verified
50a2e95docs(xwm): state that Boot takes a component-scoped loggerbriceGPG Verified
f1a2966feat(xwm): add configure, focus, close and stacking lists for X11 windowsbriceGPG Verified
01f9a3dfix(xwm): check root ownership before claiming WM state and never adopt the check windowbriceGPG Verified
69b4382feat(xwm): add window table, boot and fd flush skeleton for the built-in X11 window managerbriceGPG Verified
3ed03aabuild(deps): add bnema.dev/xturbo v0.1.0briceGPG Verified
7646086Merge branch 'feat/x6-place' into feat/xwayland-builtinbriceGPG Verified
aa24d35Merge branch 'feat/x6-window-role' into feat/xwayland-builtinbriceGPG Verified
4eada3btest(xwayland): assert AttachWM is skipped when Xwayland exits before readinessbriceGPG Verified
04fea8dtest(vulkan): skip purego allocation budgets under the race runtimebriceGPG Verified
cac0ab6feat(core): send PlaceWindow for positioned windows on position changebriceGPG Verified
957ec9crefactor(wayland): route window access through role-neutral accessorsbriceGPG Verified
57e2ff1refactor(xwayland): split attachment into Wayland fd at start and WM fd after readinessbriceGPG Verified
db0a693docs(xwayland): state that the built-in backend does not manage X11 windowsbriceGPG Verified
e519f1fbuild(deps): bump purego-libwayland to v0.12.0 and purego to v0.13.0-bnema.2briceGPG Verified
a97cce0docs(xwayland): describe the built-in backend and drop the launcher handoff notebriceGPG Verified
e82a03efeat(xwayland): add experimental built-in launcher and client attachmentbriceGPG Verified

Checks 1

note: a failing or unfinished check blocks the merge.

107 files · +18450 -160
1/1
M .mockery.yml +30 -0
M Makefile +9 -2
M README.md +1 -1
M docs/config.md +9 -3
M docs/desktop.md +12 -1
M docs/dev/headless.md +25 -0
M go.mod +4 -3
M go.sum +8 -6
M internal/adapters/config/config.go +18 -4
M internal/adapters/config/config_test.go +36 -4
M internal/adapters/drm/colorop_match.go +11 -0
A internal/adapters/drm/sdrplane_test.go +30 -0
M internal/adapters/vulkan/capture_test.go +3 -0
A internal/adapters/vulkan/norace_test.go +5 -0
A internal/adapters/vulkan/race_test.go +8 -0
M internal/adapters/vulkan/scene_perf_test.go +6 -0
M internal/adapters/wayland/activation.go +3 -3
M internal/adapters/wayland/alloc_hotpath_test.go +9 -0
M internal/adapters/wayland/clipboard.go +5 -0
A internal/adapters/wayland/clipboard_xwm.go +93 -0
A internal/adapters/wayland/clipboard_xwm_test.go +147 -0
M internal/adapters/wayland/constraints.go +14 -13
M internal/adapters/wayland/decoration.go +1 -1
M internal/adapters/wayland/dialog.go +1 -1
M internal/adapters/wayland/dmabuf.go +3 -2
M internal/adapters/wayland/extforeigntoplevel.go +1 -1
M internal/adapters/wayland/foreigntoplevel.go +6 -6
M internal/adapters/wayland/frames.go +10 -6
M internal/adapters/wayland/globals.go +11 -1
M internal/adapters/wayland/imagecopy.go +1 -1
M internal/adapters/wayland/inhibit.go +5 -0
M internal/adapters/wayland/outputs.go +72 -1
M internal/adapters/wayland/popup.go +2 -2
M internal/adapters/wayland/scale.go +5 -3
M internal/adapters/wayland/screencast.go +1 -1
M internal/adapters/wayland/seat.go +11 -9
M internal/adapters/wayland/server.go +51 -16
M internal/adapters/wayland/surface.go +15 -2
A internal/adapters/wayland/window.go +112 -0
M internal/adapters/wayland/wire_client_test.go +6 -0
A internal/adapters/wayland/x11override_test.go +107 -0
A internal/adapters/wayland/x11peer_test.go +332 -0
A internal/adapters/wayland/x11window.go +458 -0
A internal/adapters/wayland/x11window_test.go +627 -0
M internal/adapters/wayland/xdg.go +32 -21
A internal/adapters/wayland/xdgoutputx_test.go +143 -0
A internal/adapters/wayland/xwayland.go +75 -0
A internal/adapters/wayland/xwayland_test.go +84 -0
A internal/adapters/wayland/xwaylandgrab.go +90 -0
A internal/adapters/wayland/xwm/atoms.go +118 -0
A internal/adapters/wayland/xwm/configure.go +317 -0
A internal/adapters/wayland/xwm/configure_test.go +591 -0
A internal/adapters/wayland/xwm/cursor.go +109 -0
A internal/adapters/wayland/xwm/cursor_test.go +86 -0
A internal/adapters/wayland/xwm/events.go +303 -0
A internal/adapters/wayland/xwm/fdsource_mock_test.go +129 -0
A internal/adapters/wayland/xwm/fixes_test.go +230 -0
A internal/adapters/wayland/xwm/focus.go +218 -0
A internal/adapters/wayland/xwm/host.go +82 -0
A internal/adapters/wayland/xwm/host_mock_test.go +775 -0
A internal/adapters/wayland/xwm/loop_mock_test.go +180 -0
A internal/adapters/wayland/xwm/mapflow.go +359 -0
A internal/adapters/wayland/xwm/mapflow_test.go +806 -0
A internal/adapters/wayland/xwm/props.go +376 -0
A internal/adapters/wayland/xwm/selection.go +1050 -0
A internal/adapters/wayland/xwm/selection_test.go +801 -0
A internal/adapters/wayland/xwm/selection_xsettings.go +198 -0
A internal/adapters/wayland/xwm/space.go +158 -0
A internal/adapters/wayland/xwm/space_test.go +110 -0
A internal/adapters/wayland/xwm/streams_test.go +197 -0
A internal/adapters/wayland/xwm/table.go +242 -0
A internal/adapters/wayland/xwm/table_test.go +138 -0
A internal/adapters/wayland/xwm/testdata/streams/property_unchanged.hex +950 -0
A internal/adapters/wayland/xwm/testdata/streams/steady.hex +2150 -0
A internal/adapters/wayland/xwm/unmanaged.go +109 -0
A internal/adapters/wayland/xwm/unmanaged_test.go +247 -0
A internal/adapters/wayland/xwm/xpeer_test.go +655 -0
A internal/adapters/wayland/xwm/xsettings_test.go +244 -0
A internal/adapters/wayland/xwm/xwm.go +509 -0
A internal/adapters/wayland/xwm/xwm_test.go +387 -0
A internal/adapters/wayland/xwm_selection_mock_test.go +143 -0
A internal/adapters/xwayland/builtin.go +216 -0
A internal/adapters/xwayland/starter_mock_test.go +110 -0
M internal/adapters/xwayland/xwayland.go +46 -18
M internal/adapters/xwayland/xwayland_test.go +207 -0
M internal/app/app.go +13 -6
M internal/app/cursor.go +18 -0
M internal/app/session.go +2 -1
A internal/app/testdata/x11probe/main.go +692 -0
A internal/app/xwayland_test.go +808 -0
M internal/core/client.go +8 -0
M internal/core/core.go +16 -5
M internal/core/core_test.go +52 -3
M internal/core/datadrag.go +1 -1
M internal/core/fullscreen_left_core_test.go +1 -6
M internal/core/fullscreen_left_test.go +29 -0
M internal/core/layout.go +7 -1
A internal/core/place_test.go +80 -0
M internal/core/rules_test.go +44 -0
M internal/core/scene.go +12 -1
M internal/core/spawn_placement.go +9 -1
@@ -139,7 +139,15 @@ func (p *spawnPlacement) place(c *Core, v ports.WindowMapped) {
139139 }
140140 }
141141 if s, _ := c.screenOf(v.ID); s == nil {
142- if eff, ok := c.ruleFor(v); ok {
142+ eff, ok := c.ruleFor(v)
143+ // A window mapping at an output's rect goes to that output unless a
144+ // rule names a monitor or workspace.
145+ if v.HasAt && !v.Remap && eff.monitor == "" && eff.workspace == "" && eff.number == 0 {
146+ if i := c.screenAt(float64(v.AtX), float64(v.AtY)); i >= 0 && i != c.focusScreen {
147+ eff.monitor, ok = c.screens[i].name(), true
148+ }
149+ }
150+ if ok {
143151 c.placeByRule(v, eff)
144152 return
145153 }
@@ -139,7 +139,15 @@ func (p *spawnPlacement) place(c *Core, v ports.WindowMapped) {
139 }139 }
140 }140 }
141 if s, _ := c.screenOf(v.ID); s == nil {141 if s, _ := c.screenOf(v.ID); s == nil {
142- if eff, ok := c.ruleFor(v); ok {142+ eff, ok := c.ruleFor(v)
143+ // A window mapping at an output's rect goes to that output unless a
144+ // rule names a monitor or workspace.
145+ if v.HasAt && !v.Remap && eff.monitor == "" && eff.workspace == "" && eff.number == 0 {
146+ if i := c.screenAt(float64(v.AtX), float64(v.AtY)); i >= 0 && i != c.focusScreen {
147+ eff.monitor, ok = c.screens[i].name(), true
148+ }
149+ }
150+ if ok {
143 c.placeByRule(v, eff)151 c.placeByRule(v, eff)
144 return152 return
145 }153 }
M internal/core/transition.go +20 -0
@@ -400,11 +400,17 @@ func (c *Core) appearFrom(rm *rectMotion, p Placement, old *rectMotion, now time
400400 // client is configured once, to its final size.
401401 func (c *Core) mapWindow(v ports.WindowMapped) {
402402 now := c.now()
403+ if v.Fullscreen && v.Parent == 0 {
404+ // Mapped fullscreen: a tile of its own, whatever size class the
405+ // client reported (a fixed-size game is not a float).
406+ v.Floating, v.Width, v.Height = false, 0, 0
407+ }
403408 c.windows.mapped(v, now)
404409 if s, _ := c.screenOf(v.ID); s != nil || !c.animOn() || c.security.Protected {
405410 // Mapped before (a re-map keeps its place), animations off or a
406411 // session lock: nothing to animate.
407412 c.placement.place(c, v)
413+ c.initialFullscreen(v)
408414 return
409415 }
410416 shots := c.snapshot(now)
@@ -416,10 +422,23 @@ func (c *Core) mapWindow(v ports.WindowMapped) {
416422 }
417423 }
418424 c.placement.place(c, v)
425+ c.initialFullscreen(v)
419426 c.transition(shots, now)
420427 c.appearMapped(v.ID, now)
421428 }
422429
430+// initialFullscreen makes a window that mapped fullscreen cover its screen.
431+// The fullscreen grace does not apply: it is the state the client opened
432+// with, not a later request. A re-map (screen already known) is left alone.
433+func (c *Core) initialFullscreen(v ports.WindowMapped) {
434+ if !v.Fullscreen || v.Remap {
435+ return
436+ }
437+ if s, _ := c.screenOf(v.ID); s != nil {
438+ s.mon.SetFullscreen(v.ID, true)
439+ }
440+}
441+
423442 // appearMapped starts the entrance of a window that just mapped when it
424443 // shows on its screen's current workspace as a tile or an ordinary float:
425444 // not with a preview open or a session lock, not fullscreen (it keeps
@@ -453,6 +472,7 @@ func (c *Core) unmapWindow(v ports.WindowUnmapped) {
453472 // The window is unmapped: a toplevel that maps again keeps its ID and
454473 // must be configured from nothing (a leaving entry keeps no configure).
455474 c.configures.forget(v.ID)
475+ delete(c.placed, v.ID)
456476 sc, w := c.screenOf(v.ID)
457477 if sc == nil {
458478 return
@@ -400,11 +400,17 @@ func (c *Core) appearFrom(rm *rectMotion, p Placement, old *rectMotion, now time
400 // client is configured once, to its final size.400 // client is configured once, to its final size.
401 func (c *Core) mapWindow(v ports.WindowMapped) {401 func (c *Core) mapWindow(v ports.WindowMapped) {
402 now := c.now()402 now := c.now()
403+ if v.Fullscreen && v.Parent == 0 {
404+ // Mapped fullscreen: a tile of its own, whatever size class the
405+ // client reported (a fixed-size game is not a float).
406+ v.Floating, v.Width, v.Height = false, 0, 0
407+ }
403 c.windows.mapped(v, now)408 c.windows.mapped(v, now)
404 if s, _ := c.screenOf(v.ID); s != nil || !c.animOn() || c.security.Protected {409 if s, _ := c.screenOf(v.ID); s != nil || !c.animOn() || c.security.Protected {
405 // Mapped before (a re-map keeps its place), animations off or a410 // Mapped before (a re-map keeps its place), animations off or a
406 // session lock: nothing to animate.411 // session lock: nothing to animate.
407 c.placement.place(c, v)412 c.placement.place(c, v)
413+ c.initialFullscreen(v)
408 return414 return
409 }415 }
410 shots := c.snapshot(now)416 shots := c.snapshot(now)
@@ -416,10 +422,23 @@ func (c *Core) mapWindow(v ports.WindowMapped) {
416 }422 }
417 }423 }
418 c.placement.place(c, v)424 c.placement.place(c, v)
425+ c.initialFullscreen(v)
419 c.transition(shots, now)426 c.transition(shots, now)
420 c.appearMapped(v.ID, now)427 c.appearMapped(v.ID, now)
421 }428 }
422 429
430+// initialFullscreen makes a window that mapped fullscreen cover its screen.
431+// The fullscreen grace does not apply: it is the state the client opened
432+// with, not a later request. A re-map (screen already known) is left alone.
433+func (c *Core) initialFullscreen(v ports.WindowMapped) {
434+ if !v.Fullscreen || v.Remap {
435+ return
436+ }
437+ if s, _ := c.screenOf(v.ID); s != nil {
438+ s.mon.SetFullscreen(v.ID, true)
439+ }
440+}
441+
423 // appearMapped starts the entrance of a window that just mapped when it442 // appearMapped starts the entrance of a window that just mapped when it
424 // shows on its screen's current workspace as a tile or an ordinary float:443 // shows on its screen's current workspace as a tile or an ordinary float:
425 // not with a preview open or a session lock, not fullscreen (it keeps444 // not with a preview open or a session lock, not fullscreen (it keeps
@@ -453,6 +472,7 @@ func (c *Core) unmapWindow(v ports.WindowUnmapped) {
453 // The window is unmapped: a toplevel that maps again keeps its ID and472 // The window is unmapped: a toplevel that maps again keeps its ID and
454 // must be configured from nothing (a leaving entry keeps no configure).473 // must be configured from nothing (a leaving entry keeps no configure).
455 c.configures.forget(v.ID)474 c.configures.forget(v.ID)
475+ delete(c.placed, v.ID)
456 sc, w := c.screenOf(v.ID)476 sc, w := c.screenOf(v.ID)
457 if sc == nil {477 if sc == nil {
458 return478 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+}
@@ -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+}
@@ -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
@@ -47,6 +47,16 @@ type ConfigureWindow struct {
47 47
48 func (ConfigureWindow) clientCommand() {}48 func (ConfigureWindow) clientCommand() {}
49 49
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+
50 // OutputPlacement is one output in the global layout. X, Y, Width and60 // OutputPlacement is one output in the global layout. X, Y, Width and
51 // Height are logical (after Transform: they swap for 90/270);61 // Height are logical (after Transform: they swap for 90/270);
52 // physical = logical × Scale. Info.Width and Info.Height are the physical62 // physical = logical × Scale. Info.Width and Info.Height are the physical
M internal/ports/client_events.go +37 -0
@@ -20,12 +20,24 @@ 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
2629 // Modal is set for a dialog that blocks its parent (xdg_dialog_v1):
2730 // focusing the parent focuses the dialog.
2831 Modal bool
32+ // Fullscreen is set when the client asked for fullscreen before mapping
33+ // (X11 _NET_WM_STATE_FULLSCREEN): core maps it as a tile covering its
34+ // screen, without the fullscreen grace of later requests.
35+ Fullscreen bool
36+ // HasAt asks for the screen containing the layout point (AtX, AtY) when
37+ // no rule sets a monitor: the X11 host sets it for a window that maps at
38+ // exactly one output's rect.
39+ HasAt bool
40+ AtX, AtY int
2941 }
3042
3143 // SlotEnv is the environment variable neferwl sets on processes it spawns
@@ -49,6 +61,31 @@ type WindowUnmapped struct{ ID WindowID }
4961
5062 func (WindowUnmapped) clientEvent() {}
5163
64+// UnmanagedMapped carries wayland → core that an override-redirect X11 window
65+// shows. The rect is its place in the layout (logical coordinates). NoInput
66+// windows (tooltips, drag icons, notifications) never take the pointer.
67+type UnmanagedMapped struct {
68+ ID WindowID
69+ X, Y, Width, Height int
70+ NoInput bool
71+}
72+
73+func (UnmanagedMapped) clientEvent() {}
74+
75+// UnmanagedMoved carries wayland → core a move and/or resize of a mapped
76+// unmanaged window.
77+type UnmanagedMoved struct {
78+ ID WindowID
79+ X, Y, Width, Height int
80+}
81+
82+func (UnmanagedMoved) clientEvent() {}
83+
84+// UnmanagedUnmapped carries wayland → core that an unmanaged window is gone.
85+type UnmanagedUnmapped struct{ ID WindowID }
86+
87+func (UnmanagedUnmapped) clientEvent() {}
88+
5289 // ShortcutsInhibit carries wayland → core that a window asks for (Active)
5390 // or stops asking for all keys, compositor binds included, while it has
5491 // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1).
@@ -20,12 +20,24 @@ type WindowMapped struct {
20 // xdg-foreign), 0 when none. A dialog of a fullscreen window shows over20 // xdg-foreign), 0 when none. A dialog of a fullscreen window shows over
21 // it.21 // it.
22 Parent WindowID22 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
23 // Remap is set when the toplevel mapped before and was unmapped (a tray26 // Remap is set when the toplevel mapped before and was unmapped (a tray
24 // app restored): window rules only place its first map.27 // app restored): window rules only place its first map.
25 Remap bool28 Remap bool
26 // Modal is set for a dialog that blocks its parent (xdg_dialog_v1):29 // Modal is set for a dialog that blocks its parent (xdg_dialog_v1):
27 // focusing the parent focuses the dialog.30 // focusing the parent focuses the dialog.
28 Modal bool31 Modal bool
32+ // Fullscreen is set when the client asked for fullscreen before mapping
33+ // (X11 _NET_WM_STATE_FULLSCREEN): core maps it as a tile covering its
34+ // screen, without the fullscreen grace of later requests.
35+ Fullscreen bool
36+ // HasAt asks for the screen containing the layout point (AtX, AtY) when
37+ // no rule sets a monitor: the X11 host sets it for a window that maps at
38+ // exactly one output's rect.
39+ HasAt bool
40+ AtX, AtY int
29 }41 }
30 42
31 // SlotEnv is the environment variable neferwl sets on processes it spawns43 // SlotEnv is the environment variable neferwl sets on processes it spawns
@@ -49,6 +61,31 @@ type WindowUnmapped struct{ ID WindowID }
49 61
50 func (WindowUnmapped) clientEvent() {}62 func (WindowUnmapped) clientEvent() {}
51 63
64+// UnmanagedMapped carries wayland → core that an override-redirect X11 window
65+// shows. The rect is its place in the layout (logical coordinates). NoInput
66+// windows (tooltips, drag icons, notifications) never take the pointer.
67+type UnmanagedMapped struct {
68+ ID WindowID
69+ X, Y, Width, Height int
70+ NoInput bool
71+}
72+
73+func (UnmanagedMapped) clientEvent() {}
74+
75+// UnmanagedMoved carries wayland → core a move and/or resize of a mapped
76+// unmanaged window.
77+type UnmanagedMoved struct {
78+ ID WindowID
79+ X, Y, Width, Height int
80+}
81+
82+func (UnmanagedMoved) clientEvent() {}
83+
84+// UnmanagedUnmapped carries wayland → core that an unmanaged window is gone.
85+type UnmanagedUnmapped struct{ ID WindowID }
86+
87+func (UnmanagedUnmapped) clientEvent() {}
88+
52 // ShortcutsInhibit carries wayland → core that a window asks for (Active)89 // ShortcutsInhibit carries wayland → core that a window asks for (Active)
53 // or stops asking for all keys, compositor binds included, while it has90 // or stops asking for all keys, compositor binds included, while it has
54 // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1).91 // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1).
M internal/ports/config.go +35 -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,36 @@ 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+ // XSettings (xwayland.xsettings) turns the XSETTINGS manager of the
315+ // built-in XWM on.
316+ XSettings bool
317+}
318+
319+// XwaylandSettings is the plain data the built-in XWM publishes to X11
320+// clients; the app fills it from the config and the environment.
321+type XwaylandSettings struct {
322+ // XSettings turns the XSETTINGS manager on.
323+ XSettings bool
324+ // CursorTheme and CursorSize are XCURSOR_THEME and XCURSOR_SIZE
325+ // (published as Gtk/CursorThemeName and Gtk/CursorThemeSize).
326+ CursorTheme string
327+ CursorSize int
328+ // RootCursor sets a cursor on the X root window. Cursor is its image
329+ // (premultiplied ARGB8888, little-endian); empty uses the X "left_ptr" glyph.
330+ RootCursor bool
331+ Cursor CursorImage
332+}
@@ -101,9 +101,8 @@ type Config struct {
101 Command []string101 Command []string
102 AutoOpen string102 AutoOpen string
103 }103 }
104- // Xwayland is the xwayland-satellite binary serving X11 clients; empty104+ // Xwayland selects the X11 launcher.
105- // disables X11.105+ Xwayland XwaylandConfig
106- Xwayland string
107 // Startup are commands run once when the session starts, in order.106 // Startup are commands run once when the session starts, in order.
108 // One startup line may hold several, split by SplitCommands.107 // One startup line may hold several, split by SplitCommands.
109 Startup [][]string108 Startup [][]string
@@ -298,3 +297,36 @@ type WindowRule struct {
298 // (1 to 16; 0 leaves it automatic).297 // (1 to 16; 0 leaves it automatic).
299 Cells int298 Cells int
300 }299 }
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+ // XSettings (xwayland.xsettings) turns the XSETTINGS manager of the
315+ // built-in XWM on.
316+ XSettings bool
317+}
318+
319+// XwaylandSettings is the plain data the built-in XWM publishes to X11
320+// clients; the app fills it from the config and the environment.
321+type XwaylandSettings struct {
322+ // XSettings turns the XSETTINGS manager on.
323+ XSettings bool
324+ // CursorTheme and CursorSize are XCURSOR_THEME and XCURSOR_SIZE
325+ // (published as Gtk/CursorThemeName and Gtk/CursorThemeSize).
326+ CursorTheme string
327+ CursorSize int
328+ // RootCursor sets a cursor on the X root window. Cursor is its image
329+ // (premultiplied ARGB8888, little-endian); empty uses the X "left_ptr" glyph.
330+ RootCursor bool
331+ Cursor CursorImage
332+}

Comments0 open / 0

no comments yet.

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
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close