detached 83719a6

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

commit
83719a682d03af2f58e206918a9d3d9ac4c86d19
author
brice · · 2026-10-22 15:14 UTC
committer
brice · · 2026-10-22 17:54 UTC
merge of
7ef926f + bf24250 · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +278 -5
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)$$' -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)$$' -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)$$' -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)$$' -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/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
@@ -70,6 +70,14 @@ func (c *Core) clientEvent(ctx context.Context, ev ports.ClientEvent) (publish,
70 if _, w := c.screenOf(v.ID); w != nil {70 if _, w := c.screenOf(v.ID); w != nil {
71 w.SetModal(v.ID, v.Modal)71 w.SetModal(v.ID, v.Modal)
72 }72 }
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+ }
73 case ports.WindowUnmapped:81 case ports.WindowUnmapped:
74 if c.unmapClient(ctx, v) != nil {82 if c.unmapClient(ctx, v) != nil {
75 return false, true83 return false, true
M internal/core/core.go +11 -2
@@ -146,7 +146,11 @@ type Core struct {
146146 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.
147147 popups map[WindowID]*popupState
148148 popupOrder []WindowID
149- 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
150154 // activity is when core last told wayland about user input
151155 // (ports.UserActivity), sent at most once per ActivityInterval.
152156 activity time.Time
@@ -419,7 +423,7 @@ func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) {
419423 return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")
420424 }
421425 // A placeholder screen holds windows until the first output arrives.
422- 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(), placed: map[WindowID]Rect{}, 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()}
423427 c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}
424428 if err := c.apply(cfg); err != nil {
425429 return nil, err
@@ -996,6 +1000,11 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
9961000 }
9971001 sc := c.screens[i]
9981002 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+ }
9991008 // Layer popups are over everything; window popups are over the windows
10001009 // only, under the top and overlay layers (as drawn).
10011010 if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
@@ -146,7 +146,11 @@ type Core struct {
146 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.146 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.
147 popups map[WindowID]*popupState147 popups map[WindowID]*popupState
148 popupOrder []WindowID148 popupOrder []WindowID
149- keyboard keyboard149+ // 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
150 // activity is when core last told wayland about user input154 // activity is when core last told wayland about user input
151 // (ports.UserActivity), sent at most once per ActivityInterval.155 // (ports.UserActivity), sent at most once per ActivityInterval.
152 activity time.Time156 activity time.Time
@@ -419,7 +423,7 @@ func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) {
419 return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")423 return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")
420 }424 }
421 // A placeholder screen holds windows until the first output arrives.425 // A placeholder screen holds windows until the first output arrives.
422- 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(), placed: map[WindowID]Rect{}, 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()}
423 c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}427 c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}
424 if err := c.apply(cfg); err != nil {428 if err := c.apply(cfg); err != nil {
425 return nil, err429 return nil, err
@@ -996,6 +1000,11 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
996 }1000 }
997 sc := c.screens[i]1001 sc := c.screens[i]
998 lx, ly := x-float64(sc.x), y-float64(sc.y)1002 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+ }
999 // Layer popups are over everything; window popups are over the windows1008 // Layer popups are over everything; window popups are over the windows
1000 // only, under the top and overlay layers (as drawn).1009 // only, under the top and overlay layers (as drawn).
1001 if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {1010 if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
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:
@@ -101,7 +101,7 @@ func dataDragHitChanged(ev ports.ClientEvent) bool {
101 case ports.LayerChanged, ports.InputRegionChanged, ports.WindowMapped,101 case ports.LayerChanged, ports.InputRegionChanged, ports.WindowMapped,
102 ports.WindowResized, ports.PopupRequest, ports.PopupMapped,102 ports.WindowResized, ports.PopupRequest, ports.PopupMapped,
103 ports.OutputPower, ports.WindowParent, ports.WindowModal,103 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,
105 ports.WindowFullscreenRequest, ports.WorkspaceActivate, ports.WindowActivate:105 ports.WindowFullscreenRequest, ports.WorkspaceActivate, ports.WindowActivate:
106 return true106 return true
107 default:107 default:
M internal/core/scene.go +2 -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.
@@ -128,6 +128,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
128128 }
129129 }
130130 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
131+ scene.Windows = c.sceneUnmanaged(scene.Windows, sc)
131132 if icon, ok := c.dataDragIconOn(i, sc); ok {
132133 // The drag icon is over everything, popups and layers included.
133134 scene.Windows = append(scene.Windows, icon)
@@ -20,7 +20,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
20 // layout is what is drawn; settled (same indexes) is where the20 // layout is what is drawn; settled (same indexes) is where the
21 // windows are going, and alone sizes the configures.21 // windows are going, and alone sizes the configures.
22 layout, settled := sc.shown, sc.settledLayout22 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)}
24 if !sc.mon.previewing() {24 if !sc.mon.previewing() {
25 // Tiles stop at the panels; the overview lays out previews over25 // Tiles stop at the panels; the overview lays out previews over
26 // the whole output.26 // the whole output.
@@ -128,6 +128,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
128 }128 }
129 }129 }
130 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)130 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
131+ scene.Windows = c.sceneUnmanaged(scene.Windows, sc)
131 if icon, ok := c.dataDragIconOn(i, sc); ok {132 if icon, ok := c.dataDragIconOn(i, sc); ok {
132 // The drag icon is over everything, popups and layers included.133 // The drag icon is over everything, popups and layers included.
133 scene.Windows = append(scene.Windows, icon)134 scene.Windows = append(scene.Windows, icon)
A internal/core/unmanaged.go +106 -0
@@ -0,0 +1,106 @@
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+ }
52+ // The window under a still pointer may have changed.
53+ return true, c.rehit(ctx)
54+}
55+
56+// unmanagedScreen is the screen showing u: the one containing its origin, or
57+// the one the pointer clamp would pick when the origin is in a hole.
58+func (c *Core) unmanagedScreen(u *unmanagedState) *screen {
59+ x, y := float64(u.rect.X), float64(u.rect.Y)
60+ i := c.screenAt(x, y)
61+ if i < 0 {
62+ cx, cy := c.clampPointer(x, y, x, y)
63+ i = c.screenAt(cx, cy)
64+ }
65+ if i < 0 {
66+ return nil
67+ }
68+ return c.screens[i]
69+}
70+
71+// sceneUnmanaged appends the unmanaged windows shown on sc, bottom to top,
72+// drawn as popups over the layers. They are hidden while sc previews.
73+func (c *Core) sceneUnmanaged(out []ports.SceneWindow, sc *screen) []ports.SceneWindow {
74+ if len(c.unmanagedOrder) == 0 || sc.mon.previewing() {
75+ return out
76+ }
77+ for _, id := range c.unmanagedOrder {
78+ u := c.unmanaged[id]
79+ if u == nil || c.unmanagedScreen(u) != sc {
80+ continue
81+ }
82+ r := u.rect
83+ r.X, r.Y = r.X-sc.x, r.Y-sc.y
84+ out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true, OverLayers: true})
85+ }
86+ return out
87+}
88+
89+// unmanagedAt is the topmost unmanaged window taking the pointer at the
90+// screen-local point. None while the pointer is locked or the screen previews.
91+func (c *Core) unmanagedAt(sc *screen, lx, ly float64) (WindowID, float64, float64) {
92+ if len(c.unmanagedOrder) == 0 || c.constraint.Mode == ports.ConstraintLock || sc.mon.previewing() {
93+ return 0, 0, 0
94+ }
95+ for i := len(c.unmanagedOrder) - 1; i >= 0; i-- {
96+ u := c.unmanaged[c.unmanagedOrder[i]]
97+ if u == nil || u.noInput || c.unmanagedScreen(u) != sc {
98+ continue
99+ }
100+ x, y := lx-float64(u.rect.X-sc.x), ly-float64(u.rect.Y-sc.y)
101+ if x >= 0 && x < float64(u.rect.W) && y >= 0 && y < float64(u.rect.H) && c.acceptsInput(u.id, x, y) {
102+ return u.id, x, y
103+ }
104+ }
105+ return 0, 0, 0
106+}
@@ -0,0 +1,106 @@
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+ }
52+ // The window under a still pointer may have changed.
53+ return true, c.rehit(ctx)
54+}
55+
56+// unmanagedScreen is the screen showing u: the one containing its origin, or
57+// the one the pointer clamp would pick when the origin is in a hole.
58+func (c *Core) unmanagedScreen(u *unmanagedState) *screen {
59+ x, y := float64(u.rect.X), float64(u.rect.Y)
60+ i := c.screenAt(x, y)
61+ if i < 0 {
62+ cx, cy := c.clampPointer(x, y, x, y)
63+ i = c.screenAt(cx, cy)
64+ }
65+ if i < 0 {
66+ return nil
67+ }
68+ return c.screens[i]
69+}
70+
71+// sceneUnmanaged appends the unmanaged windows shown on sc, bottom to top,
72+// drawn as popups over the layers. They are hidden while sc previews.
73+func (c *Core) sceneUnmanaged(out []ports.SceneWindow, sc *screen) []ports.SceneWindow {
74+ if len(c.unmanagedOrder) == 0 || sc.mon.previewing() {
75+ return out
76+ }
77+ for _, id := range c.unmanagedOrder {
78+ u := c.unmanaged[id]
79+ if u == nil || c.unmanagedScreen(u) != sc {
80+ continue
81+ }
82+ r := u.rect
83+ r.X, r.Y = r.X-sc.x, r.Y-sc.y
84+ out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true, OverLayers: true})
85+ }
86+ return out
87+}
88+
89+// unmanagedAt is the topmost unmanaged window taking the pointer at the
90+// screen-local point. None while the pointer is locked or the screen previews.
91+func (c *Core) unmanagedAt(sc *screen, lx, ly float64) (WindowID, float64, float64) {
92+ if len(c.unmanagedOrder) == 0 || c.constraint.Mode == ports.ConstraintLock || sc.mon.previewing() {
93+ return 0, 0, 0
94+ }
95+ for i := len(c.unmanagedOrder) - 1; i >= 0; i-- {
96+ u := c.unmanaged[c.unmanagedOrder[i]]
97+ if u == nil || u.noInput || c.unmanagedScreen(u) != sc {
98+ continue
99+ }
100+ x, y := lx-float64(u.rect.X-sc.x), ly-float64(u.rect.Y-sc.y)
101+ if x >= 0 && x < float64(u.rect.W) && y >= 0 && y < float64(u.rect.H) && c.acceptsInput(u.id, x, y) {
102+ return u.id, x, y
103+ }
104+ }
105+ return 0, 0, 0
106+}
A internal/core/unmanaged_test.go +124 -0
@@ -0,0 +1,124 @@
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+}
@@ -0,0 +1,124 @@
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+}
M internal/ports/client_events.go +25 -0
@@ -52,6 +52,31 @@ type WindowUnmapped struct{ ID WindowID }
5252
5353 func (WindowUnmapped) clientEvent() {}
5454
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+
5580 // ShortcutsInhibit carries wayland → core that a window asks for (Active)
5681 // or stops asking for all keys, compositor binds included, while it has
5782 // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1).
@@ -52,6 +52,31 @@ type WindowUnmapped struct{ ID WindowID }
52 52
53 func (WindowUnmapped) clientEvent() {}53 func (WindowUnmapped) clientEvent() {}
54 54
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+
55 // ShortcutsInhibit carries wayland → core that a window asks for (Active)80 // ShortcutsInhibit carries wayland → core that a window asks for (Active)
56 // or stops asking for all keys, compositor binds included, while it has81 // or stops asking for all keys, compositor binds included, while it has
57 // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1).82 // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1).

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