Skip to content

Instantly share code, notes, and snippets.

@liuzhoou
Created October 9, 2025 08:45
Show Gist options
  • Select an option

  • Save liuzhoou/5ed90bbbf4ab85ebab20a067d69503c9 to your computer and use it in GitHub Desktop.

Select an option

Save liuzhoou/5ed90bbbf4ab85ebab20a067d69503c9 to your computer and use it in GitHub Desktop.
层叠当前窗口
-- ~/.hammerspoon/init.lua
------------------------------------------------------------
-- 仅层叠:当前聚焦窗口 → 所在显示器 + 该窗口所在 Space
-- 热键:Ctrl + Option + Command + C
------------------------------------------------------------
local hasSpaces = (hs.spaces ~= nil)
-- 层叠参数(按需改)
local CASCADE_CFG = {
step = 36, -- 每个窗口的阶梯偏移(像素)
margin = 60, -- 起始边距(离屏幕左上角)
widthPct = 0.62, -- 目标宽度占屏幕比例
heightPct = 0.72, -- 目标高度占屏幕比例
wrapAfter = 12, -- 越界回卷周期
}
local function isCascadable(win)
return win
and win:isVisible()
and win:isStandard()
and not win:isFullScreen()
end
-- 从某屏幕 + 指定 Space 抓取可层叠窗口(聚焦靠后的排后面 → 叠层更直观)
local function windowsOnScreenAndSpace(screen, targetSpaceId)
if not screen then return {} end
-- 优先用 hs.spaces 精准筛选
if hasSpaces and targetSpaceId then
local ordered = hs.window.orderedWindows() -- 最近聚焦顺序
local wins = {}
for _, w in ipairs(ordered) do
if isCascadable(w) and w:screen() == screen then
local sids = hs.spaces.windowSpaces(w)
if sids then
for _, sid in ipairs(sids) do
if sid == targetSpaceId then
table.insert(wins, w)
break
end
end
end
end
end
return wins
end
-- 回退方案:只能处理“当前 Space”,且限定屏幕
local wf = hs.window.filter.new()
wf:setCurrentSpace(true)
wf:setDefaultFilter({
visible = true,
fullscreen = false,
allowRoles = {"AXStandardWindow","AXDialog"},
})
wf:setScreens({screen})
return wf:getWindows(hs.window.filter.sortByFocusedLast)
end
local function cascadeOnScreen(screen, wins, cfg)
cfg = cfg or CASCADE_CFG
local placed = 0
if not screen or #wins == 0 then return placed end
local s = screen:frame()
local W = math.floor(s.w * cfg.widthPct)
local H = math.floor(s.h * cfg.heightPct)
local baseX, baseY = s.x + cfg.margin, s.y + cfg.margin
for i, win in ipairs(wins) do
if isCascadable(win) then
placed = placed + 1
local dx = (placed - 1) * cfg.step
local dy = (placed - 1) * cfg.step
local nx, ny = baseX + dx, baseY + dy
-- 越界回卷,保持窗口完全在屏幕内
if nx + W > s.x + s.w or ny + H > s.y + s.h then
local k = (placed - 1) % cfg.wrapAfter
nx = s.x + 20 + k * cfg.step
ny = s.y + 20 + k * cfg.step
end
win:setFrame({x = nx, y = ny, w = W, h = H}, 0)
end
end
return placed
end
-- 主入口:只动“前台窗口所在 显示器 + 该窗口所在 Space”
local function cascadeFocusedWindowSpaceOnly()
local front = hs.window.frontmostWindow()
if not front or not front:screen() then
hs.alert.show("未找到前台窗口或屏幕")
return
end
if front:isFullScreen() then
hs.alert.show("前台窗口为全屏,无法层叠")
return
end
local screen = front:screen()
local spaceId = nil
if hasSpaces then
local sids = hs.spaces.windowSpaces(front)
-- 一般窗口只在一个 Space,取第一个即可
if sids and #sids > 0 then spaceId = sids[1] end
if not spaceId then
hs.alert.show("无法获取前台窗口的 Space,已取消")
return
end
end
local wins = windowsOnScreenAndSpace(screen, spaceId)
local n = cascadeOnScreen(screen, wins, CASCADE_CFG)
hs.alert.show(("层叠完成(当前窗口所在 Space):%d 个窗口"):format(n))
end
-- 绑定热键
hs.hotkey.bind({"ctrl","alt","cmd"}, "C", cascadeFocusedWindowSpaceOnly)
-- (可选)再加个更紧凑参数的一次性层叠热键:⌃⌥⌘ + X
hs.hotkey.bind({"ctrl","alt","cmd"}, "X", function()
local bak = CASCADE_CFG
CASCADE_CFG = { step=28, margin=40, widthPct=0.66, heightPct=0.66, wrapAfter=10 }
cascadeFocusedWindowSpaceOnly()
CASCADE_CFG = bak
end)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment