From 884c7bc6fe5cfccf4d59547ab8ff89d9c853e3a4 Mon Sep 17 00:00:00 2001
From: Roman Kitaev <802.11g@bk.ru>
Date: Thu, 6 Aug 2026 22:43:16 +0500
Subject: [PATCH] feat: add Reset Mouse Look keybind to recover the macOS
stuck-cursor freeze
---
.context/patterns.md | 4 ++++
CombatMode/Bindings.xml | 3 +++
.../Core/FreeLook/FreeLookController.lua | 18 ++++++++++++++++++
CombatMode/UI/Options/Tabs/TabGeneral.lua | 17 +++++++++++++++++
4 files changed, 42 insertions(+)
diff --git a/.context/patterns.md b/.context/patterns.md
index 6c0a05d..03b23c0 100644
--- a/.context/patterns.md
+++ b/.context/patterns.md
@@ -12,3 +12,7 @@ Prefer thin domain modules ending in a façade that re-exports the public `CM.*`
Canonical rules live in **`.cursor/rules/combatmode-lua-safety.mdc`** (Secret values and instance taint).
Summary: never `==`/`>` on possibly secret UnitGUID/cast GUID/reaction/health/name/width; use `issecretvalue` first, `PublicBool` / `PublicNumber`, `UnitExists`/`UnitIsUnit`/frame identity, color curves / `EvaluateColorFromBoolean`, and combat-deferred binding flushes. Reference Crosshair, FocusNameplateMarker, CastProgress, InteractionHUD Target, PartyRadial HealthBars/RoleIcons/Visual.
+
+## macOS stuck-cursor freeze
+
+A `MouselookStop()` issued while the window is backgrounded (a watched frame auto-unlocks Mouse Look during an alt-tab, e.g. bags/LFG) leaves the OS mouse capture stuck — frozen cursor/camera on return while `IsMouselooking()` lies. There is no window-focus event and the background keeps rendering, so it can't be detected from `OnUpdate`. Recovery is the manual `Combat Mode - Reset Mouse Look` keybind (`CombatMode_ResetMouseLook`, `Core/FreeLook/FreeLookController.lua`): re-grab + release through `CM.LockFreeLook` / `CM.UnlockFreeLook` (keeps UI in sync). The config keybind (`UI/Options/Tabs/TabGeneral.lua`) is shown macOS-only via `IsMacClient()`.
diff --git a/CombatMode/Bindings.xml b/CombatMode/Bindings.xml
index 2f963ff..ab8057f 100644
--- a/CombatMode/Bindings.xml
+++ b/CombatMode/Bindings.xml
@@ -4,6 +4,9 @@
CombatMode_CursorModeKey(keystate)
end
+
+ CombatMode_ResetMouseLook()
+
if keystate == "down" or keystate == "up" then
CombatMode_PartyRadialKey(keystate)
diff --git a/CombatMode/Core/FreeLook/FreeLookController.lua b/CombatMode/Core/FreeLook/FreeLookController.lua
index 70b02e1..6e944d1 100644
--- a/CombatMode/Core/FreeLook/FreeLookController.lua
+++ b/CombatMode/Core/FreeLook/FreeLookController.lua
@@ -435,3 +435,21 @@ function _G.CombatMode_CursorModeKey(keystate)
FreeLookOverride = false
end
end
+
+-- Manual recovery for the macOS stuck-cursor freeze (a MouselookStop issued while the
+-- window was backgrounded leaves the OS capture stuck). Re-grab and release through the
+-- free-look state machine so the crosshair/tooltip/CVar side effects stay in sync rather
+-- than trusting IsMouselooking(); OnUpdate re-locks afterward if free look should be active.
+function _G.CombatMode_ResetMouseLook()
+ CM.LockFreeLook()
+ local function settle()
+ if CM.ShouldFreeLookBeOff() then
+ CM.UnlockFreeLook()
+ end
+ end
+ if C_Timer and C_Timer.After then
+ C_Timer.After(0, settle)
+ else
+ settle()
+ end
+end
diff --git a/CombatMode/UI/Options/Tabs/TabGeneral.lua b/CombatMode/UI/Options/Tabs/TabGeneral.lua
index cbd79f7..d473b54 100644
--- a/CombatMode/UI/Options/Tabs/TabGeneral.lua
+++ b/CombatMode/UI/Options/Tabs/TabGeneral.lua
@@ -25,6 +25,7 @@ local _G = _G
-- WoW API
local GetBindingKey = _G.GetBindingKey
local GetCurrentBindingSet = _G.GetCurrentBindingSet
+local IsMacClient = _G.IsMacClient
local ReloadUI = _G.ReloadUI
local SaveBindings = _G.SaveBindings
local SetBinding = _G.SetBinding
@@ -110,6 +111,22 @@ UI.Options.AddTab({
end)
end,
})
+ -- macOS only: on other clients the stuck-cursor freeze does not occur, so this
+ -- recovery keybind is hidden to avoid clutter.
+ if IsMacClient() then
+ ctx:Keybind({
+ label = "Reset Mouse Look",
+ desc = "Recover a stuck cursor after switching windows on macOS. Re-grabs and releases Mouse Look to clear a capture the game left stuck while running in the background.",
+ get = function()
+ return (GetBindingKey("Combat Mode - Reset Mouse Look"))
+ end,
+ set = function(key)
+ CM.TryApplyBindingChange("reset mouse look keybinding", function()
+ CM.AssignNamedKeybind("Combat Mode - Reset Mouse Look", key)
+ end)
+ end,
+ })
+ end
ctx:Toggle({
label = "Pulse Cursor on Unlock",
desc = "Flash the cursor when Mouse Look ends.",