Initial commit of Hammerspoon config
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
-- ~/.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
|
||||
Reference in New Issue
Block a user