Compare commits
6 Commits
75200ff664
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5af469ae3e | |||
| d0e69a03fa | |||
| e7a9ccde93 | |||
| e57eb09567 | |||
| 734b291dd6 | |||
| a710b6c5de |
+8
-3
@@ -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
|
||||||
|
|||||||
+20
-14
@@ -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
|
||||||
@@ -211,8 +212,8 @@ function obj.rescueWindowsToLaptop()
|
|||||||
if f.w > maxFrame.w then f.w = maxFrame.w - 100 end
|
if f.w > maxFrame.w then f.w = maxFrame.w - 100 end
|
||||||
if f.h > maxFrame.h then f.h = maxFrame.h - 100 end
|
if f.h > maxFrame.h then f.h = maxFrame.h - 100 end
|
||||||
|
|
||||||
f.x = maxFrame.x + 50 + staggerOffset
|
f.x = maxFrame.x + staggerOffset
|
||||||
f.y = maxFrame.y + 50 + staggerOffset
|
f.y = maxFrame.y + staggerOffset
|
||||||
|
|
||||||
win:setFrame(f, 0)
|
win:setFrame(f, 0)
|
||||||
staggerOffset = staggerOffset + 30
|
staggerOffset = staggerOffset + 30
|
||||||
@@ -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,15 +270,16 @@ 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
|
||||||
log("POWER: Wake event.")
|
log("POWER: Wake event.")
|
||||||
obj.saveCountdown = obj.saveInterval
|
obj.saveCountdown = obj.saveInterval
|
||||||
obj.isMenuUpdating = false -- Explicitly clean guard flag upon waking up
|
obj.isMenuUpdating = false
|
||||||
|
obj.isTransitioning = 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
|
||||||
@@ -286,17 +287,21 @@ 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
|
||||||
log("WAKE SKIP: Single screen detected. Syncing count only.")
|
if obj.lastScreenCount > 1 then
|
||||||
obj.isTransitioning = false
|
log("WAKE ACTIVATE: Screen count dropped from " .. obj.lastScreenCount .. " to 1 during sleep. Running rescue.")
|
||||||
obj.isRestoring = false
|
obj.rescueWindowsToLaptop()
|
||||||
|
else
|
||||||
|
log("WAKE SKIP: Woke up on single screen, matched previous state. No rescue needed.")
|
||||||
|
end
|
||||||
obj.lastScreenCount = currentScreens
|
obj.lastScreenCount = currentScreens
|
||||||
updateMenu()
|
updateMenu()
|
||||||
end
|
end
|
||||||
@@ -308,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
|
||||||
@@ -358,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),
|
||||||
@@ -387,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 "🔴"
|
||||||
|
|
||||||
@@ -432,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
|
||||||
+2
-2
@@ -5,9 +5,9 @@
|
|||||||
"dimAlpha": 0.3
|
"dimAlpha": 0.3
|
||||||
},
|
},
|
||||||
"Home": {
|
"Home": {
|
||||||
"keepOpen": ["Discord", "Steam", "Komga", "Kavita", "Plex"],
|
"keepOpen": ["Code", "Spark", "Spotify"],
|
||||||
"close": ["Slack", "Microsoft Teams"],
|
"close": ["Slack", "Microsoft Teams"],
|
||||||
"dimAlpha": 0.1
|
"dimAlpha": 0.7
|
||||||
},
|
},
|
||||||
"Study": {
|
"Study": {
|
||||||
"keepOpen": ["AFFiNE", "Obsidian", "Anki", "Preview", "Safari"],
|
"keepOpen": ["AFFiNE", "Obsidian", "Anki", "Preview", "Safari"],
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
{
|
|
||||||
"windows" : [
|
|
||||||
{
|
|
||||||
"x" : 0,
|
|
||||||
"winTitle" : "francop - Gitea: Git with a cup of tea - Google Chrome",
|
|
||||||
"y" : 30,
|
|
||||||
"appName" : "Google Chrome",
|
|
||||||
"h" : 957,
|
|
||||||
"w" : 1920,
|
|
||||||
"bundleID" : "com.google.Chrome"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x" : 2883,
|
|
||||||
"winTitle" : "raycast-scripts — -zsh — 120×30",
|
|
||||||
"y" : 25,
|
|
||||||
"appName" : "Terminal",
|
|
||||||
"h" : 499,
|
|
||||||
"w" : 860,
|
|
||||||
"bundleID" : "com.apple.Terminal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x" : 2879,
|
|
||||||
"winTitle" : "francop — -zsh — 120×30",
|
|
||||||
"y" : 528,
|
|
||||||
"appName" : "Terminal",
|
|
||||||
"h" : 499,
|
|
||||||
"w" : 860,
|
|
||||||
"bundleID" : "com.apple.Terminal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x" : 2670,
|
|
||||||
"winTitle" : "lab-status.py — raycast-scripts — Untracked",
|
|
||||||
"y" : 366,
|
|
||||||
"appName" : "Code",
|
|
||||||
"h" : 714,
|
|
||||||
"w" : 1170,
|
|
||||||
"bundleID" : "com.microsoft.VSCode"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x" : 0,
|
|
||||||
"winTitle" : "mouseJiggle.lua — .hammerspoon — Modified",
|
|
||||||
"y" : 30,
|
|
||||||
"appName" : "Code",
|
|
||||||
"h" : 956,
|
|
||||||
"w" : 1920,
|
|
||||||
"bundleID" : "com.microsoft.VSCode"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x" : 1920,
|
|
||||||
"winTitle" : "AFFiNE",
|
|
||||||
"y" : 0,
|
|
||||||
"appName" : "AFFiNE",
|
|
||||||
"h" : 1080,
|
|
||||||
"w" : 960,
|
|
||||||
"bundleID" : "pro.affine.app"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x" : 392,
|
|
||||||
"winTitle" : "Bluetooth",
|
|
||||||
"y" : 168,
|
|
||||||
"appName" : "System Settings",
|
|
||||||
"h" : 671,
|
|
||||||
"w" : 723,
|
|
||||||
"bundleID" : "com.apple.systempreferences"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"screenCount" : 3,
|
|
||||||
"saveTime" : "2026-05-16 04:21:57",
|
|
||||||
"mode" : "Docked"
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user