mirror of
https://github.com/AquaMorph/dotfiles.git
synced 2026-11-03 08:17:59 +00:00
feat(macos): configure hammerspoon window placement
This commit is contained in:
1 parent
9e84a9e32a
commit
c88edc5b8e
3 files changed
+134
No files matched your search
@@ -58,5 +58,6 @@ Library/Application Support/OrcaSlicer
|
||||
.config/aerospace
|
||||
.config/karabiner
|
||||
.config/AutoRaise
|
||||
.hammerspoon
|
||||
bin/setup-macos.sh
|
||||
{{ end -}}
|
||||
@@ -44,6 +44,9 @@ cask "font-inconsolata"
|
||||
tap "dimentium/autoraise"
|
||||
brew "dimentium/autoraise/autoraise"
|
||||
|
||||
# Save and automatically restore window layouts
|
||||
cask "hammerspoon"
|
||||
|
||||
# Optional tools for audio conversion and local AI
|
||||
# brew "ffmpeg"
|
||||
# brew "ollama"
|
||||
@@ -0,0 +1,130 @@
|
||||
-- Placement rules inferred from the running windows on this Mac.
|
||||
require("hs.ipc")
|
||||
|
||||
local spaces = require("hs.spaces")
|
||||
local log = hs.logger.new("placement", "info")
|
||||
local monitors = {
|
||||
main = "CBBC3C70-2EE1-4D28-8B7D-DC03A186F6EB", -- DELL P2715Q (2)
|
||||
portrait = "22CE41A6-C177-4B4D-85D3-43337F9566E8", -- DELL P2715Q (1)
|
||||
right = "3C0512D4-DAFF-4EE2-8CC8-8EA148E1ECB0", -- T24i-2L
|
||||
}
|
||||
|
||||
-- Desktop numbers are per monitor, excluding native fullscreen Spaces.
|
||||
-- Frames are fractions of the entire display, including the menu bar area.
|
||||
local rules = {
|
||||
{app = "com.mitchellh.ghostty", monitor = "main", desktop = 1,
|
||||
frame = {0, 30 / 1890, 1, 1860 / 1890}},
|
||||
{app = "io.robbie.HomeAssistant", monitor = "portrait", desktop = 1,
|
||||
frame = {0, 30 / 3360, 1, 1665 / 3360}},
|
||||
{app = "org.whispersystems.signal-desktop", monitor = "right", desktop = 2,
|
||||
frame = {0, 30 / 1080, 1, 1050 / 1080}},
|
||||
-- Match specialized browser windows; general browsing uses the fallback below.
|
||||
{app = "com.brave.Browser", title = "YouTube", monitor = "portrait", desktop = 1,
|
||||
frame = {0, 1695 / 3360, 1, 1665 / 3360}},
|
||||
{app = "com.brave.Browser", title = "Proxmox Console", monitor = "right", desktop = 1,
|
||||
frame = {0, 30 / 1080, 800 / 1920, 484 / 1080}},
|
||||
-- Other browser windows start on the right; a later YouTube title moves them.
|
||||
{app = "com.brave.Browser", monitor = "right", desktop = 1,
|
||||
frame = {0, 30 / 1080, 1, 1050 / 1080}},
|
||||
-- Steam's library is owned by Steam Helper, not the launcher process.
|
||||
{app = "com.valvesoftware.steam", monitor = "right", desktop = 1,
|
||||
frame = {267 / 1920, 163 / 1080, 1010 / 1920, 1050 / 1080}},
|
||||
{name = "Steam Helper", monitor = "right", desktop = 1,
|
||||
frame = {267 / 1920, 163 / 1080, 1010 / 1920, 1050 / 1080}},
|
||||
}
|
||||
|
||||
local applied, pending = {}, {}
|
||||
|
||||
local function ruleFor(window)
|
||||
local app = window:application()
|
||||
if not app or not window:isStandard() or window:isFullScreen()
|
||||
or window:isMinimized() then return end
|
||||
-- Ignore Steam's small helper/tooltip windows even if exposed as standard.
|
||||
if app:name() == "Steam Helper" and window:frame().w < 600 then return end
|
||||
for index, rule in ipairs(rules) do
|
||||
if ((rule.app and app:bundleID() == rule.app)
|
||||
or (rule.name and app:name() == rule.name))
|
||||
and (not rule.title or window:title():find(rule.title, 1, true)) then
|
||||
return rule, index
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function place(window, force)
|
||||
local rule, index = ruleFor(window)
|
||||
local id = window:id()
|
||||
if not rule or not id or (not force and applied[id] == index) then return end
|
||||
local screen
|
||||
for _, candidate in ipairs(hs.screen.allScreens()) do
|
||||
if candidate:getUUID() == monitors[rule.monitor] then screen = candidate; break end
|
||||
end
|
||||
if not screen then return end -- Leave windows alone when a monitor is unplugged.
|
||||
local desktops = {}
|
||||
for _, space in ipairs(spaces.spacesForScreen(screen) or {}) do
|
||||
if spaces.spaceType(space) == "user" then table.insert(desktops, space) end
|
||||
end
|
||||
local target = desktops[rule.desktop]
|
||||
local current = spaces.windowSpaces(window) or {}
|
||||
if not target or #current ~= 1 or spaces.spaceType(current[1]) ~= "user" then return end
|
||||
|
||||
window:moveToScreen(screen, false, true, 0)
|
||||
if current[1] ~= target then
|
||||
local ok, err = spaces.moveWindowToSpace(window, target)
|
||||
if not ok then
|
||||
log.w("Desktop placement failed: " .. tostring(err))
|
||||
return
|
||||
end
|
||||
end
|
||||
local bounds, frame = screen:fullFrame(), rule.frame
|
||||
window:setFrame(hs.geometry.rect(
|
||||
bounds.x + frame[1] * bounds.w, bounds.y + frame[2] * bounds.h,
|
||||
frame[3] * bounds.w, frame[4] * bounds.h
|
||||
), 0)
|
||||
applied[id] = index
|
||||
end
|
||||
|
||||
local filter = hs.window.filter.new(true):setOverrideFilter({
|
||||
allowRoles = "AXStandardWindow", fullscreen = false,
|
||||
})
|
||||
|
||||
local function restore()
|
||||
for _, window in ipairs(filter:getWindows()) do place(window, true) end
|
||||
end
|
||||
|
||||
filter:subscribe({hs.window.filter.windowCreated, hs.window.filter.windowTitleChanged}, function(window)
|
||||
local id = window:id()
|
||||
if not id then return end
|
||||
if pending[id] then pending[id]:stop() end
|
||||
-- Wait for apps to finish their own initial resize and browser page loading.
|
||||
pending[id] = hs.timer.doAfter(1, function()
|
||||
pending[id] = nil
|
||||
if window:id() then place(window, false) end
|
||||
end)
|
||||
end)
|
||||
filter:subscribe(hs.window.filter.windowDestroyed, function(window)
|
||||
local id = window:id()
|
||||
if id then
|
||||
applied[id] = nil
|
||||
if pending[id] then pending[id]:stop(); pending[id] = nil end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Retain the filter, watchers and menu for the lifetime of the configuration.
|
||||
windowPlacement = {
|
||||
rules = rules,
|
||||
restore = restore,
|
||||
filter = filter,
|
||||
menu = hs.menubar.new(),
|
||||
screenWatcher = hs.screen.watcher.new(function()
|
||||
hs.timer.doAfter(3, restore)
|
||||
end):start(),
|
||||
}
|
||||
windowPlacement.menu:setTitle("Windows")
|
||||
windowPlacement.menu:setMenu({
|
||||
{title = "Reapply placement rules", fn = restore},
|
||||
{title = "Reload configuration", fn = hs.reload},
|
||||
{title = "Open Console", fn = hs.openConsole},
|
||||
})
|
||||
|
||||
hs.autoLaunch(true)
|
||||
if hs.accessibilityState(true) then hs.timer.doAfter(3, restore) end
|
||||
Reference in new issue
Block a user