-- ~/.hammerspoon/NetworkMenu.lua NetworkMenu = {} NetworkMenu.menuBarItem = hs.menubar.new() -- Global state management automationEnabled = (automationEnabled == nil) and true or automationEnabled local nasOnline = false local lastInternalIP = "Offline" local lastExternalIP = "Offline" local vpnActive = false -- 1. ASYNC STATUS UPDATE (Prevents hangs and crashes) function updateStatus() if not config then print("NetworkMenu Error: Config not found") return end -- A. Async NAS Ping hs.task.new("/sbin/ping", function(code) nasOnline = (code == 0) end, {"-c", "1", "-t", "1", config.nasIP}):start() -- B. Internal VPN IP (Fast local command) local internalCmd = "ifconfig | grep -A 1 'utun' | grep 'inet ' | awk '{print $2}' | head -n 1" local internalIP = hs.execute(internalCmd):gsub("%s+", "") lastInternalIP = (internalIP == "" and "Offline" or internalIP) vpnActive = (internalIP ~= "") -- C. Async External IP hs.task.new("/usr/bin/curl", function(exitCode, stdOut) if exitCode == 0 and stdOut then lastExternalIP = stdOut:gsub("%s+", "") else lastExternalIP = "Offline" end end, {"-s", "-m", "2", "icanhazip.com"}):start() -- D. Update Menu Icon local currentSSID = hs.wifi.currentNetwork() if NetworkMenu.menuBarItem then local automationIcon = automationEnabled and "" or "🔘" local icon = (currentSSID == config.homeSSID) and "🏠" or "🌐" if vpnActive then icon = icon .. "🛡️" end if nasOnline then icon = icon .. "🟢" end NetworkMenu.menuBarItem:setTitle(automationIcon .. icon) end end -- 2. Aggressive Backgrounding for OpenVPN function NetworkMenu.deepHideOpenVPN() local count = 0 local hideTimer hideTimer = hs.timer.doEvery(0.1, function() local app = hs.application.get("OpenVPN Connect") if app then app:hide() hs.applescript.applescript('tell application "System Events" to set visible of process "OpenVPN Connect" to false') end count = count + 1 if count > 30 then hideTimer:stop() end end) end -- 3. Check Share Status (Local Filesystem Check) local function getShareStatus() local statusLines = {} if not config or not config.shares then return statusLines end for _, share in ipairs(config.shares) do local path = "/Volumes/" .. share local isMounted = hs.fs.attributes(path) ~= nil local icon = isMounted and "🟢" or "⚪️" table.insert(statusLines, { title = " " .. icon .. " " .. share, disabled = true }) end return statusLines end -- 4. THE MENU UI function NetworkMenu.buildMenu() local currentSSID = hs.wifi.currentNetwork() or "Disconnected" local isHome = config and (currentSSID == config.homeSSID) local menu = { { title = (automationEnabled and "🟢" or "🔴") .. " Automation: " .. (automationEnabled and "Enabled" or "Disabled"), fn = function() automationEnabled = not automationEnabled updateStatus() hs.alert.show("Network Automation: " .. (automationEnabled and "ON" or "OFF")) end }, { title = "-" }, { title = "📍 Profile: " .. (isHome and "Home" or "Remote"), disabled = true }, { title = " " .. (isHome and "🏠" or "🌐") .. " Location: " .. currentSSID, disabled = true }, { title = "-" }, { title = (vpnActive and "🔒" or "🔓") .. " VPN: " .. (vpnActive and "Connected" or "Disconnected"), disabled = true }, { title = " 🆔 Int IP: " .. lastInternalIP, disabled = true }, { title = " 🌍 Ext IP: " .. lastExternalIP, disabled = true }, { title = (nasOnline and "✅" or "❌") .. " NAS Status: " .. (nasOnline and "Online" or "Offline"), disabled = true }, { title = " 📂 Mounted Shares:", disabled = true }, } local shares = getShareStatus() for _, s in ipairs(shares) do table.insert(menu, s) end table.insert(menu, { title = "-" }) table.insert(menu, { title = "⚡️ VPN Controls:", disabled = true }) table.insert(menu, { title = " 🔒 Connect VPN", fn = function() if config then hs.execute(string.format('"%s" --connect-shortcut=%s', config.openvpnPath, config.vpnProfileID)) NetworkMenu.deepHideOpenVPN() hs.timer.doAfter(6, updateStatus) end end }) table.insert(menu, { title = " 🔓 Disconnect & Quit VPN", fn = function() if config then hs.execute(string.format('"%s" --disconnect-shortcut', config.openvpnPath)) hs.timer.doAfter(1, function() local app = hs.application.get("OpenVPN Connect") if app then app:kill() end updateStatus() end) end end }) table.insert(menu, { title = "-" }) table.insert(menu, { title = "📂 NAS Actions:", disabled = true }) table.insert(menu, { title = " ⬆️ Mount All Shares", fn = function() if config then for _, share in ipairs(config.shares) do hs.applescript.applescript(string.format('mount volume "smb://%s/%s"', config.nasIP, share)) end end end }) table.insert(menu, { title = " ⬇️ Unmount All Shares", fn = function() if config then for _, share in ipairs(config.shares) do hs.execute(string.format("diskutil unmount /Volumes/%s", share)) end end end }) table.insert(menu, { title = "-" }) table.insert(menu, { title = "🔄 Refresh Status", fn = updateStatus }) return menu end -- 5. INITIALIZATION if NetworkMenu.menuBarItem then NetworkMenu.menuBarItem:setMenu(NetworkMenu.buildMenu) -- Small delay before first status update to ensure config is ready hs.timer.doAfter(1, updateStatus) end -- Watchers NetworkMenu.watcher = hs.wifi.watcher.new(updateStatus):start() NetworkMenu.timer = hs.timer.doEvery(30, updateStatus) print("NetworkMenu Module Loaded Successfully") return NetworkMenu