network
NetworkManager: connectivity, Wi-Fi and wired state, scanned access points and join progress.
text {
content = mantle.network:map(function(network)
if not network.connected then
return "offline"
elseif network.ssid == "Ethernet" then
return "wired"
end
local wifi = network.wifi_devices[1] -- the primary interface
return string.format("%s %d%%", network.ssid or "", wifi and wifi.strength or 0)
end, ""),
}
State
mantle.network:get() returns NetworkState, nil before the first push. A field marked ? may be absent.
mantle.network’s payload.
| Field | Type | Description |
|---|---|---|
connect_error? | JoinError | The last failed join on any Wi-Fi device, or nil before any or after a success. Kept until the next connect, cancel_connect or abort_connect; check its ssid before showing it. |
connected | boolean | A connection carries the default route; false means offline. |
connecting_ssid? | string | The current join’s SSID on any Wi-Fi device, or nil; clears on a verdict or abort_connect. |
ethernet_enabled | boolean | A wired device is activated; set_ethernet_enabled’s read-back, unlike carrier. |
ethernet_ip? | string | The first activated wired device’s IPv4 address without prefix, or nil. |
ethernet_present | boolean | At least one wired device exists, cable or not. |
ethernet_speed? | integer | That wired device’s link speed in Mb/s; nil when unknown or none is activated. |
networking_enabled | boolean | NetworkManager networking is on (NetworkingEnabled). |
password_ssid? | string | The join SSID awaiting a password from a network/connect secure field on any device, or nil. Also set after a rejected key; cleared when a join starts or by cancel_connect. |
ssid? | string | "Ethernet" when the default route is wired, else the associated SSID, else nil. An association still getting an address has an ssid while connected is false. |
vpn_error? | VpnError | The last failed VPN activation, or nil before any or after the next connect_vpn. |
vpn_secret? | VpnSecretRequest | The one pending VPN secret request, or nil. Answer it with network/vpn_secret secure fields or drop it with cancel_vpn_secret. |
vpns | VpnInfo[] | Saved VPN and WireGuard profiles, ordered by name. |
wifi_devices | WifiDeviceInfo[] | Every Wi-Fi interface, primary first. IDs are interface names, never NetworkManager object paths. |
wifi_enabled | boolean | Wi-Fi radio power (WirelessEnabled); can be true with no Wi-Fi hardware, see wifi_present. |
wifi_present | boolean | At least one Wi-Fi device exists. |
AccessPointInfo
One scanned network in available_networks.
| Field | Type | Description |
|---|---|---|
active | boolean | The Wi-Fi device is associated with this SSID. |
band | string | "2.4 GHz", "5 GHz", "6 GHz", or empty for a frequency outside those bands. |
saved | boolean | A saved NetworkManager profile compatible with this device names this SSID. |
secure | boolean | Needs a key: WEP, WPA or RSN. |
ssid | string | Network name, "" for hidden networks; one entry per SSID, from its strongest access point. |
strength | integer | Signal strength, 0 to 100. |
JoinError
A failed join, as connect_error.
| Field | Type | Description |
|---|---|---|
message | string | Display text, such as "wrong password" or "network not found". |
ssid | string | The network the join was for. |
VpnError
A failed VPN activation, as vpn_error.
| Field | Type | Description |
|---|---|---|
message | string | Display text, such as "login failed" or "connection timed out". |
uuid | string | The profile the activation was for. |
VpnInfo
One saved NetworkManager VPN profile in vpns.
| Field | Type | Description |
|---|---|---|
activating | boolean | The profile is being activated, including while it waits for a secret. |
active | boolean | The profile is activated. |
id | string | Profile name. |
kind | VpnKind | |
uuid | string | Profile UUID; pass it to connect_vpn and disconnect_vpn. |
VpnKind
vpns[].kind.
| Value | Description |
|---|---|
"vpn" | A plugin VPN. |
"wireguard" | A native WireGuard profile. |
VpnSecretRequest
NetworkManager asking for VPN secrets, as vpn_secret.
| Field | Type | Description |
|---|---|---|
fields | string[] | Secret keys still to enter, such as "password"; each entered key leaves the list. |
id | string | Request ID. A secure field for fields[i] uses name = request.id .. "/" .. fields[i]. |
retry | boolean | NetworkManager rejected the previous secrets and asks again. |
uuid | string | UUID of the profile being activated. |
vpn_id | string | Name of that profile. |
WifiDeviceInfo
One Wi-Fi interface, with its own scan, join, address and access points. connected means
activated here, while the flat connected means a default route; ssid never uses "Ethernet".
| Field | Type | Description |
|---|---|---|
available_networks | AccessPointInfo[] | Visible networks on this interface: one per SSID, at most 20, ordered associated, then saved, then strongest. |
connect_error? | JoinError | Failed join on this interface, or nil; cleared by the next join or cancel_connect. |
connected | boolean | This interface has an activated connection. |
connecting_ssid? | string | SSID of this interface’s current join, or nil. |
id | string | Interface name, such as "wlan0"; pass it to actions ending in _device. |
password_ssid? | string | SSID awaiting a password on this interface, or nil. |
scanning | boolean | A scan is in flight on this interface. |
ssid? | string | Associated SSID, or nil. |
strength | integer | Associated access point strength, 0 to 100. |
wifi_ip? | string | IPv4 address without prefix, or nil. |
Actions
Call each as mantle.network:<action>(arguments...); ? marks an argument you may omit.
| Action | Arguments | Description |
|---|---|---|
set_networking_enabled | enabled: boolean | Turns NetworkManager networking on or off. |
set_wifi_enabled | enabled: boolean | Powers the Wi-Fi radio. |
set_ethernet_enabled | enabled: boolean | false disconnects every wired device; true activates each one’s autoconnect profile, and a device without one stays down. |
scan | Requests a Wi-Fi scan; a no-op without Wi-Fi hardware. | |
scan_device | id: string | Requests a scan on the named Wi-Fi interface. |
connect | ssid: string, hidden: boolean | Joins a network. Without a saved profile, a secured, hidden or out-of-range one sets password_ssid and waits for a key. |
connect_device | ssid: string, hidden: boolean, id: string | Joins through the named Wi-Fi interface; a removed ID is never retargeted. |
cancel_connect | Drops the one current password request, on any Wi-Fi device; a join already running continues. | |
abort_connect | Stops the one current join on any Wi-Fi device, deleting a profile the join created. | |
forget | ssid: string | Deletes every saved profile for this SSID. |
disconnect_wifi | Disconnects Wi-Fi; NetworkManager does not autoconnect it again until the next join. | |
disconnect_wifi_device | id: string | Disconnects the named Wi-Fi interface. |
connect_vpn | uuid: string | Activates the saved VPN or WireGuard profile with this UUID. A missing secret raises vpn_secret. |
disconnect_vpn | uuid: string | Deactivates the VPN or WireGuard profile with this UUID. |
cancel_vpn_secret | Declines the pending vpn_secret request, failing that activation. |
Backend
| Contract | Behavior |
|---|---|
| Updates | Every manager, device-list, device-state, access-point, association and saved-profile change re-reads the whole state from NetworkManager. A hotplugged adapter rescans the device set |
| Devices | wifi_devices lists each Wi-Fi interface by name, primary first, with its association, scan, address and access points; {} without Wi-Fi hardware. Flat join fields describe the one join on any device. Actions without _device use the primary. Wired fields describe the first activated wired device |
| Toggles | Networking through Enable, Wi-Fi through WirelessEnabled |
| Scan | RequestScan. The device’s scanning turns true on the call and false when LastScan moves or NetworkManager refuses |
| Access points | The associated one’s strength is live. The others’ are read when they appear and after each scan, when NetworkManager updates them |
| Connect | A saved profile or an open network in range joins at once. Anything else sets password_ssid and waits for the key from a secure_submit = { capability = "network", action = "connect" } field (secure fields); the key never reaches Lua |
| Join verdict | Watched for up to 45 s. A rejected key sets password_ssid again. A new network’s profile, key included, is saved when the join starts and stays after a rejection; a key retyped for a saved profile reaches disk only once NetworkManager accepts it |
| Abort | abort_connect deletes a profile the join created, else deactivates the join |
| VPN profiles | vpns lists saved profiles of type vpn (plugin VPNs) and wireguard, by name. active and activating follow NetworkManager’s active connections, so a VPN started elsewhere shows too. The list needs a profile change or restart to notice an edited profile |
| VPN activation | connect_vpn(uuid) calls ActivateConnection and watches the verdict for 45 s, not counting time on that profile’s secret prompt; at 45 s it deactivates the attempt. A failure lands in vpn_error; disconnect_vpn or cancel_vpn_secret during the attempt sets none. A second connect_vpn for a profile already waiting does nothing. Only profiles NetworkManager manages: no VPN client the shell runs itself |
| VPN secrets | The engine registers a NetworkManager secret agent, answering only NetworkManager itself. When an activation needs secrets, vpn_secret lists the fields to enter and waits for secure_submit fields named request.id .. "/" .. field; the last one answers NetworkManager. One request at a time: a second is refused. cancel_vpn_secret or a 120 s wait fails the activation. Nothing is stored; NetworkManager asks again when a secret is agent-owned |
| VPN secret scope | Only vpn and wireguard requests are answered; other connection types get “no secrets”, as with no agent. Fields come from NetworkManager’s hints, else from the profile’s <key>-flags entries marked agent-owned or not saved. WireGuard asks for private-key only; a peer’s preshared key and a plugin’s x-vpn-message text are not handled. Registering needs polkit’s org.freedesktop.NetworkManager.network-control; without it a warning is logged and VPN activation still works for secrets NetworkManager already stores. NetworkManager takes one agent per identifier per user, so only the first running shell gets prompts; a second logs the same warning |
| Missing | Stays nil. The next generation’s first read retries |
Use scan_device(id), connect_device(ssid, hidden, id) and disconnect_wifi_device(id) to target an entry in wifi_devices. An unknown or removed ID is never switched to another device: all three log a warning and return. Without Wi-Fi hardware, scan and disconnect_wifi do nothing; connect reports an error in the flat connect_error field. A password prompt keeps its selected device through submission and activation. Only one password prompt or join attempt is tracked across all devices. Starting another settles the previous join first.
How do I…
Ask for a Wi-Fi password
A click on an available_networks entry of a wifi_devices interface calls mantle.network:connect(entry.ssid, false);
a secured one without a saved profile then sets password_ssid. Show the field while it is set. Escape clears a secure
field and keeps it armed, then calls its on_cancel, the place to call cancel_connect:
local asking = mantle.network:map(function(network)
return network.password_ssid ~= nil
end, false)
return column {
visible = asking,
spacing = 6,
children = {
text {
content = mantle.network:map(function(network)
return network.password_ssid and ("Password for " .. network.password_ssid) or ""
end, ""),
},
textfield {
width = 240,
height = 24,
placeholder = "Password",
secure_submit = { capability = "network", action = "connect" },
on_cancel = function() mantle.network:cancel_connect() end,
},
},
}
List and toggle a VPN
Which profile counts as the preferred one is the shell’s call; vpns carries what it needs:
local vpns = mantle.network:map(function(network) return network.vpns or {} end, {})
return column {
spacing = 4,
children = vpns:map(function(list)
local rows = {}
for _, vpn in ipairs(list) do
rows[#rows + 1] = row {
on_click = function()
if vpn.active or vpn.activating then
mantle.network:disconnect_vpn(vpn.uuid)
else
mantle.network:connect_vpn(vpn.uuid)
end
end,
children = { text { content = vpn.id .. (vpn.active and " (on)" or "") } },
}
end
return rows
end),
}
Ask for a VPN secret
NetworkManager’s request sets vpn_secret; show one secure field per entry of fields. cancel_vpn_secret
drops the whole request, not one field, and fails the activation. Here a first Escape clears the field and
a second cancels. retry is true after rejected secrets:
local request = mantle.network:map(function(network) return network.vpn_secret end)
return column {
visible = request:map(function(secret) return secret ~= nil end),
spacing = 6,
children = request:map(function(secret)
local rows = {}
for _, field in ipairs(secret and secret.fields or {}) do
rows[#rows + 1] = textfield {
width = 240,
height = 24,
placeholder = field,
secure_submit = { capability = "network", action = "vpn_secret", name = secret.id .. "/" .. field },
on_cancel = function(cleared)
if not cleared then mantle.network:cancel_vpn_secret() end
end,
}
end
return rows
end),
}
Know whether a join worked
A failed connect lands in connect_error:
text {
foreground = "#F38BA8",
content = mantle.network:map(function(network)
local failure = network.connect_error
return failure and (failure.ssid .. ": " .. failure.message) or ""
end, ""),
}
Gotchas
| Trap | Fix |
|---|---|
A wifi_devices entry’s available_networks has a row with ssid == "" | Hidden networks broadcast no name; they merge into one nameless row. Skip it and join hidden networks with connect(ssid, true) |
| A hidden network asks for a password even when open | Its security is unknown until it answers. Enter on the empty field joins it as open |