Files
2026-05-14 18:59:23 -04:00

157 lines
6.0 KiB
Lua

local obj = {}
local json = require("hs.json")
-- ==========================================
-- CONFIGURATION
-- ==========================================
obj.layoutFile = os.getenv("HOME") .. "/.hammerspoon/saved_layout.json"
obj.saveInterval = 300
-- Apps that require AppleScript authority
local stubbornApps = {
["Gemini"] = true,
["AFFiNE"] = true,
["Hammerspoon"] = true,
["System Settings"] = true
}
-- Apps or Bundle IDs that the window manager should NEVER touch
local ignoreList = {
["TheBoringNotch"] = true,
["theboringteam.boringnotch"] = true,
["boring.notch"] = true,
["The Boring Notch"] = true,
["Control Center"] = true,
["Notification Center"] = true,
["Dock"] = true,
["com.surteesstudios.Bartender"] = true,
["pro.betterdisplay.BetterDisplay"] = true
}
-- ==========================================
-- LOGIC
-- ==========================================
-- Check if window actually needs to be moved
local function needsMoving(win, savedFrame)
local curr = win:frame()
local threshold = 10
return math.abs(curr.x - savedFrame.x) > threshold or
math.abs(curr.y - savedFrame.y) > threshold
end
function obj.saveLayout(silent)
-- Safety: Don't save if the system is currently sleeping or screens are locked
if hs.caffeinate.get("displayIdle") or hs.caffeinate.get("systemIdle") then return end
local allWindows = hs.window.allWindows()
local consoleWin = hs.console.hswindow()
if consoleWin then table.insert(allWindows, consoleWin) end
local currentLayout = {
saveTime = os.date("%Y-%m-%d %H:%M:%S"),
windows = {}
}
for _, win in ipairs(allWindows) do
local app = win:application()
local screen = win:screen()
local frame = win:frame()
-- GHOST WINDOW FIX: Only save windows that have actual size
if app and screen and win:isVisible() and frame.w > 0 and frame.h > 0 then
local name = app:name() or ""
local bid = app:bundleID() or ""
-- IGNORE LOGIC: Match by name, Bundle ID, or fuzzy notch match
local isIgnored = ignoreList[name] or ignoreList[bid] or name:find("TheBoringNotch")
if not isIgnored then
table.insert(currentLayout.windows, {
appName = name,
bundleID = bid,
winTitle = win:title(),
screenName = screen:name(),
x = frame.x, y = frame.y, w = frame.w, h = frame.h
})
end
end
end
hs.json.write(currentLayout, obj.layoutFile, true, true)
if not silent then hs.alert.show("Layout Saved: " .. currentLayout.saveTime, 1.5) end
end
function obj.restoreLayout()
local data = hs.json.read(obj.layoutFile)
if not data then return end
local windowList = data.windows or data
local appleScriptParts = {"tell application \"System Events\""}
local needsAppleScript = false
for _, winData in ipairs(windowList) do
-- Handle Hammerspoon Console specifically
if winData.appName == "Hammerspoon" and winData.winTitle == "Hammerspoon Console" then
local cWin = hs.console.hswindow()
if cWin and needsMoving(cWin, winData) then
cWin:setFrame({x=winData.x, y=winData.y, w=winData.w, h=winData.h}, 0)
end
else
local app = hs.application.get(winData.bundleID) or hs.application.get(winData.appName)
if app then
for _, win in ipairs(app:allWindows()) do
local targetScreen = hs.screen.find(winData.screenName) or hs.screen.primaryScreen()
local isFuzzy = (winData.bundleID == "com.apple.Terminal" or winData.appName == "Gemini")
local isStubborn = stubbornApps[winData.appName]
local shouldMove = (win:title() == winData.winTitle or #app:allWindows() == 1 or isFuzzy or isStubborn)
if targetScreen and shouldMove and needsMoving(win, winData) then
if isStubborn then
needsAppleScript = true
table.insert(appleScriptParts, string.format([[
try
tell process "%s" to set position of window 1 to {%d, %d}
tell process "%s" to set size of window 1 to {%d, %d}
end try
]], winData.appName, winData.x, winData.y, winData.appName, winData.w, winData.h))
else
win:setFrame({x=winData.x, y=winData.y, w=winData.w, h=winData.h}, 0)
end
end
end
end
end
end
if needsAppleScript then
table.insert(appleScriptParts, "end tell")
hs.applescript.applescript(table.concat(appleScriptParts, "\n"))
end
end
-- Hotkeys
hs.hotkey.bind({"shift", "cmd"}, "W", function() obj.saveLayout(false) end)
hs.hotkey.bind({"shift", "cmd"}, "R", function() obj.restoreLayout() end)
-- Automation: Screen Watcher
obj.screenWatcher = hs.screen.watcher.new(function()
hs.timer.doAfter(2, function() obj.restoreLayout() end)
end):start()
-- Automation: Sleep Watcher
obj.sleepWatcher = hs.caffeinate.watcher.new(function(event)
if event == hs.caffeinate.watcher.systemWillSleep or event == hs.caffeinate.watcher.screensDidLock then
if obj.autoSaveTimer then obj.autoSaveTimer:stop() end
elseif event == hs.caffeinate.watcher.systemDidWake or event == hs.caffeinate.watcher.screensDidUnlock then
if obj.autoSaveTimer then obj.autoSaveTimer:start() end
hs.timer.doAfter(3, function() obj.restoreLayout() end)
end
end):start()
obj.autoSaveTimer = hs.timer.doEvery(obj.saveInterval, function() obj.saveLayout(true) end)
hs.alert.show("Window Manager Loaded", 1.5)
return obj