Compare commits
20 Commits
6a0a954e7e
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5af469ae3e | |||
| d0e69a03fa | |||
| e7a9ccde93 | |||
| e57eb09567 | |||
| 734b291dd6 | |||
| a710b6c5de | |||
| 75200ff664 | |||
| 73e5617176 | |||
| c9d516c6fd | |||
| efadc1e218 | |||
| 07226bfa7b | |||
| 21580ae0d6 | |||
| 86f8072d1b | |||
| 3c9c03b64b | |||
| 11ed9b80bc | |||
| c3308429ff | |||
| 4b1d6808c9 | |||
| 940ed913aa | |||
| 1990c2f998 | |||
| e16532cf1b |
Vendored
BIN
Binary file not shown.
+38
-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
|
||||||
@@ -235,9 +240,39 @@ function selector.showLayoutActions(layoutName)
|
|||||||
text = "🗑️ Delete Layout Profile",
|
text = "🗑️ Delete Layout Profile",
|
||||||
subText = "Permanently remove the profile config file from disk",
|
subText = "Permanently remove the profile config file from disk",
|
||||||
action = "delete"
|
action = "delete"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "───────────────────────────────────────────────────────",
|
||||||
|
subText = "📦 Captured Windows Inside Profile State File:",
|
||||||
|
action = "info"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Dynamically read layout profile file and append saved windows
|
||||||
|
local data = hs.json.read(path)
|
||||||
|
if data and data.windows and #data.windows > 0 then
|
||||||
|
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 statusIndicator = isRunning and "🟢" or "🔴"
|
||||||
|
|
||||||
|
local cleanTitle = (win.winTitle and win.winTitle ~= "") and win.winTitle or "Untitled Window"
|
||||||
|
if #cleanTitle > 60 then cleanTitle = string.sub(cleanTitle, 1, 57) .. "..." end
|
||||||
|
|
||||||
|
table.insert(actionChoices, {
|
||||||
|
text = string.format("%s %s", statusIndicator, win.appName),
|
||||||
|
subText = string.format("↳ Title: \"%s\" | Bounds: %dx%d at (%d,%d)", cleanTitle, win.w, win.h, win.x, win.y),
|
||||||
|
action = "info"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(actionChoices, {
|
||||||
|
text = "⚠️ No Saved Window Metrics Found",
|
||||||
|
subText = "This layout profile does not contain any captured windows.",
|
||||||
|
action = "info"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
if selector.actionChooser then selector.actionChooser:hide() end
|
if selector.actionChooser then selector.actionChooser:hide() end
|
||||||
|
|
||||||
selector.actionChooser = hs.chooser.new(function(choice)
|
selector.actionChooser = hs.chooser.new(function(choice)
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+58
-7
@@ -34,9 +34,11 @@ obj.saveCountdown = obj.saveInterval
|
|||||||
obj.isRescued = false
|
obj.isRescued = false
|
||||||
obj.isTransitioning = false
|
obj.isTransitioning = false
|
||||||
obj.isRestoring = false
|
obj.isRestoring = false
|
||||||
|
obj.isMenuUpdating = false -- Safety guard to prevent concurrent IPC menu rendering loops
|
||||||
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
|
||||||
@@ -210,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
|
||||||
@@ -231,6 +233,9 @@ end
|
|||||||
local timerMenu = hs.menubar.new()
|
local timerMenu = hs.menubar.new()
|
||||||
|
|
||||||
function updateMenu()
|
function updateMenu()
|
||||||
|
if obj.isMenuUpdating then return end
|
||||||
|
obj.isMenuUpdating = true
|
||||||
|
|
||||||
if timerMenu then
|
if timerMenu then
|
||||||
local screens = hs.screen.allScreens()
|
local screens = hs.screen.allScreens()
|
||||||
timerMenu:setTitle(string.format("💠 %d:%02d", math.floor(obj.saveCountdown / 60), obj.saveCountdown % 60))
|
timerMenu:setTitle(string.format("💠 %d:%02d", math.floor(obj.saveCountdown / 60), obj.saveCountdown % 60))
|
||||||
@@ -257,6 +262,8 @@ function updateMenu()
|
|||||||
end
|
end
|
||||||
timerMenu:setMenu(menuTable)
|
timerMenu:setMenu(menuTable)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
obj.isMenuUpdating = false
|
||||||
end
|
end
|
||||||
|
|
||||||
obj.powerWatcher = hs.caffeinate.watcher.new(function(event)
|
obj.powerWatcher = hs.caffeinate.watcher.new(function(event)
|
||||||
@@ -264,27 +271,39 @@ obj.powerWatcher = hs.caffeinate.watcher.new(function(event)
|
|||||||
log("POWER: Sleep event.")
|
log("POWER: Sleep event.")
|
||||||
obj.isTransitioning = true
|
obj.isTransitioning = true
|
||||||
if obj.autoSaveTimer then obj.autoSaveTimer:stop() end
|
if obj.autoSaveTimer then obj.autoSaveTimer: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
|
||||||
|
obj.isTransitioning = false
|
||||||
|
obj.isRestoring = false
|
||||||
|
obj.wakeTimestamp = os.time()
|
||||||
|
|
||||||
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.wakeTimer then obj.wakeTimer:stop() end
|
if obj.wakeTimer then obj.wakeTimer:stop() end
|
||||||
|
|
||||||
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()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end):start()
|
end):start()
|
||||||
@@ -294,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,9 +388,37 @@ function obj.showMenu()
|
|||||||
text = "🚀 Rescue Windows",
|
text = "🚀 Rescue Windows",
|
||||||
subText = "Cascade active window threads onto primary laptop screen space",
|
subText = "Cascade active window threads onto primary laptop screen space",
|
||||||
action = "rescue"
|
action = "rescue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "───────────────────────────────────────────────────────",
|
||||||
|
subText = "📦 Captured Windows Inside Profile State File:",
|
||||||
|
action = "info"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local data = hs.json.read(obj.layoutFile)
|
||||||
|
if data and data.windows and #data.windows > 0 then
|
||||||
|
for _, win in ipairs(data.windows) do
|
||||||
|
local isRunning = hs.application.get(win.bundleID) or hs.application.get(win.appName)
|
||||||
|
local statusIndicator = isRunning and "🟢" or "🔴"
|
||||||
|
|
||||||
|
local cleanTitle = (win.winTitle and win.winTitle ~= "") and win.winTitle or "Untitled Window"
|
||||||
|
if #cleanTitle > 60 then cleanTitle = string.sub(cleanTitle, 1, 57) .. "..." end
|
||||||
|
|
||||||
|
table.insert(choices, {
|
||||||
|
text = string.format("%s %s", statusIndicator, win.appName),
|
||||||
|
subText = string.format("↳ Title: \"%s\" | Bounds: %dx%d at (%d,%d)", cleanTitle, win.w, win.h, win.x, win.y),
|
||||||
|
action = "info"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(choices, {
|
||||||
|
text = "⚠️ No Saved Window Metrics Found",
|
||||||
|
subText = "Run a Save operation to record your desktop layout profile context.",
|
||||||
|
action = "info"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
if obj.instanceChooser then obj.instanceChooser:hide() end
|
if obj.instanceChooser then obj.instanceChooser:hide() end
|
||||||
|
|
||||||
obj.instanceChooser = hs.chooser.new(function(choice)
|
obj.instanceChooser = hs.chooser.new(function(choice)
|
||||||
@@ -387,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
-1
@@ -28,6 +28,7 @@
|
|||||||
"TypeWhisper": true,
|
"TypeWhisper": true,
|
||||||
"com.typewhisper.mac": true,
|
"com.typewhisper.mac": true,
|
||||||
"dk.heyiam.monocle": true,
|
"dk.heyiam.monocle": true,
|
||||||
"Monocle": true
|
"Monocle": true,
|
||||||
|
"com.privdev.Focusdim": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"Work": {
|
||||||
|
"keepOpen": ["Slack", "Microsoft Teams", "Visual Studio Code", "Terminal", "Google Chrome"],
|
||||||
|
"close": ["Discord", "Steam", "Komga", "Kavita"],
|
||||||
|
"dimAlpha": 0.3
|
||||||
|
},
|
||||||
|
"Home": {
|
||||||
|
"keepOpen": ["Code", "Spark", "Spotify"],
|
||||||
|
"close": ["Slack", "Microsoft Teams"],
|
||||||
|
"dimAlpha": 0.7
|
||||||
|
},
|
||||||
|
"Study": {
|
||||||
|
"keepOpen": ["AFFiNE", "Obsidian", "Anki", "Preview", "Safari"],
|
||||||
|
"close": ["Slack", "Microsoft Teams", "Discord", "Steam", "Instagram"],
|
||||||
|
"dimAlpha": 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,105 +1,53 @@
|
|||||||
-- ========================================================================
|
-- ========================================================================
|
||||||
-- HEADLESS WORKSPACE BACKGROUND ENGINE (FORCED AT BOOT)
|
-- HEADLESS WORKSPACE BACKGROUND ENGINE (FORCED AT BOOT)
|
||||||
-- ========================================================================
|
-- ========================================================================
|
||||||
|
hs.allowAppleScript(true)
|
||||||
hs.menuIcon(false)
|
hs.menuIcon(false)
|
||||||
hs.dockIcon(false)
|
hs.dockIcon(false)
|
||||||
hs.ipc.cliInstall("/opt/homebrew")
|
hs.ipc.cliInstall("/opt/homebrew")
|
||||||
hs.alert.show("Hammerspoon Headless Daemon Active", 2)
|
hs.alert.show("Hammerspoon Headless Daemon Active", 2)
|
||||||
|
|
||||||
-- ~/.hammerspoon/init.lua
|
-- Core Subsystems and Global Bindings
|
||||||
-- Load Config First
|
|
||||||
-- config = require("Config")
|
|
||||||
--
|
|
||||||
-- Load your HyperKey
|
|
||||||
require("HyperKey")
|
require("HyperKey")
|
||||||
|
|
||||||
require('SearchWindows')
|
require('SearchWindows')
|
||||||
require('Caffeine')
|
require('Caffeine')
|
||||||
require('AppBorders')
|
require('AppBorders')
|
||||||
|
|
||||||
-- CRITICAL FOR RAYCAST INTERACTION: Bind the return value to a global variable
|
-- IPC Bridges & Trackers
|
||||||
LayoutSelector = require('LayoutSelector')
|
LayoutSelector = require('LayoutSelector')
|
||||||
|
|
||||||
-- require('System_Tweaks') -- Used for Time Machine Throttle Disable
|
-- Active Modules
|
||||||
-- require("Focus") -- Does not work with layout saver - Not needed if using Monocle
|
require("modules.mouseJiggle").start()
|
||||||
require("FocusMode")
|
|
||||||
Network = require("NetworkCenter")
|
|
||||||
|
|
||||||
-- Load the window management module
|
|
||||||
local windowMgr = require("WindowManager")
|
local windowMgr = require("WindowManager")
|
||||||
local productivity = require("productivity")
|
local productivity = require("productivity")
|
||||||
|
|
||||||
-- For Affine
|
-- DEEP FOCUS MODULE (New Integration)
|
||||||
|
local focus = require("modules.focus")
|
||||||
|
|
||||||
|
-- Affine Note Engine Integration
|
||||||
local quickNote = require("affine_quick_note")
|
local quickNote = require("affine_quick_note")
|
||||||
quickNote.init()
|
quickNote.init()
|
||||||
require("affine_clipper"):init()
|
require("affine_clipper"):init()
|
||||||
|
|
||||||
-- Load Spoon Files
|
-- Spoons Engine & External Tool Integrations
|
||||||
hs.loadSpoon('SpoonInstall')
|
hs.loadSpoon('SpoonInstall')
|
||||||
hs.loadSpoon('SpeedMenu')
|
hs.loadSpoon('SpeedMenu')
|
||||||
hs.loadSpoon('BrewInfo')
|
hs.loadSpoon('BrewInfo')
|
||||||
|
|
||||||
-- ==========================================
|
-- Run isolated Spoon setup configurations
|
||||||
-- SPOON CONFIGURATION
|
require("modules.spoon_config")
|
||||||
-- ==========================================
|
|
||||||
|
|
||||||
---- SpeedMenu Config
|
-- ========================================================================
|
||||||
if spoon.SpeedMenu then
|
-- RAYCAST / INTER-PROCESS COMMUNICATION (IPC) BRIDGE BINDINGS
|
||||||
-- Define the Fix Function (includes MAC and IPv6)
|
-- ========================================================================
|
||||||
local function applyFullMenuFix()
|
-- Expose the focus module methods globally so the hs CLI tool can call them
|
||||||
local interface = spoon.SpeedMenu.interface or "en0"
|
function ActivateDeepFocus(profileName)
|
||||||
local ssid = hs.wifi.currentNetwork() or "Disconnected"
|
return focus.activate(profileName)
|
||||||
local details = hs.network.interfaceDetails(interface)
|
|
||||||
|
|
||||||
local ipv4 = (details and details.IPv4) and details.IPv4.Addresses[1] or "N/A"
|
|
||||||
local ipv6 = (details and details.IPv6) and details.IPv6.Addresses[1] or "N/A"
|
|
||||||
|
|
||||||
-- Get MAC Address via shell
|
|
||||||
local macaddr = hs.execute('ifconfig ' .. interface .. ' | grep ether | awk \'{print $2}\''):gsub("%s+", "")
|
|
||||||
|
|
||||||
local menuitems = {
|
|
||||||
{ title = "SSID: " .. ssid, fn = function() hs.pasteboard.setContents(ssid) end },
|
|
||||||
{ title = "IPv4: " .. ipv4, fn = function() hs.pasteboard.setContents(ipv4) end },
|
|
||||||
{ title = "IPv6: " .. ipv6, fn = function() hs.pasteboard.setContents(ipv6) end },
|
|
||||||
{ title = "MAC: " .. macaddr, fn = function() hs.pasteboard.setContents(macaddr) end },
|
|
||||||
{ title = "-" },
|
|
||||||
{ title = "Rescan Network Interfaces", fn = function() spoon.SpeedMenu:rescan() end }
|
|
||||||
}
|
|
||||||
spoon.SpeedMenu.menubar:setMenu(menuitems)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Hook the rescan method
|
function ClearDeepFocus()
|
||||||
local oldRescan = spoon.SpeedMenu.rescan
|
return focus.clear()
|
||||||
spoon.SpeedMenu.rescan = function(self)
|
|
||||||
oldRescan(self)
|
|
||||||
applyFullMenuFix()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Toggle Logic (Starts as OFF)
|
-- Boot Confirmation
|
||||||
local speedMenuRunning = false
|
|
||||||
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function()
|
|
||||||
if speedMenuRunning then
|
|
||||||
spoon.SpeedMenu:stop()
|
|
||||||
hs.alert.show("SpeedMenu Stopped")
|
|
||||||
else
|
|
||||||
spoon.SpeedMenu:start() -- This puts it in the menu bar
|
|
||||||
applyFullMenuFix() -- This populates the data
|
|
||||||
hs.alert.show("SpeedMenu Started")
|
|
||||||
end
|
|
||||||
speedMenuRunning = not speedMenuRunning
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
---- SpeedMenu Config END
|
|
||||||
|
|
||||||
---- BrewInfo
|
|
||||||
if spoon.BrewInfo then
|
|
||||||
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "I", function()
|
|
||||||
spoon.BrewInfo:showBrewInfo()
|
|
||||||
end)
|
|
||||||
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "J", function()
|
|
||||||
spoon.BrewInfo:showBrewInfoCurSel()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
---- BrewInfo END
|
|
||||||
|
|
||||||
hs.alert.show("Hammerspoon Config Reloaded")
|
hs.alert.show("Hammerspoon Config Reloaded")
|
||||||
Vendored
BIN
Binary file not shown.
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
{
|
|
||||||
"screenCount" : 3,
|
|
||||||
"windows" : [
|
|
||||||
{
|
|
||||||
"x" : -952,
|
|
||||||
"bundleID" : "com.apple.finder",
|
|
||||||
"y" : 159,
|
|
||||||
"appName" : "Finder",
|
|
||||||
"h" : 492,
|
|
||||||
"w" : 920,
|
|
||||||
"winTitle" : "raycast-scripts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x" : 2070,
|
|
||||||
"bundleID" : "com.raycast.macos",
|
|
||||||
"y" : 121,
|
|
||||||
"appName" : "Raycast",
|
|
||||||
"h" : 319,
|
|
||||||
"w" : 1000,
|
|
||||||
"winTitle" : "Settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x" : 1986,
|
|
||||||
"bundleID" : "com.google.GeminiMacOS",
|
|
||||||
"y" : 110,
|
|
||||||
"appName" : "Gemini",
|
|
||||||
"h" : 536,
|
|
||||||
"w" : 1002,
|
|
||||||
"winTitle" : "Gemini — Hammerspoon Menu Bar Icon Troubleshooting"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x" : 0,
|
|
||||||
"bundleID" : "com.google.Chrome",
|
|
||||||
"y" : 39,
|
|
||||||
"appName" : "Google Chrome",
|
|
||||||
"h" : 957,
|
|
||||||
"w" : 1920,
|
|
||||||
"winTitle" : "francop - Dashboard - Gitea: Git with a cup of tea - Google Chrome"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x" : 2794,
|
|
||||||
"bundleID" : "com.apple.Terminal",
|
|
||||||
"y" : 131,
|
|
||||||
"appName" : "Terminal",
|
|
||||||
"h" : 499,
|
|
||||||
"w" : 860,
|
|
||||||
"winTitle" : "francop — -zsh — 120×30"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x" : 0,
|
|
||||||
"bundleID" : "com.microsoft.VSCode",
|
|
||||||
"y" : 30,
|
|
||||||
"appName" : "Code",
|
|
||||||
"h" : 957,
|
|
||||||
"w" : 1920,
|
|
||||||
"winTitle" : "init.lua — .hammerspoon"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"saveTime" : "2026-05-15 22:26:48",
|
|
||||||
"mode" : "Docked"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
local focus = require("modules.focus")
|
||||||
|
|
||||||
|
-- Expose clean functions globally for the hs CLI tool
|
||||||
|
function ActivateDeepFocus(profileName)
|
||||||
|
return focus.activate(profileName)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClearDeepFocus()
|
||||||
|
return focus.clear()
|
||||||
|
end
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
local M = {}
|
||||||
|
|
||||||
|
local focusCanvas = nil
|
||||||
|
local activeProfile = nil
|
||||||
|
local configPath = hs.configdir .. "/focus_profiles.json"
|
||||||
|
|
||||||
|
local function loadProfiles()
|
||||||
|
local file = io.open(configPath, "r")
|
||||||
|
if not file then
|
||||||
|
print("Error: Could not open focus_profiles.json")
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local content = file:read("*a")
|
||||||
|
file:close()
|
||||||
|
return hs.json.decode(content)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function manageApps(profileConfig)
|
||||||
|
for _, appName in ipairs(profileConfig.close) do
|
||||||
|
local app = hs.application.get(appName)
|
||||||
|
if app then app:kill() end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, appName in ipairs(profileConfig.keepOpen) do
|
||||||
|
hs.application.launchOrFocus(appName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function setFocusOverlay(alpha)
|
||||||
|
if focusCanvas then
|
||||||
|
focusCanvas:delete()
|
||||||
|
focusCanvas = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if not alpha or alpha == 0 then return end
|
||||||
|
|
||||||
|
local mainScreen = hs.screen.mainScreen()
|
||||||
|
local rect = mainScreen:frame()
|
||||||
|
|
||||||
|
focusCanvas = hs.canvas.new(rect)
|
||||||
|
focusCanvas:insertElement({
|
||||||
|
action = "fill",
|
||||||
|
type = "rectangle",
|
||||||
|
fillColor = { red = 0, green = 0, blue = 0, alpha = alpha }
|
||||||
|
})
|
||||||
|
|
||||||
|
focusCanvas:level(50)
|
||||||
|
focusCanvas:show()
|
||||||
|
|
||||||
|
local frontApp = hs.application.frontmostApplication()
|
||||||
|
if frontApp then
|
||||||
|
local win = frontApp:mainWindow()
|
||||||
|
if win then win:focus() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.activate(profileName)
|
||||||
|
local profiles = loadProfiles()
|
||||||
|
if not profiles or not profiles[profileName] then
|
||||||
|
print("Error: Profile '" .. tostring(profileName) .. "' not found.")
|
||||||
|
return "Profile not found"
|
||||||
|
end
|
||||||
|
|
||||||
|
activeProfile = profileName
|
||||||
|
local currentProfileConfig = profiles[profileName]
|
||||||
|
|
||||||
|
manageApps(currentProfileConfig)
|
||||||
|
setFocusOverlay(currentProfileConfig.dimAlpha)
|
||||||
|
|
||||||
|
return "Activated " .. profileName .. " mode"
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.clear()
|
||||||
|
setFocusOverlay(nil)
|
||||||
|
activeProfile = nil
|
||||||
|
return "Focus mode cleared"
|
||||||
|
end
|
||||||
|
|
||||||
|
hs.screen.watcher.new(function()
|
||||||
|
if activeProfile then
|
||||||
|
local profiles = loadProfiles()
|
||||||
|
if profiles and profiles[activeProfile] then
|
||||||
|
setFocusOverlay(profiles[activeProfile].dimAlpha)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end):start()
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
local M = {}
|
||||||
|
|
||||||
|
-- Configuration
|
||||||
|
local REQUIRED_REVERSALS = 3 -- Must switch direction 3 times (e.g., Left -> Right -> Left)
|
||||||
|
local RESET_TIMEOUT = 0.3 -- Fast window: shake must be completed rapidly or it resets
|
||||||
|
local LOCKOUT_DURATION = 3.0 -- Cooldown duration (seconds) before allowed to trigger again
|
||||||
|
|
||||||
|
-- Asymmetrical Thresholds
|
||||||
|
local SPEED_TO_ENABLE = 115.0 -- Slightly lower: easier to turn ON
|
||||||
|
local SPEED_TO_DISABLE = 140.0 -- Slightly higher: harder to accidentally turn OFF
|
||||||
|
|
||||||
|
-- Focusdim Global Hotkey Configuration from your settings
|
||||||
|
local FOCUS_DIM_MODS = { "ctrl", "alt", "shift", "cmd" }
|
||||||
|
local FOCUS_DIM_KEY = "f"
|
||||||
|
|
||||||
|
-- Internal state tracking
|
||||||
|
local jiggleWatcher = nil
|
||||||
|
local isLocked = false
|
||||||
|
local isFocusDimActive = false -- Tracks state to apply the correct threshold
|
||||||
|
local lastXDirection = 0 -- -1 for left, 1 for right, 0 for still
|
||||||
|
local reversalCount = 0
|
||||||
|
local lastEventTime = 0
|
||||||
|
|
||||||
|
-- Forward declaration for the timer loop
|
||||||
|
M.start = function() end
|
||||||
|
|
||||||
|
local function eventCallback(event)
|
||||||
|
if isLocked then return false end
|
||||||
|
|
||||||
|
local now = hs.timer.secondsSinceEpoch()
|
||||||
|
|
||||||
|
-- Reset the counter if the movements aren't happening fast enough
|
||||||
|
if (now - lastEventTime) > RESET_TIMEOUT then
|
||||||
|
reversalCount = 0
|
||||||
|
lastXDirection = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
lastEventTime = now
|
||||||
|
|
||||||
|
-- Extract horizontal movement exclusively
|
||||||
|
local dx = event:getProperty(hs.eventtap.event.properties.mouseEventDeltaX)
|
||||||
|
|
||||||
|
-- Dynamically choose threshold based on current state
|
||||||
|
local currentThreshold = isFocusDimActive and SPEED_TO_DISABLE or SPEED_TO_ENABLE
|
||||||
|
|
||||||
|
-- Only evaluate if it's a high-velocity horizontal snap matching the current state's rule
|
||||||
|
if math.abs(dx) > currentThreshold then
|
||||||
|
local currentDirection = (dx > 0) and 1 or -1
|
||||||
|
|
||||||
|
-- Track direction flips
|
||||||
|
if lastXDirection ~= 0 and currentDirection ~= lastXDirection then
|
||||||
|
reversalCount = reversalCount + 1
|
||||||
|
|
||||||
|
-- Trigger only when the full rapid shake sequence completes
|
||||||
|
if reversalCount >= REQUIRED_REVERSALS then
|
||||||
|
isLocked = true
|
||||||
|
reversalCount = 0
|
||||||
|
lastXDirection = 0
|
||||||
|
|
||||||
|
-- Toggle our internal tracking state
|
||||||
|
isFocusDimActive = not isFocusDimActive
|
||||||
|
|
||||||
|
-- Instantly detach the listener
|
||||||
|
M.stop()
|
||||||
|
|
||||||
|
-- Simulate the hotkey press to toggle Focusdim
|
||||||
|
hs.eventtap.keyStroke(FOCUS_DIM_MODS, FOCUS_DIM_KEY, 0)
|
||||||
|
|
||||||
|
-- Re-enable after the lockout duration
|
||||||
|
hs.timer.doAfter(LOCKOUT_DURATION, function()
|
||||||
|
isLocked = false
|
||||||
|
M.start()
|
||||||
|
end)
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
lastXDirection = currentDirection
|
||||||
|
end
|
||||||
|
|
||||||
|
return false -- Pass event through to macOS normally
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.start()
|
||||||
|
if jiggleWatcher then return end
|
||||||
|
|
||||||
|
jiggleWatcher = hs.eventtap.new({ hs.eventtap.event.types.mouseMoved }, eventCallback)
|
||||||
|
jiggleWatcher:start()
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.stop()
|
||||||
|
if jiggleWatcher then
|
||||||
|
jiggleWatcher:stop()
|
||||||
|
jiggleWatcher = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
-- ========================================================================
|
||||||
|
-- SPOON RUNTIME CONFIGURATION MODULE
|
||||||
|
-- ========================================================================
|
||||||
|
|
||||||
|
---- SpeedMenu Implementation
|
||||||
|
if spoon.SpeedMenu then
|
||||||
|
-- Define the Fix Function (includes MAC and IPv6 parsing via shell)
|
||||||
|
local function applyFullMenuFix()
|
||||||
|
local interface = spoon.SpeedMenu.interface or "en0"
|
||||||
|
local ssid = hs.wifi.currentNetwork() or "Disconnected"
|
||||||
|
local details = hs.network.interfaceDetails(interface)
|
||||||
|
|
||||||
|
local ipv4 = (details and details.IPv4) and details.IPv4.Addresses[1] or "N/A"
|
||||||
|
local ipv6 = (details and details.IPv6) and details.IPv6.Addresses[1] or "N/A"
|
||||||
|
|
||||||
|
-- Get MAC Address via native shell pipe
|
||||||
|
local macaddr = hs.execute('ifconfig ' .. interface .. ' | grep ether | awk \'{print $2}\''):gsub("%s+", "")
|
||||||
|
|
||||||
|
local menuitems = {
|
||||||
|
{ title = "SSID: " .. ssid, fn = function() hs.pasteboard.setContents(ssid) end },
|
||||||
|
{ title = "IPv4: " .. ipv4, fn = function() hs.pasteboard.setContents(ipv4) end },
|
||||||
|
{ title = "IPv6: " .. ipv6, fn = function() hs.pasteboard.setContents(ipv6) end },
|
||||||
|
{ title = "MAC: " .. macaddr, fn = function() hs.pasteboard.setContents(macaddr) end },
|
||||||
|
{ title = "-" },
|
||||||
|
{ title = "Rescan Network Interfaces", fn = function() spoon.SpeedMenu:rescan() end }
|
||||||
|
}
|
||||||
|
spoon.SpeedMenu.menubar:setMenu(menuitems)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Overwrite the baseline rescan hook cleanly
|
||||||
|
local oldRescan = spoon.SpeedMenu.rescan
|
||||||
|
spoon.SpeedMenu.rescan = function(self)
|
||||||
|
oldRescan(self)
|
||||||
|
applyFullMenuFix()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Toggle Engine State Manager
|
||||||
|
local speedMenuRunning = false
|
||||||
|
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function()
|
||||||
|
if speedMenuRunning then
|
||||||
|
spoon.SpeedMenu:stop()
|
||||||
|
hs.alert.show("SpeedMenu Stopped")
|
||||||
|
else
|
||||||
|
spoon.SpeedMenu:start()
|
||||||
|
applyFullMenuFix()
|
||||||
|
hs.alert.show("SpeedMenu Started")
|
||||||
|
end
|
||||||
|
speedMenuRunning = not speedMenuRunning
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---- BrewInfo Implementation
|
||||||
|
if spoon.BrewInfo then
|
||||||
|
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "I", function()
|
||||||
|
spoon.BrewInfo:showBrewInfo()
|
||||||
|
end)
|
||||||
|
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "J", function()
|
||||||
|
spoon.BrewInfo:showBrewInfoCurSel()
|
||||||
|
end)
|
||||||
|
end
|
||||||
Vendored
BIN
Binary file not shown.
Reference in New Issue
Block a user