detached b77ab13

Merge branch 'feat/x8-grab' into feat/x8

commit
b77ab1345dcc9f9b0c7282a2c07e021cc9ea32aa
author
brice · · 2026-10-22 15:35 UTC
committer
brice · · 2026-10-22 17:54 UTC
merge of
224cf30 + 65eceab · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +175 -1
M Makefile +1 -1
@@ -70,7 +70,7 @@ adapter-imports:
7070 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
7171 # Fast, mandatory guards for tiled content publications and SHM copies.
7272 perf-check:
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|TestFullscreenConfigureAllocations)$$' -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|TestFullscreenConfigureAllocations|TestKeyboardGrabSyncAllocations)$$' -count=1
7474 CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1
7575 check: vet test arch fakes-check log-check perf-check
7676
@@ -70,7 +70,7 @@ adapter-imports:
70 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }70 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
71 # Fast, mandatory guards for tiled content publications and SHM copies.71 # Fast, mandatory guards for tiled content publications and SHM copies.
72 perf-check:72 perf-check:
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|TestFullscreenConfigureAllocations)$$' -count=173+ 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|TestFullscreenConfigureAllocations|TestKeyboardGrabSyncAllocations)$$' -count=1
74 CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=174 CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1
75 check: vet test arch fakes-check log-check perf-check75 check: vet test arch fakes-check log-check perf-check
76 76
M internal/adapters/wayland/alloc_hotpath_test.go +9 -0
@@ -246,3 +246,12 @@ func TestCommandForwardAllocations(t *testing.T) {
246246 t.Errorf("command push + merge + drain + stats: %.1f allocs, want 0", allocs)
247247 }
248248 }
249+
250+// A settled grab (none, or unchanged) is re-checked at every focus and
251+// configure without allocating.
252+func TestKeyboardGrabSyncAllocations(t *testing.T) {
253+ s := &Server{windows: map[ports.WindowID]*window{}}
254+ if n := testing.AllocsPerRun(100, s.syncKeyboardGrab); n != 0 {
255+ t.Fatalf("%v allocs", n)
256+ }
257+}
@@ -246,3 +246,12 @@ func TestCommandForwardAllocations(t *testing.T) {
246 t.Errorf("command push + merge + drain + stats: %.1f allocs, want 0", allocs)246 t.Errorf("command push + merge + drain + stats: %.1f allocs, want 0", allocs)
247 }247 }
248 }248 }
249+
250+// A settled grab (none, or unchanged) is re-checked at every focus and
251+// configure without allocating.
252+func TestKeyboardGrabSyncAllocations(t *testing.T) {
253+ s := &Server{windows: map[ports.WindowID]*window{}}
254+ if n := testing.AllocsPerRun(100, s.syncKeyboardGrab); n != 0 {
255+ t.Fatalf("%v allocs", n)
256+ }
257+}
M internal/adapters/wayland/globals.go +1 -0
@@ -14,6 +14,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
1414 for _, register := range []func() error{
1515 func() error { return registerXDG(d, s) },
1616 func() error { return registerXwaylandShell(d, s) },
17+ func() error { return registerXwaylandGrab(d, s) },
1718 func() error { return registerLayer(d, s) },
1819 func() error {
1920 // Advertise only with the production owner gate and authoritative
@@ -14,6 +14,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
14 for _, register := range []func() error{14 for _, register := range []func() error{
15 func() error { return registerXDG(d, s) },15 func() error { return registerXDG(d, s) },
16 func() error { return registerXwaylandShell(d, s) },16 func() error { return registerXwaylandShell(d, s) },
17+ func() error { return registerXwaylandGrab(d, s) },
17 func() error { return registerLayer(d, s) },18 func() error { return registerLayer(d, s) },
18 func() error {19 func() error {
19 // Advertise only with the production owner gate and authoritative20 // Advertise only with the production owner gate and authoritative
M internal/adapters/wayland/inhibit.go +4 -0
@@ -121,6 +121,10 @@ func (s *Server) syncInhibitors() {
121121 idle[in.window] = true
122122 }
123123 }
124+ if id := s.xgrabWindow(); id != 0 {
125+ shortcuts[id] = true
126+ }
127+ s.xgrabbed = s.xgrabWindow()
124128 for id, on := range diffSets(s.shortcutWindows, shortcuts) {
125129 s.emit(ports.ShortcutsInhibit{Window: id, Active: on})
126130 }
@@ -121,6 +121,10 @@ func (s *Server) syncInhibitors() {
121 idle[in.window] = true121 idle[in.window] = true
122 }122 }
123 }123 }
124+ if id := s.xgrabWindow(); id != 0 {
125+ shortcuts[id] = true
126+ }
127+ s.xgrabbed = s.xgrabWindow()
124 for id, on := range diffSets(s.shortcutWindows, shortcuts) {128 for id, on := range diffSets(s.shortcutWindows, shortcuts) {
125 s.emit(ports.ShortcutsInhibit{Window: id, Active: on})129 s.emit(ports.ShortcutsInhibit{Window: id, Active: on})
126 }130 }
M internal/adapters/wayland/seat.go +1 -0
@@ -456,6 +456,7 @@ func (s *Server) changeFocus(id ports.WindowID) {
456456 }
457457 s.textInputFocus()
458458 s.x11Focus()
459+ s.syncKeyboardGrab()
459460 s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus")
460461 s.updateConstraint()
461462 }
@@ -456,6 +456,7 @@ func (s *Server) changeFocus(id ports.WindowID) {
456 }456 }
457 s.textInputFocus()457 s.textInputFocus()
458 s.x11Focus()458 s.x11Focus()
459+ s.syncKeyboardGrab()
459 s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus")460 s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus")
460 s.updateConstraint()461 s.updateConstraint()
461 }462 }
M internal/adapters/wayland/server.go +3 -0
@@ -274,6 +274,8 @@ type Server struct {
274274 // inhibitors are the live inhibitor objects (inhibit.go);
275275 // shortcutWindows and idleWindows what core was told.
276276 inhibitors []*inhibitor
277+ xgrab *xwaylandGrab // Xwayland keyboard grab (xwaylandgrab.go)
278+ xgrabbed ports.WindowID // window the grab inhibits for
277279 shortcutWindows, idleWindows map[ports.WindowID]bool
278280 // busInhibited is the last IdleInhibited value (idle.go).
279281 busInhibited bool
@@ -816,6 +818,7 @@ func (s *Server) apply(cmd ports.ClientCommand) {
816818 s.wakePacer() // reconsider FIFO barriers at the new period
817819 }
818820 w.sendConfigure()
821+ s.syncKeyboardGrab()
819822 s.toplevelChanged(w)
820823 if scanoutChanged && w.surf() != nil {
821824 s.dmabuf.resendSurface(w.surf())
@@ -274,6 +274,8 @@ type Server struct {
274 // inhibitors are the live inhibitor objects (inhibit.go);274 // inhibitors are the live inhibitor objects (inhibit.go);
275 // shortcutWindows and idleWindows what core was told.275 // shortcutWindows and idleWindows what core was told.
276 inhibitors []*inhibitor276 inhibitors []*inhibitor
277+ xgrab *xwaylandGrab // Xwayland keyboard grab (xwaylandgrab.go)
278+ xgrabbed ports.WindowID // window the grab inhibits for
277 shortcutWindows, idleWindows map[ports.WindowID]bool279 shortcutWindows, idleWindows map[ports.WindowID]bool
278 // busInhibited is the last IdleInhibited value (idle.go).280 // busInhibited is the last IdleInhibited value (idle.go).
279 busInhibited bool281 busInhibited bool
@@ -816,6 +818,7 @@ func (s *Server) apply(cmd ports.ClientCommand) {
816 s.wakePacer() // reconsider FIFO barriers at the new period818 s.wakePacer() // reconsider FIFO barriers at the new period
817 }819 }
818 w.sendConfigure()820 w.sendConfigure()
821+ s.syncKeyboardGrab()
819 s.toplevelChanged(w)822 s.toplevelChanged(w)
820 if scanoutChanged && w.surf() != nil {823 if scanoutChanged && w.surf() != nil {
821 s.dmabuf.resendSurface(w.surf())824 s.dmabuf.resendSurface(w.surf())
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_xwaylandkeyboardgrab "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab"
5859 wire_xwaylandshell "github.com/bnema/go-wayland-bindings/server/xwaylandshell"
5960 wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture"
6061 "github.com/bnema/purego-libwayland/server"
@@ -254,6 +255,8 @@ var wireSchemas = map[string]wireSchema{
254255 "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil},
255256 "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil},
256257 "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil},
258+ "zwp_xwayland_keyboard_grab_manager_v1": {wire_xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1Interface, nil, map[uint16]bool{0: true}, nil},
259+ "zwp_xwayland_keyboard_grab_v1": {wire_xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1Interface, nil, map[uint16]bool{0: true}, nil},
257260 "xwayland_shell_v1": {wire_xwaylandshell.XwaylandShellV1Interface, nil, map[uint16]bool{0: true}, nil},
258261 "xwayland_surface_v1": {wire_xwaylandshell.XwaylandSurfaceV1Interface, nil, map[uint16]bool{1: true}, nil},
259262 "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil},
@@ -55,6 +55,7 @@ import (
55 wire_xdgforeign "github.com/bnema/go-wayland-bindings/server/xdgforeign"55 wire_xdgforeign "github.com/bnema/go-wayland-bindings/server/xdgforeign"
56 wire_xdgoutput "github.com/bnema/go-wayland-bindings/server/xdgoutput"56 wire_xdgoutput "github.com/bnema/go-wayland-bindings/server/xdgoutput"
57 wire_xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell"57 wire_xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell"
58+ wire_xwaylandkeyboardgrab "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab"
58 wire_xwaylandshell "github.com/bnema/go-wayland-bindings/server/xwaylandshell"59 wire_xwaylandshell "github.com/bnema/go-wayland-bindings/server/xwaylandshell"
59 wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture"60 wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture"
60 "github.com/bnema/purego-libwayland/server"61 "github.com/bnema/purego-libwayland/server"
@@ -254,6 +255,8 @@ var wireSchemas = map[string]wireSchema{
254 "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil},255 "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil},
255 "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil},256 "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil},
256 "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil},257 "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil},
258+ "zwp_xwayland_keyboard_grab_manager_v1": {wire_xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1Interface, nil, map[uint16]bool{0: true}, nil},
259+ "zwp_xwayland_keyboard_grab_v1": {wire_xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1Interface, nil, map[uint16]bool{0: true}, nil},
257 "xwayland_shell_v1": {wire_xwaylandshell.XwaylandShellV1Interface, nil, map[uint16]bool{0: true}, nil},260 "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},261 "xwayland_surface_v1": {wire_xwaylandshell.XwaylandSurfaceV1Interface, nil, map[uint16]bool{1: true}, nil},
259 "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil},262 "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil},
M internal/adapters/wayland/x11window_test.go +63 -0
@@ -11,6 +11,7 @@ import (
1111 "github.com/bnema/go-wayland-bindings/server/pointerconstraints"
1212 "github.com/bnema/go-wayland-bindings/server/wayland"
1313 "github.com/bnema/go-wayland-bindings/server/xdgshell"
14+ "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab"
1415 "github.com/bnema/go-wayland-bindings/server/xwaylandshell"
1516 "github.com/bnema/neferwl/internal/ports"
1617 "github.com/bnema/wlturbo"
@@ -351,3 +352,65 @@ func TestX11WindowPointerLock(t *testing.T) {
351352 t.Fatalf("constrained %+v", got)
352353 }
353354 }
355+
356+// grabRig is an X11 window with an Xwayland keyboard grab on its surface.
357+func grabRig(t *testing.T) (r *x11Rig, w ports.WindowID, surf uint32) {
358+ t.Helper()
359+ r = newX11Rig(t)
360+ r.x11Window(x11Win)
361+ surf = r.pair(x11Win, x11Serial)
362+ w = awaitEvent[ports.WindowMapped](t, r.events).ID
363+ seat := bindProtocol(t, r.xc, "wl_seat")
364+ registerProtocol(t, r.xc, seat)
365+ mgr := bindProtocol(t, r.xc, "zwp_xwayland_keyboard_grab_manager_v1")
366+ grab := r.xc.AllocateID()
367+ requestProtocol(t, r.xc, mgr, xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1RequestGrabKeyboard, grab, surf, seat)
368+ registerProtocol(t, r.xc, grab)
369+ if err := r.xc.Roundtrip(); err != nil {
370+ t.Fatal(err)
371+ }
372+ return r, w, grab
373+}
374+
375+// noInhibit fails when a ShortcutsInhibit event arrives shortly.
376+func noInhibit(t *testing.T, r *x11Rig) {
377+ t.Helper()
378+ if err := r.xc.Roundtrip(); err != nil {
379+ t.Fatal(err)
380+ }
381+ for deadline := time.After(150 * time.Millisecond); ; {
382+ select {
383+ case ev := <-r.events:
384+ if v, ok := ev.(ports.ShortcutsInhibit); ok {
385+ t.Fatalf("unexpected %+v", v)
386+ }
387+ case <-deadline:
388+ return
389+ }
390+ }
391+}
392+
393+func TestXwaylandKeyboardGrabThreeStates(t *testing.T) {
394+ r, w, grab := grabRig(t)
395+ // Windowed and focused: accepted, no effect.
396+ r.commands <- ports.ConfigureWindow{ID: w, Width: 100, Height: 100, Visible: true}
397+ r.commands <- ports.FocusWindow{ID: w}
398+ noInhibit(t, r)
399+ // Focused and fullscreen: inhibits.
400+ r.commands <- ports.ConfigureWindow{ID: w, Width: 100, Height: 100, Visible: true, Fullscreen: true}
401+ if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w, Active: true}) {
402+ t.Fatal(v)
403+ }
404+ // Focus lost: released.
405+ r.commands <- ports.FocusWindow{}
406+ if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w}) {
407+ t.Fatal(v)
408+ }
409+ // Back, then the grab object goes away.
410+ r.commands <- ports.FocusWindow{ID: w}
411+ awaitEvent[ports.ShortcutsInhibit](t, r.events)
412+ requestProtocol(t, r.xc, grab, xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1RequestDestroy)
413+ if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w}) {
414+ t.Fatal(v)
415+ }
416+}
@@ -11,6 +11,7 @@ import (
11 "github.com/bnema/go-wayland-bindings/server/pointerconstraints"11 "github.com/bnema/go-wayland-bindings/server/pointerconstraints"
12 "github.com/bnema/go-wayland-bindings/server/wayland"12 "github.com/bnema/go-wayland-bindings/server/wayland"
13 "github.com/bnema/go-wayland-bindings/server/xdgshell"13 "github.com/bnema/go-wayland-bindings/server/xdgshell"
14+ "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab"
14 "github.com/bnema/go-wayland-bindings/server/xwaylandshell"15 "github.com/bnema/go-wayland-bindings/server/xwaylandshell"
15 "github.com/bnema/neferwl/internal/ports"16 "github.com/bnema/neferwl/internal/ports"
16 "github.com/bnema/wlturbo"17 "github.com/bnema/wlturbo"
@@ -351,3 +352,65 @@ func TestX11WindowPointerLock(t *testing.T) {
351 t.Fatalf("constrained %+v", got)352 t.Fatalf("constrained %+v", got)
352 }353 }
353 }354 }
355+
356+// grabRig is an X11 window with an Xwayland keyboard grab on its surface.
357+func grabRig(t *testing.T) (r *x11Rig, w ports.WindowID, surf uint32) {
358+ t.Helper()
359+ r = newX11Rig(t)
360+ r.x11Window(x11Win)
361+ surf = r.pair(x11Win, x11Serial)
362+ w = awaitEvent[ports.WindowMapped](t, r.events).ID
363+ seat := bindProtocol(t, r.xc, "wl_seat")
364+ registerProtocol(t, r.xc, seat)
365+ mgr := bindProtocol(t, r.xc, "zwp_xwayland_keyboard_grab_manager_v1")
366+ grab := r.xc.AllocateID()
367+ requestProtocol(t, r.xc, mgr, xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1RequestGrabKeyboard, grab, surf, seat)
368+ registerProtocol(t, r.xc, grab)
369+ if err := r.xc.Roundtrip(); err != nil {
370+ t.Fatal(err)
371+ }
372+ return r, w, grab
373+}
374+
375+// noInhibit fails when a ShortcutsInhibit event arrives shortly.
376+func noInhibit(t *testing.T, r *x11Rig) {
377+ t.Helper()
378+ if err := r.xc.Roundtrip(); err != nil {
379+ t.Fatal(err)
380+ }
381+ for deadline := time.After(150 * time.Millisecond); ; {
382+ select {
383+ case ev := <-r.events:
384+ if v, ok := ev.(ports.ShortcutsInhibit); ok {
385+ t.Fatalf("unexpected %+v", v)
386+ }
387+ case <-deadline:
388+ return
389+ }
390+ }
391+}
392+
393+func TestXwaylandKeyboardGrabThreeStates(t *testing.T) {
394+ r, w, grab := grabRig(t)
395+ // Windowed and focused: accepted, no effect.
396+ r.commands <- ports.ConfigureWindow{ID: w, Width: 100, Height: 100, Visible: true}
397+ r.commands <- ports.FocusWindow{ID: w}
398+ noInhibit(t, r)
399+ // Focused and fullscreen: inhibits.
400+ r.commands <- ports.ConfigureWindow{ID: w, Width: 100, Height: 100, Visible: true, Fullscreen: true}
401+ if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w, Active: true}) {
402+ t.Fatal(v)
403+ }
404+ // Focus lost: released.
405+ r.commands <- ports.FocusWindow{}
406+ if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w}) {
407+ t.Fatal(v)
408+ }
409+ // Back, then the grab object goes away.
410+ r.commands <- ports.FocusWindow{ID: w}
411+ awaitEvent[ports.ShortcutsInhibit](t, r.events)
412+ requestProtocol(t, r.xc, grab, xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1RequestDestroy)
413+ if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w}) {
414+ t.Fatal(v)
415+ }
416+}
A internal/adapters/wayland/xwaylandgrab.go +90 -0
@@ -0,0 +1,90 @@
1+package wayland
2+
3+import (
4+ "github.com/bnema/go-wayland-bindings/server/wayland"
5+ "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab"
6+ "github.com/bnema/neferwl/internal/ports"
7+ "github.com/bnema/purego-libwayland/server"
8+)
9+
10+// zwp_xwayland_keyboard_grab_v1. Xwayland asks for it when an X client
11+// takes an active keyboard grab. It is a shortcuts inhibitor, but only
12+// while the grabbing surface's window has the keyboard focus and is
13+// fullscreen: a windowed app must not lock the compositor binds out.
14+// Otherwise the request is accepted and has no effect. Never honoured
15+// while the session is protected. The global is visible to Xwayland only
16+// (xwaylandRestricted).
17+
18+// xwaylandGrab is the seat's single grab.
19+type xwaylandGrab struct {
20+ res *xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1
21+ surf *surface
22+}
23+
24+func registerXwaylandGrab(d *server.Display, s *Server) error {
25+ return xwaylandkeyboardgrab.NewZwpXwaylandKeyboardGrabManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
26+ _, _ = xwaylandkeyboardgrab.NewZwpXwaylandKeyboardGrabManagerV1(c, int32(v), id, xwaylandGrabManager{s})
27+ })
28+}
29+
30+type xwaylandGrabManager struct{ s *Server }
31+
32+func (xwaylandGrabManager) Destroy(*xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1) {}
33+
34+// GrabKeyboard replaces the seat's grab with a new one.
35+func (m xwaylandGrabManager) GrabKeyboard(r *xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1, id uint32, surf *wayland.Surface, _ *wayland.Seat) {
36+ state := m.s.surfaceOf(surf)
37+ if state == nil {
38+ return
39+ }
40+ g := &xwaylandGrab{surf: state}
41+ res, err := xwaylandkeyboardgrab.NewZwpXwaylandKeyboardGrabV1(r.Client(), r.Version(), id, xwaylandGrabHandler{})
42+ if err != nil {
43+ return
44+ }
45+ g.res = res
46+ old := m.s.xgrab
47+ m.s.xgrab = g
48+ res.OnDestroy = func() {
49+ if m.s.xgrab == g {
50+ m.s.xgrab = nil
51+ m.s.syncKeyboardGrab()
52+ }
53+ }
54+ if old != nil && old.res != nil && old.res.Resource.Alive() {
55+ old.res.Resource.Destroy()
56+ }
57+ m.s.syncKeyboardGrab()
58+}
59+
60+type xwaylandGrabHandler struct{}
61+
62+func (xwaylandGrabHandler) Destroy(*xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1) {}
63+
64+// xgrabWindow is the window whose grab is in effect (0: none).
65+func (s *Server) xgrabWindow() ports.WindowID {
66+ g := s.xgrab
67+ if g == nil || g.surf == nil || g.surf.destroyed || !g.res.Resource.Alive() {
68+ return 0
69+ }
70+ if s.security != nil && s.security.Snapshot().Protected {
71+ return 0
72+ }
73+ id := g.surf.root().windowID()
74+ if id == 0 || id != s.seat.focused {
75+ return 0
76+ }
77+ if w := s.windows[id]; w == nil || !w.hasLast || !w.last.Fullscreen {
78+ return 0
79+ }
80+ return id
81+}
82+
83+// syncKeyboardGrab re-evaluates the grab after a focus or fullscreen
84+// change, and reports to core only when its effect changed.
85+func (s *Server) syncKeyboardGrab() {
86+ if id := s.xgrabWindow(); id != s.xgrabbed {
87+ s.xgrabbed = id
88+ s.syncInhibitors()
89+ }
90+}
@@ -0,0 +1,90 @@
1+package wayland
2+
3+import (
4+ "github.com/bnema/go-wayland-bindings/server/wayland"
5+ "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab"
6+ "github.com/bnema/neferwl/internal/ports"
7+ "github.com/bnema/purego-libwayland/server"
8+)
9+
10+// zwp_xwayland_keyboard_grab_v1. Xwayland asks for it when an X client
11+// takes an active keyboard grab. It is a shortcuts inhibitor, but only
12+// while the grabbing surface's window has the keyboard focus and is
13+// fullscreen: a windowed app must not lock the compositor binds out.
14+// Otherwise the request is accepted and has no effect. Never honoured
15+// while the session is protected. The global is visible to Xwayland only
16+// (xwaylandRestricted).
17+
18+// xwaylandGrab is the seat's single grab.
19+type xwaylandGrab struct {
20+ res *xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1
21+ surf *surface
22+}
23+
24+func registerXwaylandGrab(d *server.Display, s *Server) error {
25+ return xwaylandkeyboardgrab.NewZwpXwaylandKeyboardGrabManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
26+ _, _ = xwaylandkeyboardgrab.NewZwpXwaylandKeyboardGrabManagerV1(c, int32(v), id, xwaylandGrabManager{s})
27+ })
28+}
29+
30+type xwaylandGrabManager struct{ s *Server }
31+
32+func (xwaylandGrabManager) Destroy(*xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1) {}
33+
34+// GrabKeyboard replaces the seat's grab with a new one.
35+func (m xwaylandGrabManager) GrabKeyboard(r *xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1, id uint32, surf *wayland.Surface, _ *wayland.Seat) {
36+ state := m.s.surfaceOf(surf)
37+ if state == nil {
38+ return
39+ }
40+ g := &xwaylandGrab{surf: state}
41+ res, err := xwaylandkeyboardgrab.NewZwpXwaylandKeyboardGrabV1(r.Client(), r.Version(), id, xwaylandGrabHandler{})
42+ if err != nil {
43+ return
44+ }
45+ g.res = res
46+ old := m.s.xgrab
47+ m.s.xgrab = g
48+ res.OnDestroy = func() {
49+ if m.s.xgrab == g {
50+ m.s.xgrab = nil
51+ m.s.syncKeyboardGrab()
52+ }
53+ }
54+ if old != nil && old.res != nil && old.res.Resource.Alive() {
55+ old.res.Resource.Destroy()
56+ }
57+ m.s.syncKeyboardGrab()
58+}
59+
60+type xwaylandGrabHandler struct{}
61+
62+func (xwaylandGrabHandler) Destroy(*xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1) {}
63+
64+// xgrabWindow is the window whose grab is in effect (0: none).
65+func (s *Server) xgrabWindow() ports.WindowID {
66+ g := s.xgrab
67+ if g == nil || g.surf == nil || g.surf.destroyed || !g.res.Resource.Alive() {
68+ return 0
69+ }
70+ if s.security != nil && s.security.Snapshot().Protected {
71+ return 0
72+ }
73+ id := g.surf.root().windowID()
74+ if id == 0 || id != s.seat.focused {
75+ return 0
76+ }
77+ if w := s.windows[id]; w == nil || !w.hasLast || !w.last.Fullscreen {
78+ return 0
79+ }
80+ return id
81+}
82+
83+// syncKeyboardGrab re-evaluates the grab after a focus or fullscreen
84+// change, and reports to core only when its effect changed.
85+func (s *Server) syncKeyboardGrab() {
86+ if id := s.xgrabWindow(); id != s.xgrabbed {
87+ s.xgrabbed = id
88+ s.syncInhibitors()
89+ }
90+}

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 26

Tags 10

↑↓ moveenter openesc close