Compare commits

...

16 Commits

15 changed files with 306 additions and 162 deletions
Vendored
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+38 -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
@@ -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)
BIN
View File
Binary file not shown.
+58 -7
View File
@@ -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
+17
View File
@@ -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
}
}
+20 -76
View File
@@ -1,109 +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 - Focusdim is better than Monocle
-- require("FocusMode") -- Removed since I am using Focusdim
--
-- Network Center is currently disabled since menus are not working in Hammerspoon - Will re-enable once I can fix the menu issues
-- Network = require("NetworkCenter")
--
require("modules.mouseJiggle").start() require("modules.mouseJiggle").start()
-- 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
-- Hook the rescan method
local oldRescan = spoon.SpeedMenu.rescan
spoon.SpeedMenu.rescan = function(self)
oldRescan(self)
applyFullMenuFix()
end
-- Toggle Logic (Starts as OFF)
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 end
---- SpeedMenu Config END
---- BrewInfo function ClearDeepFocus()
if spoon.BrewInfo then return focus.clear()
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 end
---- BrewInfo END
-- Boot Confirmation
hs.alert.show("Hammerspoon Config Reloaded") hs.alert.show("Hammerspoon Config Reloaded")
BIN
View File
Binary file not shown.
+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"
} }
-61
View File
@@ -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"
}
+10
View File
@@ -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
+88
View File
@@ -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
+1 -1
View File
@@ -7,7 +7,7 @@ local LOCKOUT_DURATION = 3.0 -- Cooldown duration (seconds) before allowed to
-- Asymmetrical Thresholds -- Asymmetrical Thresholds
local SPEED_TO_ENABLE = 115.0 -- Slightly lower: easier to turn ON local SPEED_TO_ENABLE = 115.0 -- Slightly lower: easier to turn ON
local SPEED_TO_DISABLE = 150.0 -- Slightly higher: harder to accidentally turn OFF local SPEED_TO_DISABLE = 140.0 -- Slightly higher: harder to accidentally turn OFF
-- Focusdim Global Hotkey Configuration from your settings -- Focusdim Global Hotkey Configuration from your settings
local FOCUS_DIM_MODS = { "ctrl", "alt", "shift", "cmd" } local FOCUS_DIM_MODS = { "ctrl", "alt", "shift", "cmd" }
+60
View File
@@ -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
BIN
View File
Binary file not shown.