hypr: fix AI desktop apps stealing focus via xdg-activation
claude-desktop and codex-desktop fire xdg-activation requests while streaming responses. With misc:focus_on_activate=true this yanked focus (and warped the cursor) to those windows mid-use — most visibly causing Super+Ctrl+Space to always end up focused on claude-desktop regardless of which window was active. Fix in settings.lua: window rules for claude-desktop and codex-desktop with focus_on_activate=false (dynamic — applies to already-mapped windows on reload) and suppress_event="activatefocus" (static — applies at map time). Forced activations from the taskbar/foreign-toplevel-wlr protocol still focus the window normally. Secondary fixes in layouts.lua: - find_tabbed_group_anchor: constrain search to current workspace so the binding can't jump across workspaces when a stale anchor exists elsewhere - restore_workspace_tabbed_group / gather_workspace_into_tabbed_group: use the window that was focused at binding invocation time rather than the group anchor, so focus is returned to where the user actually was - Add focus_window_with_cursor helper: focuses the target window and warps the cursor to its center when the cursor is outside it, preventing follow_mouse=1 from fighting the explicit focus dispatch Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -312,6 +312,50 @@ function M.setup(ctx)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function window_contains_point(window, x, y)
|
||||||
|
local at = window and window.at
|
||||||
|
local size = window and window.size
|
||||||
|
if not at or not size then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local left = tonumber(at.x or at[1])
|
||||||
|
local top = tonumber(at.y or at[2])
|
||||||
|
local width = tonumber(size.x or size[1])
|
||||||
|
local height = tonumber(size.y or size[2])
|
||||||
|
if not left or not top or not width or not height then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return x >= left and x < left + width and y >= top and y < top + height
|
||||||
|
end
|
||||||
|
|
||||||
|
-- With follow_mouse=1, a bare focus dispatch does not survive the next
|
||||||
|
-- pointer motion unless the cursor already sits inside the target window,
|
||||||
|
-- so warp the cursor into the window when needed.
|
||||||
|
local function focus_window_with_cursor(window)
|
||||||
|
local selector = window_selector(window)
|
||||||
|
if not selector then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local live = type(hl.get_window) == "function" and hl.get_window(selector) or window
|
||||||
|
if not live then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
dispatch(hl.dsp.focus({ window = selector }))
|
||||||
|
|
||||||
|
local cursor = hl.get_cursor_pos and hl.get_cursor_pos()
|
||||||
|
if cursor and window_contains_point(live, cursor.x, cursor.y) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local center_x, center_y = window_center(live)
|
||||||
|
dispatch(hl.dsp.cursor.move({ x = math.floor(center_x), y = math.floor(center_y) }))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
local function find_tabbed_group_anchor(state)
|
local function find_tabbed_group_anchor(state)
|
||||||
local active = hl.get_active_window()
|
local active = hl.get_active_window()
|
||||||
if active and active.group and active.group.size and active.group.size > 1 then
|
if active and active.group and active.group.size and active.group.size > 1 then
|
||||||
@@ -322,8 +366,16 @@ function M.setup(ctx)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local workspace = active_workspace()
|
||||||
for _, window in ipairs(hl.get_windows()) do
|
for _, window in ipairs(hl.get_windows()) do
|
||||||
if window and window.address == state.anchor and window.group and window.group.size and window.group.size > 1 then
|
if
|
||||||
|
window
|
||||||
|
and window.address == state.anchor
|
||||||
|
and same_workspace(window.workspace, workspace)
|
||||||
|
and window.group
|
||||||
|
and window.group.size
|
||||||
|
and window.group.size > 1
|
||||||
|
then
|
||||||
return window
|
return window
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -378,6 +430,7 @@ function M.setup(ctx)
|
|||||||
local function restore_workspace_tabbed_group()
|
local function restore_workspace_tabbed_group()
|
||||||
local key = workspace_key()
|
local key = workspace_key()
|
||||||
local state = tabbed_workspace_groups[key]
|
local state = tabbed_workspace_groups[key]
|
||||||
|
local entry_focused = hl.get_active_window()
|
||||||
local anchor = find_tabbed_group_anchor(state)
|
local anchor = find_tabbed_group_anchor(state)
|
||||||
local anchor_selector = window_selector(anchor)
|
local anchor_selector = window_selector(anchor)
|
||||||
local target_workspace_id = anchor and anchor.workspace and anchor.workspace.id
|
local target_workspace_id = anchor and anchor.workspace and anchor.workspace.id
|
||||||
@@ -394,7 +447,9 @@ function M.setup(ctx)
|
|||||||
tabbed_workspace_groups[key] = nil
|
tabbed_workspace_groups[key] = nil
|
||||||
set_layout(columns_layout)
|
set_layout(columns_layout)
|
||||||
restore_tabbed_group_window_order(state, target_workspace_id)
|
restore_tabbed_group_window_order(state, target_workspace_id)
|
||||||
dispatch(hl.dsp.focus({ window = anchor_selector }))
|
if not focus_window_with_cursor(entry_focused) then
|
||||||
|
focus_window_with_cursor(anchor)
|
||||||
|
end
|
||||||
schedule_nstack_count_update()
|
schedule_nstack_count_update()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -469,6 +524,9 @@ function M.setup(ctx)
|
|||||||
dispatch(hl.dsp.focus({ window = anchor_selector }))
|
dispatch(hl.dsp.focus({ window = anchor_selector }))
|
||||||
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
|
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
|
||||||
notify_tabbed_group("Unable to group tiled windows")
|
notify_tabbed_group("Unable to group tiled windows")
|
||||||
|
if not focus_window_with_cursor(focused) then
|
||||||
|
focus_window_with_cursor(anchor)
|
||||||
|
end
|
||||||
return
|
return
|
||||||
elseif grouped_count < #candidates then
|
elseif grouped_count < #candidates then
|
||||||
notify_tabbed_group("Grouped " .. tostring(grouped_count) .. " of " .. tostring(#candidates) .. " tiled windows")
|
notify_tabbed_group("Grouped " .. tostring(grouped_count) .. " of " .. tostring(#candidates) .. " tiled windows")
|
||||||
@@ -479,7 +537,9 @@ function M.setup(ctx)
|
|||||||
order = original_order,
|
order = original_order,
|
||||||
windows = candidate_addresses,
|
windows = candidate_addresses,
|
||||||
}
|
}
|
||||||
dispatch(hl.dsp.focus({ window = anchor_selector }))
|
if not focus_window_with_cursor(focused) then
|
||||||
|
focus_window_with_cursor(anchor)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function force_columns_layout()
|
local function force_columns_layout()
|
||||||
|
|||||||
@@ -429,6 +429,20 @@ function M.setup(ctx)
|
|||||||
})
|
})
|
||||||
hl.window_rule({ match = { title = "^(Confirm)$" }, float = true })
|
hl.window_rule({ match = { title = "^(Confirm)$" }, float = true })
|
||||||
|
|
||||||
|
-- The AI desktop apps fire xdg-activation requests while streaming
|
||||||
|
-- responses; with misc:focus_on_activate=true that steals focus from
|
||||||
|
-- whatever window the user is actually working in. focus_on_activate is
|
||||||
|
-- a dynamic rule (applies to already-mapped windows on reload);
|
||||||
|
-- suppress_event only applies at map time.
|
||||||
|
for index, class in ipairs({ "^(claude-desktop)$", "^(codex-desktop)$" }) do
|
||||||
|
hl.window_rule({
|
||||||
|
name = "ai-app-no-activate-focus-" .. tostring(index),
|
||||||
|
match = { class = class },
|
||||||
|
focus_on_activate = false,
|
||||||
|
suppress_event = "activatefocus",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
for index, match in ipairs({
|
for index, match in ipairs({
|
||||||
{ class = "^(flameshot)$" },
|
{ class = "^(flameshot)$" },
|
||||||
{ title = "^(flameshot)$" },
|
{ title = "^(flameshot)$" },
|
||||||
|
|||||||
Reference in New Issue
Block a user