Compare commits

..

4 Commits

4 changed files with 31 additions and 96 deletions
+8 -3
View File
@@ -166,9 +166,14 @@ local function executeRestore(filePath, layoutName)
win:setFrame({x=x, y=y, w=w, h=h}, 0) win:setFrame({x=x, y=y, w=w, h=h}, 0)
end end
end end
-- Establish sequential window targeting intervals
-- Shifts execution past the window manager canvas updates
moveAction() moveAction()
hs.timer.doAfter(0.5, moveAction) local intervals = isStubborn and {0.2, 0.6, 1.2, 2.2} or {0.3, 1.0}
hs.timer.doAfter(1.5, moveAction) for _, delay in ipairs(intervals) do
hs.timer.doAfter(delay, moveAction)
end
end end
end end
end end
@@ -178,7 +183,7 @@ local function executeRestore(filePath, layoutName)
if launchedAny then if launchedAny then
hs.alert.show("Syncing Apps...", 3) hs.alert.show("Syncing Apps...", 3)
hs.timer.doAfter(4.5, moveWindows) hs.timer.doAfter(5.0, moveWindows) -- Bumped to 5s to let heavy frameworks build window handles completely
else else
moveWindows() moveWindows()
end end
+9 -9
View File
@@ -38,6 +38,7 @@ obj.isMenuUpdating = false -- Safety guard to prevent concurrent IPC menu render
obj.wakeTimer = nil obj.wakeTimer = nil
obj.lastScreenCount = #hs.screen.allScreens() obj.lastScreenCount = #hs.screen.allScreens()
obj.lastSavedTime = "Never" obj.lastSavedTime = "Never"
obj.wakeTimestamp = 0 -- Track wake time to isolate screen changes during system startup
-- ========================================== -- ==========================================
-- INTERNAL UTILITIES -- INTERNAL UTILITIES
@@ -232,7 +233,6 @@ end
local timerMenu = hs.menubar.new() local timerMenu = hs.menubar.new()
function updateMenu() function updateMenu()
-- Safety Guard: Bail if update is already running from a previous thread block
if obj.isMenuUpdating then return end if obj.isMenuUpdating then return end
obj.isMenuUpdating = true obj.isMenuUpdating = true
@@ -270,7 +270,6 @@ obj.powerWatcher = hs.caffeinate.watcher.new(function(event)
if event == hs.caffeinate.watcher.systemWillSleep or event == hs.caffeinate.watcher.screensDidSleep or event == hs.caffeinate.watcher.screensDidLock then if event == hs.caffeinate.watcher.systemWillSleep or event == hs.caffeinate.watcher.screensDidSleep or event == hs.caffeinate.watcher.screensDidLock then
log("POWER: Sleep event.") log("POWER: Sleep event.")
obj.isTransitioning = true obj.isTransitioning = true
-- Stop both timers entirely so they don't fire or stack requests while suspended
if obj.autoSaveTimer then obj.autoSaveTimer:stop() end if obj.autoSaveTimer then obj.autoSaveTimer:stop() end
if obj.clockTimer then obj.clockTimer:stop() end if obj.clockTimer then obj.clockTimer:stop() end
elseif event == hs.caffeinate.watcher.systemDidWake or event == hs.caffeinate.watcher.screensDidWake or event == hs.caffeinate.watcher.screensDidUnlock then elseif event == hs.caffeinate.watcher.systemDidWake or event == hs.caffeinate.watcher.screensDidWake or event == hs.caffeinate.watcher.screensDidUnlock then
@@ -279,8 +278,8 @@ obj.powerWatcher = hs.caffeinate.watcher.new(function(event)
obj.isMenuUpdating = false obj.isMenuUpdating = false
obj.isTransitioning = false obj.isTransitioning = false
obj.isRestoring = false obj.isRestoring = false
obj.wakeTimestamp = os.time()
-- Safely start timers up only after wakeup initialization
if obj.autoSaveTimer then obj.autoSaveTimer:start() end if obj.autoSaveTimer then obj.autoSaveTimer:start() end
if obj.clockTimer then obj.clockTimer:start() end if obj.clockTimer then obj.clockTimer:start() end
if obj.wakeTimer then obj.wakeTimer:stop() end if obj.wakeTimer then obj.wakeTimer:stop() end
@@ -288,15 +287,15 @@ obj.powerWatcher = hs.caffeinate.watcher.new(function(event)
local currentScreens = #hs.screen.allScreens() local currentScreens = #hs.screen.allScreens()
if currentScreens > 1 then if currentScreens > 1 then
log("WAKE ACTIVATE: Multi-screen detected on wake. Scheduling layout restoration.")
obj.lastScreenCount = currentScreens -- Sync variable immediately so it knows we are docked
obj.wakeTimer = hs.timer.doAfter(12, function() obj.wakeTimer = hs.timer.doAfter(12, function()
obj.isTransitioning = false obj.isTransitioning = false
obj.isRestoring = false obj.isRestoring = false
obj.lastScreenCount = currentScreens
obj.restoreLayout() obj.restoreLayout()
obj.wakeTimer = nil obj.wakeTimer = nil
end) end)
else else
-- Only run rescue if the state actually dropped from multi-monitor down to single monitor during sleep
if obj.lastScreenCount > 1 then if obj.lastScreenCount > 1 then
log("WAKE ACTIVATE: Screen count dropped from " .. obj.lastScreenCount .. " to 1 during sleep. Running rescue.") log("WAKE ACTIVATE: Screen count dropped from " .. obj.lastScreenCount .. " to 1 during sleep. Running rescue.")
obj.rescueWindowsToLaptop() obj.rescueWindowsToLaptop()
@@ -314,6 +313,11 @@ hs.hotkey.bind({"shift", "cmd"}, "R", function() obj.restoreLayout() end)
hs.hotkey.bind({"shift", "cmd", "ctrl"}, "L", obj.rescueWindowsToLaptop) hs.hotkey.bind({"shift", "cmd", "ctrl"}, "L", obj.rescueWindowsToLaptop)
obj.screenWatcher = hs.screen.watcher.new(function() obj.screenWatcher = hs.screen.watcher.new(function()
if (os.time() - obj.wakeTimestamp) < 10 then
log("DOCK EVENT: Dropped via wake isolation guard.")
return
end
if obj.isTransitioning or obj.isRestoring or obj.wakeTimer then if obj.isTransitioning or obj.isRestoring or obj.wakeTimer then
log("DOCK EVENT: Ignored.") log("DOCK EVENT: Ignored.")
return return
@@ -364,7 +368,6 @@ function obj.showMenu()
local min = math.floor(obj.saveCountdown / 60) local min = math.floor(obj.saveCountdown / 60)
local sec = obj.saveCountdown % 60 local sec = obj.saveCountdown % 60
-- 1. Base Core Operational Rows
local choices = { local choices = {
{ {
text = string.format("⏱️ Next Autosave: %d:%02d", min, sec), text = string.format("⏱️ Next Autosave: %d:%02d", min, sec),
@@ -393,11 +396,9 @@ function obj.showMenu()
} }
} }
-- 2. Dynamically read layout file and append saved windows
local data = hs.json.read(obj.layoutFile) local data = hs.json.read(obj.layoutFile)
if data and data.windows and #data.windows > 0 then if data and data.windows and #data.windows > 0 then
for _, win in ipairs(data.windows) do for _, win in ipairs(data.windows) do
-- Verify if the application is currently running on the system server
local isRunning = hs.application.get(win.bundleID) or hs.application.get(win.appName) local isRunning = hs.application.get(win.bundleID) or hs.application.get(win.appName)
local statusIndicator = isRunning and "🟢" or "🔴" local statusIndicator = isRunning and "🟢" or "🔴"
@@ -438,7 +439,6 @@ function obj.showMenu()
obj.instanceChooser:show() obj.instanceChooser:show()
end end
-- Export module instance globally for direct IPC command routing
WindowManager = obj WindowManager = obj
return obj return obj
@@ -1,70 +0,0 @@
{
"screenCount" : 3,
"windows" : [
{
"x" : 0,
"winTitle" : "ASUS Wireless Router GT-AX11000 - VPN Status - Google Chrome",
"y" : 30,
"appName" : "Google Chrome",
"h" : 957,
"w" : 1920,
"bundleID" : "com.google.Chrome"
},
{
"x" : 2883,
"winTitle" : "francop — -zsh — 120×30",
"y" : 25,
"appName" : "Terminal",
"h" : 499,
"w" : 860,
"bundleID" : "com.apple.Terminal"
},
{
"x" : 2879,
"winTitle" : "raycast-scripts — -zsh — 120×30",
"y" : 528,
"appName" : "Terminal",
"h" : 499,
"w" : 860,
"bundleID" : "com.apple.Terminal"
},
{
"x" : -862,
"winTitle" : "raycast-scripts — francop@nextcloud: \/mnt\/docs\/Consume — -zsh — 120×30",
"y" : 65,
"appName" : "Terminal",
"h" : 499,
"w" : 860,
"bundleID" : "com.apple.Terminal"
},
{
"x" : 1920,
"winTitle" : "AFFiNE",
"y" : 0,
"appName" : "AFFiNE",
"h" : 1080,
"w" : 960,
"bundleID" : "pro.affine.app"
},
{
"x" : 0,
"winTitle" : "LayoutSelector.lua — .hammerspoon",
"y" : 30,
"appName" : "Code",
"h" : 957,
"w" : 1920,
"bundleID" : "com.microsoft.VSCode"
},
{
"x" : 453,
"winTitle" : "Software Update",
"y" : 177,
"appName" : "System Settings",
"h" : 671,
"w" : 723,
"bundleID" : "com.apple.systempreferences"
}
],
"saveTime" : "2026-05-17 17:58:50",
"mode" : "Docked"
}
+14 -14
View File
@@ -3,50 +3,50 @@
{ {
"x" : 1920, "x" : 1920,
"winTitle" : "AFFiNE", "winTitle" : "AFFiNE",
"y" : 0,
"appName" : "AFFiNE", "appName" : "AFFiNE",
"y" : 0,
"h" : 1080, "h" : 1080,
"w" : 960, "w" : 960,
"bundleID" : "pro.affine.app" "bundleID" : "pro.affine.app"
}, },
{ {
"x" : 0, "x" : 0,
"winTitle" : "TrueNAS - 192.168.1.135 - Google Chrome", "winTitle" : "[D] Coding Workspace.json (Working Tree) ([D] Coding Workspace.json) — .hammerspoon",
"appName" : "Code",
"y" : 30, "y" : 30,
"appName" : "Google Chrome", "h" : 956,
"h" : 957,
"w" : 1920, "w" : 1920,
"bundleID" : "com.google.Chrome" "bundleID" : "com.microsoft.VSCode"
}, },
{ {
"x" : 2880, "x" : 2880,
"winTitle" : "francop — -zsh — 134×33", "winTitle" : "francop — francop@media: \/mnt\/media\/Movies — -zsh — 134×33",
"y" : 536,
"appName" : "Terminal", "appName" : "Terminal",
"y" : 536,
"h" : 544, "h" : 544,
"w" : 958, "w" : 958,
"bundleID" : "com.apple.Terminal" "bundleID" : "com.apple.Terminal"
}, },
{ {
"x" : 2880, "x" : 2880,
"winTitle" : "francop — tail -f \/tmp\/litellm.out.log — 134×33", "winTitle" : "raycast-scripts — francop@nextcloud: \/mnt\/docs\/Consume — -zsh — 134×33",
"y" : 0,
"appName" : "Terminal", "appName" : "Terminal",
"y" : 0,
"h" : 544, "h" : 544,
"w" : 958, "w" : 958,
"bundleID" : "com.apple.Terminal" "bundleID" : "com.apple.Terminal"
}, },
{ {
"x" : 0, "x" : 0,
"winTitle" : "WindowManager.lua — .hammerspoon", "winTitle" : "anker desk outlet - Google Search - Google Chrome",
"appName" : "Google Chrome",
"y" : 30, "y" : 30,
"appName" : "Code", "h" : 957,
"h" : 956,
"w" : 1920, "w" : 1920,
"bundleID" : "com.microsoft.VSCode" "bundleID" : "com.google.Chrome"
} }
], ],
"screenCount" : 3, "screenCount" : 3,
"saveTime" : "2026-05-08 23:19:07", "saveTime" : "2026-05-24 14:34:05",
"mode" : "Docked" "mode" : "Docked"
} }