Initial commit of Hammerspoon config
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
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
|
||||
Reference in New Issue
Block a user