Created
October 9, 2025 08:45
-
-
Save liuzhoou/5ed90bbbf4ab85ebab20a067d69503c9 to your computer and use it in GitHub Desktop.
层叠当前窗口
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- ~/.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