Files
hammerspoon/NetworkCenter.lua
T
2026-05-14 18:59:23 -04:00

326 lines
13 KiB
Lua

-- ~/.hammerspoon/NetworkCenter.lua
local NetworkCenter = {}
local Config = require("Config")
local menuBar = hs.menubar.new()
-- State Management
local nasOnline = false
local vpnActive = false
local ovpnIP = "Offline"
local tailscaleIP = "Offline"
local tailnetDomain = "Offline" -- NEW: Added state for domain
local speedTestResult = "Idle"
local lastExternalIP = "Offline"
local ispName = "Fetching..."
local wifiInfo = {
ssid = "N/A", speed = 0, gen = "N/A", band = "N/A",
channel = "N/A", protocol = "N/A", rssi = 0, noise = 0
}
local gatewayLatency = "---"
-- Service Monitoring State
local services = {
{ name = "Plex", host = "192.168.1.105", port = 32400 },
{ name = "Komga", host = "192.168.1.101", port = 25600 },
{ name = "Sonarr", host = "192.168.1.101", port = 8989 },
{ name = "Radarr", host = "192.168.1.101", port = 7878 },
{ name = "qBitorrent", host = "192.168.1.101", port = 8080 }
}
local serviceStatus = {}
-- ==========================================
-- 1. DATA RETRIEVAL
-- ==========================================
local function getWifiDetails()
local details = hs.wifi.interfaceDetails()
if details then
wifiInfo.ssid = details.ssid or "Disconnected"
wifiInfo.speed = details.transmitRate or 0
wifiInfo.rssi = details.rssi or 0
wifiInfo.noise = details.noise or 0
local phy = details.activePHYMode
local phyStr = tostring(phy)
if phy == 6 or phyStr:find("6") or phyStr:find("ax") then
wifiInfo.gen, wifiInfo.protocol = "6", "AX"
elseif phy == 5 or phyStr:find("5") or phyStr:find("ac") then
wifiInfo.gen, wifiInfo.protocol = "5", "AC"
elseif phy == 4 or phyStr:find("4") or phyStr:find("n") then
wifiInfo.gen, wifiInfo.protocol = "4", "N"
else
wifiInfo.gen, wifiInfo.protocol = "?", "?"
end
if details.wlanChannel and type(details.wlanChannel) == "table" then
wifiInfo.band = tostring(details.wlanChannel.band):gsub("GHz", "") or "N/A"
wifiInfo.channel = tostring(details.wlanChannel.number) or "N/A"
end
else
wifiInfo.ssid = hs.wifi.currentNetwork() or "N/A"
end
end
function NetworkCenter.updateStatus()
getWifiDetails()
-- Async NAS Ping
hs.task.new("/sbin/ping", function(code)
nasOnline = (code == 0)
NetworkCenter.refreshUI()
end, {"-c", "1", "-t", "1", Config.nasIP}):start()
-- Gateway Latency
hs.task.new("/sbin/ping", function(code, stdOut)
if code == 0 and stdOut then
local ms = stdOut:match("time=(%d+%.?%d*) ms")
gatewayLatency = ms and (math.floor(tonumber(ms)) .. "ms") or "Error"
else
gatewayLatency = "Timeout"
end
NetworkCenter.refreshUI()
end, {"-c", "1", "-t", "1", "192.168.1.1"}):start()
-- Service Port Checks
for _, svc in ipairs(services) do
hs.task.new("/usr/bin/nc", function(code)
serviceStatus[svc.name] = (code == 0 and "🟢" or "🔴")
NetworkCenter.refreshUI()
end, {"-z", "-w", "2", svc.host, tostring(svc.port)}):start()
end
-- Tailscale Detection & Domain Retrieval
local tsPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale"
local tsStatus = hs.execute(tsPath .. " status --json 2>/dev/null")
local foundTSIP = "Offline"
local foundTSDomain = "Offline"
if tsStatus and tsStatus ~= "" then
local tsData = hs.json.decode(tsStatus)
if tsData and tsData.BackendState == "Running" then
-- Get the Tailnet name/domain
foundTSDomain = tsData.MagicDNSSuffix or "N/A"
if tsData.Self and tsData.Self.TailscaleIPs then
for _, ip in ipairs(tsData.Self.TailscaleIPs) do
if ip:match("^100%.") then foundTSIP = ip break end
end
end
end
end
tailscaleIP = foundTSIP
tailnetDomain = foundTSDomain
-- OpenVPN Detection
local ovpnCheck = hs.execute("pgrep -x 'OpenVPN Connect'"):gsub("%s+", "")
local ovpnIPQuery = hs.execute("ifconfig | grep -A 1 'utun' | grep 'inet 10.' | awk '{print $2}'"):gsub("%s+", "")
if ovpnCheck ~= "" and ovpnIPQuery ~= "" then
vpnActive = true
ovpnIP = ovpnIPQuery
else
vpnActive = false
ovpnIP = "Offline"
end
-- External IP & ISP
hs.task.new("/usr/bin/curl", function(exitCode, stdOut)
if exitCode == 0 and stdOut then
local data = hs.json.decode(stdOut)
if data then
lastExternalIP = data.ip or "Offline"
ispName = data.asn_org or data.company_name or "Unknown ISP"
end
else
lastExternalIP = "Offline"
ispName = "Offline"
end
NetworkCenter.refreshUI()
end, {"-s", "-m", "5", "https://ifconfig.co/json"}):start()
end
local function runSpeedTest()
speedTestResult = "Testing..."
NetworkCenter.refreshUI()
hs.task.new("/usr/bin/networkQuality", function(exitCode, stdOut)
if exitCode == 0 and stdOut then
local data = hs.json.decode(stdOut)
if data then
local downMbps = math.floor((data.dl_throughput / 1048576) + 0.5)
local upMbps = math.floor((data.ul_throughput / 1048576) + 0.5)
speedTestResult = string.format("D:%d / U:%d Mbps", downMbps, upMbps)
else
speedTestResult = "Parse Error"
end
else
speedTestResult = "Failed"
end
NetworkCenter.refreshUI()
end, {"-c"}):start()
end
-- ==========================================
-- 2. ACTIONS & STEALTH LOGIC
-- ==========================================
local function fastHideOpenVPN()
local count = 0
local hideTimer
hideTimer = hs.timer.doEvery(0.02, 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')
local win = app:mainWindow()
if win then win:setFrame(hs.geometry.rect(5000, 5000, 0, 0), 0) end
end
count = count + 1
if count > 75 then hideTimer:stop() end
end)
end
local function watchForVPN()
local attempts = 0
local watchTimer
watchTimer = hs.timer.doEvery(1, function()
attempts = attempts + 1
local checkIP = hs.execute("ifconfig | grep -A 1 'utun' | grep 'inet 10.'"):gsub("%s+", "")
if checkIP ~= "" or attempts > 10 then
NetworkCenter.updateStatus()
watchTimer:stop()
end
end)
end
local function vpnAction(mode)
if mode == "connect" then
hs.alert.show("Connecting VPN...")
fastHideOpenVPN()
hs.execute(string.format('"%s" --connect-shortcut=%s', Config.openvpnPath, Config.vpnProfileID))
watchForVPN()
else
hs.alert.show("Disconnecting VPN...")
hs.execute(string.format('"%s" --disconnect-shortcut', Config.openvpnPath))
hs.timer.doAfter(0.5, function()
hs.execute("pkill -9 'OpenVPN Connect'")
NetworkCenter.updateStatus()
end)
end
end
local function tailscaleAction(mode)
local tsPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale"
if mode == "up" then
hs.alert.show("Starting Tailscale...")
hs.execute(tsPath .. " up")
else
hs.alert.show("Stopping Tailscale...")
hs.execute(tsPath .. " down")
end
hs.timer.doAfter(1, NetworkCenter.updateStatus)
hs.timer.doAfter(3, NetworkCenter.updateStatus)
end
-- ==========================================
-- 3. THE MENU UI
-- ==========================================
function NetworkCenter.refreshUI()
if not menuBar then return end
local isHome = (wifiInfo.ssid == Config.homeSSID)
local locationStr = isHome and "HOME" or "REMOTE"
local topIcon = isHome and "🏠" or "🌐"
local tsActive = (tailscaleIP ~= "Offline")
if vpnActive and tsActive then
topIcon = topIcon .. "🛡️"
elseif vpnActive then
topIcon = topIcon .. "🔒"
elseif tsActive then
topIcon = topIcon .. "📡"
end
menuBar:setTitle(topIcon .. " " .. math.floor(wifiInfo.speed) .. "Mb")
local shareSubMenu = {}
for i, share in ipairs(Config.shares) do
local isMounted = hs.fs.attributes("/Volumes/" .. share) ~= nil
local sIcon = isMounted and "" or "⚪️"
local titleText = sIcon .. " " .. share
if isMounted then
local capCmd = "df -h '/Volumes/" .. share .. "' | tail -1 | awk '{gsub(\"i\", \"b\", $2); print \"(\" $2 \")\"}'"
titleText = titleText .. " " .. hs.execute(capCmd):gsub("\n", "")
end
table.insert(shareSubMenu, {
title = titleText,
fn = function()
if isMounted then hs.execute("diskutil unmount /Volumes/" .. share)
else hs.applescript.applescript(string.format('mount volume "smb://%s/%s"', Config.nasIP, share)) end
hs.timer.doAfter(3, NetworkCenter.updateStatus)
end
})
if isMounted then
local barCmd = "df -h '/Volumes/" .. share .. "' | tail -1 | awk '{p=int($5); bar=\"\"; for(i=1;i<=10;i++){if(i<=p/10) bar=bar \"\"; else bar=bar \"\"} gsub(\"i\", \"b\", $3); print \" [\" bar \"] \" p \"% (\" $3 \" Used)\"}'"
table.insert(shareSubMenu, { title = hs.execute(barCmd):gsub("\n", ""), disabled = true })
table.insert(shareSubMenu, { title = " 📂 Browse Folder", fn = function() hs.execute("open /Volumes/" .. share) end })
table.insert(shareSubMenu, { title = "-" })
end
end
local labSubMenu = {}
local labIcons = ""
for i, svc in ipairs(services) do
local status = serviceStatus[svc.name] or "⚪️"
labIcons = labIcons .. status
table.insert(labSubMenu, {
title = status .. " " .. svc.name,
fn = function() hs.execute(string.format("open http://%s:%s", svc.host, svc.port)) end
})
end
local ovpnHeader = "🔐 OPENVPN CONTROLS" .. (vpnActive and " (🟢 ACTIVE)" or "")
local tsHeader = "🚀 TAILSCALE MESH" .. (tsActive and " (🟢 ACTIVE)" or "")
local menu = {
{ title = "📍 LOCATION STATUS" },
{ title = " ├─ Profile: " .. locationStr, disabled = true },
{ title = " ├─ Gateway: " .. gatewayLatency, disabled = true },
{ title = " ├─ Channel: " .. wifiInfo.channel .. " (" .. wifiInfo.band .. " GHz)", disabled = true },
{ title = " ├─ Signal: " .. wifiInfo.rssi .. " dBm / Noise: " .. wifiInfo.noise .. " dBm", disabled = true },
{ title = " └─ WiFi " .. wifiInfo.gen .. " (" .. wifiInfo.protocol .. ") @ " .. wifiInfo.speed .. " Mbps", disabled = true },
{ title = "-" },
{ title = "🚀 LAB SERVICES " .. labIcons, menu = labSubMenu },
{ title = "-" },
{ title = "🛡️ VPN & MESH NET" },
{ title = " ├─ OpenVPN: " .. ovpnIP, disabled = true },
{ title = " ├─ Tailscale IP: " .. tailscaleIP, disabled = true },
{ title = " ├─ Tailnet: " .. tailnetDomain, disabled = true }, -- NEW: Display Domain
{ title = " ├─ ISP: " .. ispName, disabled = true },
{ title = " └─ Public IP: " .. lastExternalIP, disabled = true },
{ title = "-" },
{ title = "📦 STORAGE & SHARES", menu = shareSubMenu },
{ title = " ├─ NAS IP: " .. Config.nasIP, disabled = true },
{ title = " └─ NAS Status: " .. (nasOnline and "🟢 Online" or "🔴 Offline"), disabled = true },
{ title = "-" },
{ title = "⚡️ SPEED TEST: " .. speedTestResult, fn = runSpeedTest },
{ title = "-" },
{ title = ovpnHeader },
{ title = " ├─ 🔒 Connect tunnel", fn = function() vpnAction("connect") end },
{ title = " └─ 🔓 Disconnect & Quit", fn = function() vpnAction("disconnect") end },
{ title = "-" },
{ title = tsHeader },
{ title = " ├─ 🟢 Start Service", fn = function() tailscaleAction("up") end },
{ title = " └─ 🔴 Stop Service", fn = function() tailscaleAction("down") end },
{ title = "-" },
{ title = "🔄 Refresh Status", fn = NetworkCenter.updateStatus }
}
menuBar:setMenu(menu)
end
NetworkCenter.updateStatus()
hs.timer.doEvery(30, NetworkCenter.updateStatus)
hs.alert.show("Network Center Platinum Loaded")
return NetworkCenter