fix(core): keep rule workspaces ahead of the X11 output hint and read the keyboard grab once per sync
- commit
- 1b0bfc7be7ac404481a59bb6f9f28abfd2205323
- author
- brice · · 2026-10-22 15:39 UTC
- committer
- brice · · 2026-10-22 17:54 UTC
- parent
- 56a0abd
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +15 -4
M internal/adapters/wayland/inhibit.go +4 -3
| @@ -121,10 +121,11 @@ func (s *Server) syncInhibitors() { | |||
| 121 | 121 | idle[in.window] = true | |
| 122 | 122 | } | |
| 123 | 123 | } | |
| 124 | - | if id := s.xgrabWindow(); id != 0 { | |
| 125 | - | shortcuts[id] = true | |
| 126 | - | } | |
| 124 | + | // syncKeyboardGrab compares against xgrabbed: it is only written here. | |
| 127 | 125 | s.xgrabbed = s.xgrabWindow() | |
| 126 | + | if s.xgrabbed != 0 { | |
| 127 | + | shortcuts[s.xgrabbed] = true | |
| 128 | + | } | |
| 128 | 129 | for id, on := range diffSets(s.shortcutWindows, shortcuts) { | |
| 129 | 130 | s.emit(ports.ShortcutsInhibit{Window: id, Active: on}) | |
| 130 | 131 | } | |
| @@ -121,10 +121,11 @@ func (s *Server) syncInhibitors() { | |||||
| 121 | idle[in.window] = true | 121 | idle[in.window] = true | ||
| 122 | } | 122 | } | ||
| 123 | } | 123 | } | ||
| 124 | - | if id := s.xgrabWindow(); id != 0 { | 124 | + | // syncKeyboardGrab compares against xgrabbed: it is only written here. |
| 125 | - | shortcuts[id] = true | |||
| 126 | - | } | |||
| 127 | s.xgrabbed = s.xgrabWindow() | 125 | s.xgrabbed = s.xgrabWindow() | ||
| 126 | + | if s.xgrabbed != 0 { | |||
| 127 | + | shortcuts[s.xgrabbed] = true | |||
| 128 | + | } | |||
| 128 | for id, on := range diffSets(s.shortcutWindows, shortcuts) { | 129 | for id, on := range diffSets(s.shortcutWindows, shortcuts) { | ||
| 129 | s.emit(ports.ShortcutsInhibit{Window: id, Active: on}) | 130 | s.emit(ports.ShortcutsInhibit{Window: id, Active: on}) | ||
| 130 | } | 131 | } | ||
M internal/core/rules_test.go +10 -0
| @@ -432,6 +432,16 @@ func TestMapAtOutputKeepsRuleMonitor(t *testing.T) { | |||
| 432 | 432 | } | |
| 433 | 433 | } | |
| 434 | 434 | ||
| 435 | + | func TestMapAtOutputKeepsRuleWorkspace(t *testing.T) { | |
| 436 | + | r := startMulti(t, func(c *ports.Config) { | |
| 437 | + | c.Rules = []ports.WindowRule{winRule("g", "game", func(r *ports.WindowRule) { r.Workspace = "1" })} | |
| 438 | + | }, left, right) | |
| 439 | + | r.client <- ports.WindowMapped{ID: 2, AppID: "game", HasAt: true, AtX: 200, AtY: 0} | |
| 440 | + | if w := stWindow(r.stateWith(t, 2), 2); w.Output != "DP-1" { | |
| 441 | + | t.Fatalf("rule workspace lost to the hint: %+v", w) | |
| 442 | + | } | |
| 443 | + | } | |
| 444 | + | ||
| 435 | 445 | func TestRuleMatchesSteamAppClass(t *testing.T) { | |
| 436 | 446 | r := startMulti(t, func(c *ports.Config) { | |
| 437 | 447 | c.Rules = []ports.WindowRule{winRule("steam", `steam_app_\d+`, func(r *ports.WindowRule) { r.Floating = ruleOn() })} | |
| @@ -432,6 +432,16 @@ func TestMapAtOutputKeepsRuleMonitor(t *testing.T) { | |||||
| 432 | } | 432 | } | ||
| 433 | } | 433 | } | ||
| 434 | 434 | ||||
| 435 | + | func TestMapAtOutputKeepsRuleWorkspace(t *testing.T) { | |||
| 436 | + | r := startMulti(t, func(c *ports.Config) { | |||
| 437 | + | c.Rules = []ports.WindowRule{winRule("g", "game", func(r *ports.WindowRule) { r.Workspace = "1" })} | |||
| 438 | + | }, left, right) | |||
| 439 | + | r.client <- ports.WindowMapped{ID: 2, AppID: "game", HasAt: true, AtX: 200, AtY: 0} | |||
| 440 | + | if w := stWindow(r.stateWith(t, 2), 2); w.Output != "DP-1" { | |||
| 441 | + | t.Fatalf("rule workspace lost to the hint: %+v", w) | |||
| 442 | + | } | |||
| 443 | + | } | |||
| 444 | + | ||||
| 435 | func TestRuleMatchesSteamAppClass(t *testing.T) { | 445 | func TestRuleMatchesSteamAppClass(t *testing.T) { | ||
| 436 | r := startMulti(t, func(c *ports.Config) { | 446 | r := startMulti(t, func(c *ports.Config) { | ||
| 437 | c.Rules = []ports.WindowRule{winRule("steam", `steam_app_\d+`, func(r *ports.WindowRule) { r.Floating = ruleOn() })} | 447 | c.Rules = []ports.WindowRule{winRule("steam", `steam_app_\d+`, func(r *ports.WindowRule) { r.Floating = ruleOn() })} | ||
M internal/core/spawn_placement.go +1 -1
| @@ -142,7 +142,7 @@ func (p *spawnPlacement) place(c *Core, v ports.WindowMapped) { | |||
| 142 | 142 | eff, ok := c.ruleFor(v) | |
| 143 | 143 | // A window mapping at an output's rect goes to that output unless a | |
| 144 | 144 | // rule names a monitor or workspace. | |
| 145 | - | if v.HasAt && !v.Remap && eff.monitor == "" { | |
| 145 | + | if v.HasAt && !v.Remap && eff.monitor == "" && eff.workspace == "" && eff.number == 0 { | |
| 146 | 146 | if i := c.screenAt(float64(v.AtX), float64(v.AtY)); i >= 0 && i != c.focusScreen { | |
| 147 | 147 | eff.monitor, ok = c.screens[i].name(), true | |
| 148 | 148 | } | |
| @@ -142,7 +142,7 @@ func (p *spawnPlacement) place(c *Core, v ports.WindowMapped) { | |||||
| 142 | eff, ok := c.ruleFor(v) | 142 | eff, ok := c.ruleFor(v) | ||
| 143 | // A window mapping at an output's rect goes to that output unless a | 143 | // A window mapping at an output's rect goes to that output unless a | ||
| 144 | // rule names a monitor or workspace. | 144 | // rule names a monitor or workspace. | ||
| 145 | - | if v.HasAt && !v.Remap && eff.monitor == "" { | 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 { | 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 | 147 | eff.monitor, ok = c.screens[i].name(), true | ||
| 148 | } | 148 | } | ||