feat(wifi-hotspot): add NetworkManager hotspot control plugin (#120)

* feat(wifi-hotspot): add NetworkManager hotspot control plugin

Expose bar toggle, panel, and IPC to start/stop a Wi-Fi hotspot and list connected clients.

* refactor(hotspot): rename plugin directory to hotspot

Align the folder name with the cleboost/hotspot plugin id.

* refactor(hotspot): align plugin id and state keys with hotspot slug

Update manifest id, IPC references, and shared state keys after the directory rename.
This commit is contained in:
Cleboost
2026-07-27 13:56:18 -04:00
committed by GitHub
parent 33f82fcc76
commit fc1c390a7f
7 changed files with 988 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
# Wi-Fi Hotspot
Start and stop a NetworkManager Wi-Fi hotspot from the bar, inspect connected
devices in a panel, and control the service over IPC.
## Plugin
| Field | Value |
| --- | --- |
| ID | `cleboost/hotspot` |
| Entries | Bar widget: `toggle`; panel: `panel`; service: `hotspot` |
## Requirements
Install `nmcli` from NetworkManager, plus `iw` and `ip` from iproute2, on
`PATH`. The active user must be allowed to manage NetworkManager connections
(the default on most desktop setups).
## Usage
1. Open the plugin settings and set the hotspot name and password.
2. Add the `toggle` widget to a bar.
3. Left-click the widget to open the panel with the connected device list.
4. Right-click the widget to start or stop the hotspot.
```sh
noctalia msg panel-toggle cleboost/hotspot:panel
```
Starting a hotspot may disconnect the Wi-Fi adapter from its current network
while the access point is active.
## Settings
| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `ssid` | `string` | `Noctalia Hotspot` | Network name broadcast to clients. |
| `password` | `string` | *(empty)* | WPA password. Must be at least 8 characters before starting. |
| `interface` | `string` | *(empty)* | Optional Wi-Fi interface such as `wlan0`. Auto-detected when empty. |
| `refresh_interval` | `int` | `5` | Seconds between status and client-list refreshes. |
## IPC
```sh
noctalia msg plugin cleboost/hotspot:hotspot all enable
noctalia msg plugin cleboost/hotspot:hotspot all disable
noctalia msg plugin cleboost/hotspot:hotspot all toggle
noctalia msg plugin cleboost/hotspot:hotspot all refresh
```
The `enable`, `disable`, and `toggle` events change the hotspot state.
`refresh` and `status` re-read NetworkManager without changing it. IPC events
take no payload.
## Notes
- The plugin uses `nmcli device wifi hotspot` to create the access point and
`nmcli connection down` to stop the active AP profile.
- Connected devices are discovered with `iw station dump` and `ip neigh`.
- The plugin makes no network requests and writes no user content to disk.
- Notifications use the host `notification-show` IPC with Wi-Fi glyphs (`wifi`,
`wifi-off`, `alert-circle`) because `noctalia.notify()` does not accept a
custom icon yet.
- Hotspots started outside Noctalia are detected when NetworkManager reports an
active `802-11-wireless` connection in `ap` mode.
+208
View File
@@ -0,0 +1,208 @@
--!nonstrict
local opened = false
local render
local status = noctalia.state.get("hotspot_status") or {
active = false,
busy = false,
available = true,
clients = {},
clientCount = 0,
error = "",
}
local function sendCommand(action)
noctalia.state.set("hotspot_command", { action = action })
end
local function phaseDetails()
if status.busy == true then
if status.active == true then
return noctalia.tr("panel.status.stopping_title"), noctalia.tr("panel.status.stopping_message"), "on_surface_variant", "loader-2"
end
return noctalia.tr("panel.status.starting_title"), noctalia.tr("panel.status.starting_message"), "on_surface_variant", "loader-2"
end
if status.active == true then
return noctalia.tr("panel.status.active_title"), noctalia.tr("panel.status.active_message", {
ssid = status.ssid or "?",
interface = status.interface or "?",
}), "tertiary", "router"
end
if status.available == false then
local message = status.error
if message == nil or message == "" then
message = noctalia.tr("error.missing_dependencies")
end
return noctalia.tr("panel.status.error_title"), message, "error", "alert-circle"
end
return noctalia.tr("panel.status.inactive_title"), noctalia.tr("panel.status.inactive_message"), "on_surface_variant", "wifi-off"
end
local function clientRow(client)
local subtitle = client.mac
if client.ip ~= nil and client.ip ~= "" then
subtitle = client.ip .. " · " .. client.mac
end
local trailing = nil
if client.signal ~= nil then
trailing = ui.label({
text = tostring(client.signal) .. " dBm",
fontSize = 11,
color = "on_surface_variant",
})
end
return ui.row({
align = "center",
gap = 10,
paddingH = 14,
paddingV = 10,
fill = "surface_variant/0.35",
radius = 10,
}, {
ui.glyph({ name = "device-mobile", size = 18, color = "primary" }),
ui.column({ gap = 2, flexGrow = 1 }, {
ui.label({ text = client.ip ~= "" and client.ip or client.mac, fontWeight = "medium", maxLines = 1 }),
ui.label({ text = subtitle, fontSize = 11, color = "on_surface_variant", maxLines = 1 }),
}),
trailing,
})
end
render = function()
local busy = status.busy == true
local active = status.active == true
local canToggle = status.available ~= false and not busy
local stateTitle, stateMessage, stateColor, stateGlyph = phaseDetails()
local actionText = noctalia.tr("panel.action.start")
local actionGlyph = "player-play"
local actionVariant = "primary"
if active then
actionText = noctalia.tr("panel.action.stop")
actionGlyph = "player-stop"
actionVariant = "destructive"
elseif busy then
actionText = active and noctalia.tr("panel.action.stopping") or noctalia.tr("panel.action.starting")
actionGlyph = "loader-2"
end
local children = {
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
ui.row({ align = "center", gap = 9, flexGrow = 1 }, {
ui.glyph({ name = "router", size = 18, color = "primary" }),
ui.label({ text = noctalia.tr("title"), fontSize = 16, fontWeight = "bold" }),
}),
ui.button({
glyph = "refresh",
variant = "ghost",
tooltip = noctalia.tr("panel.refresh"),
enabled = not busy,
onClick = "onRefresh",
}),
ui.button({
glyph = "close",
variant = "ghost",
tooltip = noctalia.tr("panel.close"),
onClick = "onCloseClicked",
}),
}),
ui.row({
align = "center",
gap = 10,
fill = "surface_variant/0.45",
radius = 10,
padding = 12,
}, {
ui.glyph({ name = stateGlyph, size = 18, color = stateColor }),
ui.column({ gap = 3, flexGrow = 1 }, {
ui.label({ text = stateTitle, fontWeight = "medium", color = stateColor }),
ui.label({ text = stateMessage, color = "on_surface_variant", maxLines = 3 }),
}),
}),
}
if status.gateway ~= nil and status.gateway ~= "" then
table.insert(children, ui.label({
text = noctalia.tr("panel.gateway", { address = status.gateway }),
fontSize = 11,
color = "on_surface_variant",
}))
end
table.insert(children, ui.button({
text = actionText,
glyph = actionGlyph,
variant = actionVariant,
controlSize = "lg",
enabled = canToggle,
onClick = "onToggle",
}))
table.insert(children, ui.row({ align = "center", justify = "space_between" }, {
ui.label({
text = noctalia.tr("panel.clients_title"),
fontWeight = "medium",
color = "on_surface_variant",
}),
ui.label({
text = tostring(status.clientCount or #(status.clients or {})),
fontSize = 11,
color = "on_surface_variant",
}),
}))
local clients = status.clients or {}
if #clients == 0 then
table.insert(children, ui.label({
text = active and noctalia.tr("panel.no_clients") or noctalia.tr("panel.clients_inactive"),
color = "on_surface_variant",
maxLines = 2,
padding = 12,
}))
else
local listChildren = {}
for _, client in ipairs(clients) do
table.insert(listChildren, clientRow(client))
end
table.insert(children, ui.scroll({ flexGrow = 1, gap = 8 }, listChildren))
end
panel.render(ui.column({ flexGrow = 1, gap = 12 }, children))
end
noctalia.state.watch("hotspot_status", function(value)
if type(value) ~= "table" then return end
status = value
if opened then render() end
end)
function onOpen(_context)
opened = true
status = noctalia.state.get("hotspot_status") or status
sendCommand("refresh")
render()
end
function onClose()
opened = false
end
function onCloseClicked()
panel.close()
end
function onToggle()
if status.busy == true or status.available == false then return end
sendCommand("toggle")
end
function onRefresh()
sendCommand("refresh")
end
+63
View File
@@ -0,0 +1,63 @@
id = "cleboost/hotspot"
name = "Wi-Fi Hotspot"
version = "1.0.0"
plugin_api = 9
author = "Cleboost"
license = "MIT"
icon = "router"
description = "Start and stop a Wi-Fi hotspot from the bar, with connected device list and IPC control."
tags = ["network", "bar", "panel", "service", "utility", "system", "indicator"]
dependencies = ["nmcli", "iw", "ip"]
[[setting]]
key = "ssid"
type = "string"
label_key = "settings.ssid.label"
description_key = "settings.ssid.description"
default = "Noctalia Hotspot"
[[setting]]
key = "password"
type = "string"
label_key = "settings.password.label"
description_key = "settings.password.description"
default = ""
[[setting]]
key = "interface"
type = "string"
label_key = "settings.interface.label"
description_key = "settings.interface.description"
default = ""
[[setting]]
key = "refresh_interval"
type = "int"
label_key = "settings.refresh_interval.label"
description_key = "settings.refresh_interval.description"
default = 5
min = 2
max = 60
[[service]]
id = "hotspot"
entry = "service.luau"
[[widget]]
id = "toggle"
entry = "widget.luau"
[[widget.setting]]
key = "glyph"
type = "glyph"
label_key = "settings.glyph.label"
default = "router"
[[panel]]
id = "panel"
entry = "panel.luau"
width = 420
height = 520
placement = "attached"
position = "top_right"
open_near_click = true
+502
View File
@@ -0,0 +1,502 @@
--!nonstrict
local STATUS_KEY = "hotspot_status"
local COMMAND_KEY = "hotspot_command"
local busy = false
local revision = 0
local available = noctalia.commandExists("nmcli")
local iwAvailable = noctalia.commandExists("iw")
local ipAvailable = noctalia.commandExists("ip")
local refreshing = false
local refreshAgain = false
local status = {
active = false,
busy = false,
available = available,
error = "",
revision = 0,
ssid = "",
interface = "",
connection = "",
gateway = "",
clients = {},
clientCount = 0,
}
local refresh
local setActive
local function publish(errorMessage)
revision += 1
status.busy = busy
status.available = available
status.revision = revision
status.error = errorMessage or status.error or ""
status.clientCount = #(status.clients or {})
noctalia.state.set(STATUS_KEY, status)
end
local function shellQuote(value)
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
end
local function showNotification(title, body, icon, urgency)
local payload = noctalia.json.encode({
app_name = noctalia.tr("title"),
summary = title,
body = body or "",
icon = icon,
urgency = urgency,
})
if payload == nil then
if urgency == "critical" then
noctalia.notifyError(title, body)
else
noctalia.notify(title, body)
end
return
end
noctalia.runAsync("noctalia msg notification-show " .. shellQuote(payload))
end
local function runCommand(command, callback, timeoutMs)
local started = noctalia.runAsync(command, callback, timeoutMs or 15000)
if not started and type(callback) == "function" then
callback({
exitCode = -1,
stdout = "",
stderr = noctalia.tr("error.command_start"),
timedOut = false,
})
end
return started
end
local function validateSettings()
local ssid = noctalia.string.trim(noctalia.getConfig("ssid") or "")
if ssid == "" then
return false, noctalia.tr("error.missing_ssid")
end
local password = tostring(noctalia.getConfig("password") or "")
if #password < 8 then
return false, noctalia.tr("error.password_too_short")
end
return true, ssid, password
end
local function parseNmcliTriple(stdout, filterType)
local entries = {}
for line in tostring(stdout or ""):gmatch("[^\n]+") do
local name, entryType, extra = line:match("^([^:]+):([^:]+):([^:]*)$")
if entryType == filterType and name ~= nil and name ~= "" then
table.insert(entries, { name = name, device = extra or "" })
end
end
return entries
end
local function pickWifiInterface(callback)
local configured = noctalia.string.trim(noctalia.getConfig("interface") or "")
if configured ~= "" then
callback(configured)
return
end
runCommand("nmcli -t -f DEVICE,TYPE,STATE device status", function(result)
local devices = parseNmcliTriple(result.stdout, "wifi")
callback(devices[1] and devices[1].name or nil)
end)
end
local function parseIwStations(stdout)
local clients = {}
local current = nil
for line in tostring(stdout or ""):gmatch("[^\n]+") do
local mac = line:match("^Station ([%x:]+)")
if mac ~= nil then
current = { mac = mac:lower(), ip = "", signal = nil }
table.insert(clients, current)
elseif current ~= nil then
local signal = line:match("signal:%s*([%-%d]+)")
if signal ~= nil then
current.signal = tonumber(signal)
end
end
end
return clients
end
local function parseIpNeigh(stdout)
local byMac = {}
for line in tostring(stdout or ""):gmatch("[^\n]+") do
local ip, mac = line:match("^([%d%.]+)%s+lladdr%s+([%x:]+)")
if ip ~= nil and mac ~= nil then
byMac[mac:lower()] = ip
end
end
return byMac
end
local function mergeClients(stations, neighByMac)
local merged = {}
local seen = {}
for _, client in ipairs(stations) do
table.insert(merged, {
mac = client.mac,
ip = neighByMac[client.mac] or client.ip or "",
signal = client.signal,
})
seen[client.mac] = true
end
for mac, ip in pairs(neighByMac) do
if not seen[mac] then
table.insert(merged, { mac = mac, ip = ip, signal = nil })
end
end
table.sort(merged, function(left, right)
if left.ip ~= right.ip then
if left.ip == "" then return false end
if right.ip == "" then return true end
return left.ip < right.ip
end
return left.mac < right.mac
end)
return merged
end
local function fetchClients(iface, callback)
if iface == nil or iface == "" then
callback({})
return
end
local stations = {}
local neighByMac = {}
local remaining = 0
local function finish()
remaining -= 1
if remaining > 0 then return end
callback(mergeClients(stations, neighByMac))
end
if iwAvailable then
remaining += 1
runCommand("iw dev " .. shellQuote(iface) .. " station dump", function(result)
if result.exitCode == 0 then
stations = parseIwStations(result.stdout)
end
finish()
end)
end
if ipAvailable then
remaining += 1
runCommand("ip neigh show dev " .. shellQuote(iface), function(result)
if result.exitCode == 0 then
neighByMac = parseIpNeigh(result.stdout)
end
finish()
end)
end
if remaining == 0 then
callback({})
end
end
local function inspectConnection(entry, callback)
local command = "nmcli -g 802-11-wireless.mode,802-11-wireless.ssid,IP4.ADDRESS connection show "
.. shellQuote(entry.name)
runCommand(command, function(result)
if result.exitCode ~= 0 then
callback(nil)
return
end
local mode, ssid, gateway
local index = 0
for line in tostring(result.stdout or ""):gmatch("[^\n]+") do
index += 1
local value = noctalia.string.trim(line)
if index == 1 then mode = value
elseif index == 2 then ssid = value
elseif index == 3 then gateway = value end
end
if mode ~= "ap" then
callback(nil)
return
end
callback({
active = true,
connection = entry.name,
interface = entry.device,
ssid = ssid or "",
gateway = gateway or "",
})
end)
end
local function findActiveHotspot(callback)
runCommand("nmcli -t -f NAME,TYPE,DEVICE connection show --active", function(result)
if result.exitCode ~= 0 then
local detail = noctalia.string.trim(result.stderr or "")
callback(nil, detail ~= "" and detail or noctalia.tr("error.status_failed"))
return
end
local entries = parseNmcliTriple(result.stdout, "802-11-wireless")
if #entries == 0 then
callback(nil)
return
end
local index = 1
local function nextEntry()
local entry = entries[index]
if entry == nil then
callback(nil)
return
end
inspectConnection(entry, function(info)
if info ~= nil then
callback(info)
return
end
index += 1
nextEntry()
end)
end
nextEntry()
end)
end
local function completeRefresh(callback, active)
refreshing = false
if callback then callback(active) end
if refreshAgain then
refreshAgain = false
refresh()
end
end
refresh = function(callback)
if not available then
status.active = false
status.clients = {}
publish(noctalia.tr("error.missing_dependencies"))
if callback then callback(false) end
return
end
if refreshing then
refreshAgain = true
if callback then callback(status.active) end
return
end
refreshing = true
findActiveHotspot(function(info, err)
if err ~= nil then
status.active = false
status.clients = {}
publish(err)
completeRefresh(callback, false)
return
end
if info == nil then
status.active = false
status.ssid = ""
status.interface = ""
status.connection = ""
status.gateway = ""
status.clients = {}
publish()
completeRefresh(callback, false)
return
end
status.active = true
status.ssid = info.ssid
status.interface = info.interface
status.connection = info.connection
status.gateway = info.gateway
fetchClients(info.interface, function(clients)
status.clients = clients
publish()
completeRefresh(callback, true)
end)
end)
end
local function finishToggle(expectedActive, result)
if result.exitCode ~= 0 then
busy = false
local detail = noctalia.string.trim(result.stderr or "")
if detail == "" then detail = noctalia.string.trim(result.stdout or "") end
if detail == "" then detail = noctalia.tr("error.command_failed") end
publish(detail)
showNotification(noctalia.tr("title"), detail, "alert-circle", "critical")
refresh()
return
end
refresh(function(currentActive)
busy = false
publish()
if currentActive ~= expectedActive then
showNotification(noctalia.tr("title"), noctalia.tr("error.state_mismatch"), "alert-circle", "critical")
elseif currentActive then
showNotification(
noctalia.tr("notification.enabled.title"),
noctalia.tr("notification.enabled.body", { ssid = status.ssid or noctalia.getConfig("ssid") or "" }),
"wifi",
"normal"
)
else
showNotification(
noctalia.tr("notification.disabled.title"),
noctalia.tr("notification.disabled.body"),
"wifi-off",
"normal"
)
end
end)
end
local function startHotspot()
local ok, ssidOrError, password = validateSettings()
if not ok then
busy = false
publish(ssidOrError)
showNotification(noctalia.tr("title"), ssidOrError, "alert-circle", "critical")
return
end
pickWifiInterface(function(iface)
if iface == nil or iface == "" then
busy = false
local message = noctalia.tr("error.no_wifi_device")
publish(message)
showNotification(noctalia.tr("title"), message, "alert-circle", "critical")
return
end
local command = table.concat({
"nmcli device wifi hotspot",
"ifname " .. shellQuote(iface),
"ssid " .. shellQuote(ssidOrError),
"password " .. shellQuote(password),
}, " ")
runCommand(command, function(result)
finishToggle(true, result)
end, 30000)
end)
end
local function stopHotspot()
if status.connection ~= nil and status.connection ~= "" then
runCommand("nmcli connection down " .. shellQuote(status.connection), function(result)
finishToggle(false, result)
end)
return
end
findActiveHotspot(function(info)
if info == nil then
busy = false
status.active = false
status.clients = {}
publish()
showNotification(
noctalia.tr("notification.disabled.title"),
noctalia.tr("notification.disabled.body"),
"wifi-off",
"normal"
)
return
end
runCommand("nmcli connection down " .. shellQuote(info.connection), function(result)
finishToggle(false, result)
end)
end)
end
setActive = function(nextActive)
if busy or not available then return end
if nextActive then
local ok, message = validateSettings()
if not ok then
publish(message)
showNotification(noctalia.tr("title"), message, "alert-circle", "critical")
return
end
end
busy = true
publish()
if nextActive then
startHotspot()
else
stopHotspot()
end
end
local function handleCommand(action)
if action == "toggle" then
setActive(not status.active)
elseif action == "enable" then
setActive(true)
elseif action == "disable" then
setActive(false)
elseif action == "refresh" or action == "status" then
refresh()
end
end
noctalia.state.watch(COMMAND_KEY, function(command)
if type(command) ~= "table" or type(command.action) ~= "string" then return end
handleCommand(command.action)
end)
publish(available and "" or noctalia.tr("error.missing_dependencies"))
refresh()
function update()
local seconds = tonumber(noctalia.getConfig("refresh_interval")) or 5
noctalia.setUpdateInterval(math.max(2, seconds) * 1000)
if not busy then
refresh()
end
end
function onConfigChanged()
update()
end
function onIpc(event, _payload)
handleCommand(event)
end
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

+75
View File
@@ -0,0 +1,75 @@
{
"error": {
"command_failed": "The NetworkManager command failed.",
"command_start": "Noctalia could not start the hotspot command.",
"missing_dependencies": "nmcli is required to manage the hotspot.",
"missing_ssid": "Set a hotspot name in the plugin settings.",
"no_wifi_device": "No Wi-Fi adapter was found.",
"password_too_short": "The hotspot password must be at least 8 characters.",
"state_mismatch": "The requested hotspot state could not be confirmed.",
"status_failed": "NetworkManager status could not be read."
},
"notification": {
"disabled": {
"body": "The Wi-Fi hotspot has been turned off.",
"title": "Hotspot disabled"
},
"enabled": {
"body": "Clients can connect to {ssid}.",
"title": "Hotspot enabled"
}
},
"panel": {
"action": {
"start": "Start hotspot",
"starting": "Starting…",
"stop": "Stop hotspot",
"stopping": "Stopping…"
},
"clients_inactive": "Start the hotspot to see connected devices here.",
"clients_title": "Connected devices",
"close": "Close",
"gateway": "Gateway: {address}",
"no_clients": "No device is connected right now.",
"refresh": "Refresh",
"status": {
"active_message": "{ssid} is broadcasting on {interface}.",
"active_title": "Hotspot active",
"error_title": "Unavailable",
"inactive_message": "Configure the name and password in settings, then start the hotspot.",
"inactive_title": "Hotspot off",
"starting_message": "NetworkManager is creating the access point…",
"starting_title": "Starting",
"stopping_message": "NetworkManager is shutting down the access point…",
"stopping_title": "Stopping"
}
},
"settings": {
"glyph": {
"label": "Bar glyph"
},
"interface": {
"description": "Optional Wi-Fi interface name. Leave empty to use the first adapter NetworkManager reports.",
"label": "Wi-Fi interface"
},
"password": {
"description": "WPA password shared with connecting devices. Must be at least 8 characters.",
"label": "Password"
},
"refresh_interval": {
"description": "How often the service refreshes hotspot status and the connected device list, in seconds.",
"label": "Refresh interval"
},
"ssid": {
"description": "Network name broadcast by the hotspot.",
"label": "Hotspot name"
}
},
"title": "Wi-Fi Hotspot",
"tooltip": {
"active": "{ssid} is active · {count} device(s) connected · click for panel · right-click to stop",
"changing": "Changing hotspot state…",
"inactive": "Hotspot is off · click for panel · right-click to start",
"unavailable": "NetworkManager is not available"
}
}
+75
View File
@@ -0,0 +1,75 @@
--!nonstrict
local PANEL_ID = "cleboost/hotspot:panel"
local COMMAND_KEY = "hotspot_command"
local status = noctalia.state.get("hotspot_status") or {
active = false,
busy = false,
available = true,
clients = {},
clientCount = 0,
}
local glyph = noctalia.getConfig("glyph") or "router"
local function sendCommand(action)
noctalia.state.set(COMMAND_KEY, { action = action })
end
local function render()
if status.available == false then
barWidget.setGlyph("wifi-off")
barWidget.setGlyphColor("error")
barWidget.setTooltip(status.error ~= "" and status.error or noctalia.tr("tooltip.unavailable"))
elseif status.busy == true then
barWidget.setGlyph("loader-2")
barWidget.setGlyphColor("primary")
barWidget.setTooltip(noctalia.tr("tooltip.changing"))
elseif status.active == true then
barWidget.setGlyph(glyph)
barWidget.setGlyphColor("tertiary")
local count = status.clientCount or #(status.clients or {})
barWidget.setText(count > 0 and tostring(count) or "")
barWidget.setTooltip(noctalia.tr("tooltip.active", {
ssid = status.ssid or "?",
count = tostring(count),
}))
else
barWidget.setGlyph(glyph)
barWidget.setGlyphColor("on_surface_variant")
barWidget.setText("")
barWidget.setTooltip(noctalia.tr("tooltip.inactive"))
end
end
noctalia.state.watch("hotspot_status", function(value)
if type(value) == "table" then
status = value
render()
end
end)
render()
function onClick()
noctalia.togglePanel(PANEL_ID)
end
function onRightClick()
if status.available == false then
noctalia.notifyError(noctalia.tr("title"), status.error or noctalia.tr("error.missing_dependencies"))
return
end
if status.busy == true then return end
sendCommand("toggle")
end
function onMiddleClick()
sendCommand("refresh")
end
function onConfigChanged()
glyph = noctalia.getConfig("glyph") or "router"
render()
end