Own the network: details, VPN, enterprise Wi-Fi, and a firewall that can also allow

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 16:31:52 -04:00
parent aba2d16ffa
commit b30bf40407
29 changed files with 4452 additions and 241 deletions
@@ -2,12 +2,26 @@
//
// The quick settings version is a popover: a compact list you glance at. This
// is the one you sit in front of when a network is not behaving, so each row
// carries what you would otherwise open a terminal to find out signal,
// carries what you would otherwise open a terminal to find out -- signal,
// security, and whether it is a network this machine already knows.
//
// Joining a secured network reveals an inline password field rather than
// failing silently, which is the one interaction the popover already got right
// and is worth keeping identical.
// The connected network opens: addresses, whether it comes back on its own,
// whether this machine shows the same hardware address to it every time, a QR
// code for a guest, and the way out. Those five were the whole reason this page
// still had a door back to GNOME's Wi-Fi panel.
//
// Three ways in, and they are genuinely different networks rather than three
// styles of the same one:
//
// * open or already known -- connect, nothing to type;
// * WPA with a passphrase -- an inline field, revealed on the row you clicked
// rather than in a dialog over a tiled window;
// * 802.1X enterprise -- a form, because a passphrase field cannot join
// eduroam and pretending otherwise is how people ended up in a terminal.
//
// Forgetting is two-stage and says what it costs, per the danger pattern the
// rest of Settings uses: the arming press changes the row's copy, and only the
// second press does anything.
import QtQuick
import Quickshell
@@ -21,25 +35,76 @@ Column {
spacing: 0
// SSID whose password field is open, and the last failure.
// Exactly one of these is non-empty at a time, so the panel never shows two
// ways to join the same network or two networks half-open at once.
property string expandedFor: ""
property string passwordFor: ""
property string enterpriseFor: ""
property string confirmingForget: ""
property string failedSsid: ""
property string failedText: ""
// The list shows what matters and folds the rest: connected and saved
// networks always render (they lead the sort, so a slice keeps them), and
// strangers fill the remaining slots up to the cap. Everything else waits
// behind the "more networks" row -- an apartment building's worth of
// neighbors' SSIDs is noise, not a list.
property bool showAll: false
readonly property int visibleCap: 6
readonly property var shown: {
const list = Connectivity.networks;
if (root.showAll || list.length <= root.visibleCap)
return list;
const pinned = list.filter(network => network.connected || network.known).length;
return list.slice(0, Math.max(root.visibleCap, pinned));
}
readonly property int hiddenCount: Connectivity.networks.length - root.shown.length
function isEnterprise(network: var): bool {
return Connectivity.securityLabel(network) === "Enterprise";
}
function closeAll(): void {
root.expandedFor = "";
root.passwordFor = "";
root.enterpriseFor = "";
root.confirmingForget = "";
}
// One click on a row means whatever that row's state makes it mean. The
// connected network opens rather than reconnecting to itself.
function activate(network: var): void {
root.failedSsid = "";
if (network.connected)
const name = network.name;
if (network.connected) {
const wasOpen = root.expandedFor === name;
root.closeAll();
root.expandedFor = wasOpen ? "" : name;
return;
}
if (root.isEnterprise(network)) {
const wasOpen = root.enterpriseFor === name;
root.closeAll();
root.enterpriseFor = wasOpen ? "" : name;
return;
}
if (network.known || !Connectivity.isSecured(network)) {
root.passwordFor = "";
root.closeAll();
network.connect();
return;
}
root.passwordFor = root.passwordFor === network.name ? "" : network.name;
const wasOpen = root.passwordFor === name;
root.closeAll();
root.passwordFor = wasOpen ? "" : name;
}
Repeater {
model: Connectivity.networks
model: root.shown
Column {
id: entry
@@ -47,6 +112,23 @@ Column {
required property var modelData
required property int index
readonly property string ssid: entry.modelData.name || ""
readonly property bool open: root.expandedFor === entry.ssid && entry.ssid !== ""
readonly property bool joining: root.passwordFor === entry.ssid && entry.ssid !== ""
readonly property bool enterprising: root.enterpriseFor === entry.ssid && entry.ssid !== ""
readonly property bool confirming: root.confirmingForget === entry.ssid && entry.ssid !== ""
// Addresses for the connected network, once the helper has answered.
// Null means "not read yet", which the details grid renders as such
// rather than as "no address".
readonly property var details: entry.modelData.connected && entry.ssid !== ""
? NetworkTools.detailsFor(entry.ssid) : null
// The saved profile behind this SSID, if it holds a passphrase a QR
// code could carry. Networks with no stored key cannot be shared.
readonly property var shareEntry: WifiShare.shareable.find(
candidate => String(candidate.ssid ?? "") === entry.ssid) ?? null
width: parent.width
SettingRow {
@@ -60,12 +142,20 @@ Column {
else if (entry.modelData.known)
bits.push("Saved");
bits.push(Connectivity.signalLabel(entry.modelData.signalStrength));
bits.push(Connectivity.securityLabel(entry.modelData));
bits.push(root.isEnterprise(entry.modelData)
? "Enterprise (802.1X)"
: Connectivity.securityLabel(entry.modelData));
return bits.join(" · ");
}
divider: entry.index < Connectivity.networks.length - 1 || root.passwordFor === entry.modelData.name
// When the drawer is open its own last row carries the hairline,
// so the header does not draw one immediately above it.
divider: entry.open
? false
: (entry.index < root.shown.length - 1
|| root.hiddenCount > 0 || root.showAll
|| entry.joining || entry.enterprising)
controlWidth: 190
activatable: !entry.modelData.connected
activatable: true
onActivated: root.activate(entry.modelData)
Row {
@@ -73,38 +163,162 @@ Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 7
Text {
anchors.verticalCenter: parent.verticalCenter
visible: entry.modelData.connected
text: "Connected"
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.DemiBold
}
SettingsButton {
anchors.verticalCenter: parent.verticalCenter
visible: entry.modelData.connected
text: "Disconnect"
onClicked: entry.modelData.disconnect()
onClicked: {
root.closeAll();
entry.modelData.disconnect();
}
}
SettingsButton {
anchors.verticalCenter: parent.verticalCenter
visible: !entry.modelData.connected
text: entry.modelData.known ? "Connect" : "Join"
text: root.isEnterprise(entry.modelData)
? (entry.enterprising ? "Cancel" : "Join…")
: (entry.modelData.known ? "Connect" : "Join")
onClicked: root.activate(entry.modelData)
}
// Only the connected row opens, so only it gets a caret.
Text {
anchors.verticalCenter: parent.verticalCenter
visible: entry.modelData.connected
text: entry.open ? "▴" : "▾"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
}
// The password field for this network, when it is the one being
// joined. Inline rather than a dialog: a dialog over a tiled window
// is a worse place to type than the row you just clicked.
// ── The connected network, opened ────────────────────────────────
Column {
width: parent.width
visible: entry.open
ConnectionDetails {
width: parent.width
details: entry.details
}
// These two describe a saved profile, and until the helper has
// answered this panel does not know what the profile says. A
// switch drawn on a guess is a switch that lies about the state
// it is reporting.
SwitchRow {
width: parent.width
visible: !!entry.details
label: "Connect automatically"
detail: "Rejoin this network whenever it is in range"
checked: entry.details?.autoconnect !== false
enabled: !NetworkTools.busy
onToggled: value => NetworkTools.setAutoconnect(entry.ssid, value)
}
SwitchRow {
width: parent.width
visible: !!entry.details
label: "Randomize MAC address"
detail: entry.details?.macRandomized === true
? "This network sees a made-up hardware address, so it cannot follow this machine between visits. Takes effect on the next reconnect."
: "Show this network a made-up hardware address instead of the adapter's real one. Takes effect on the next reconnect."
checked: entry.details?.macRandomized === true
enabled: !NetworkTools.busy
onToggled: value => NetworkTools.setMacRandom(entry.ssid, value)
}
ActionRow {
width: parent.width
visible: entry.shareEntry !== null
label: "Share this network"
detail: WifiShare.sharing === String(entry.shareEntry?.name ?? "")
? "Anyone who can see this screen can join"
: "Shows a QR code a phone can scan, so nobody has to read the password out"
action: WifiShare.sharing === String(entry.shareEntry?.name ?? "")
? "Hide" : "Show code"
onTriggered: WifiShare.sharing === String(entry.shareEntry?.name ?? "")
? WifiShare.stopSharing()
: WifiShare.share(String(entry.shareEntry?.name ?? ""))
}
// Drawn at its natural size on a white plate: a QR code inverted
// or tinted to match a dark theme is unreliable to scan, and this
// one has exactly one job.
Item {
width: parent.width
visible: entry.shareEntry !== null
&& WifiShare.sharing === String(entry.shareEntry?.name ?? "")
&& WifiShare.imagePath !== ""
implicitHeight: visible ? plate.height + 20 : 0
Rectangle {
id: plate
anchors.horizontalCenter: parent.horizontalCenter
y: 10
width: 208
height: 208
radius: 10
color: "white"
Image {
anchors.centerIn: parent
width: 184
height: 184
smooth: false
fillMode: Image.PreserveAspectFit
cache: false
source: WifiShare.imagePath !== "" ? "file://" + WifiShare.imagePath : ""
}
}
}
SettingRow {
width: parent.width
label: "Forget this network"
detail: entry.confirming
? "This disconnects now and deletes the saved password. Rejoining " + entry.ssid + " means typing it again."
: "Remove the saved profile for " + entry.ssid
controlWidth: 200
divider: entry.index < root.shown.length - 1
|| root.hiddenCount > 0 || root.showAll
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 8
SettingsButton {
text: entry.confirming ? "Keep it" : "Forget…"
enabled: !NetworkTools.busy
onClicked: root.confirmingForget = entry.confirming ? "" : entry.ssid
}
SettingsButton {
visible: entry.confirming
text: "Forget"
tone: "danger"
enabled: !NetworkTools.busy
onClicked: {
root.closeAll();
NetworkTools.forget(entry.ssid);
}
}
}
}
}
// ── A passphrase, for the network being joined ───────────────────
//
// Inline rather than a dialog: a dialog over a tiled window is a
// worse place to type than the row you just clicked.
Item {
width: parent.width
height: root.passwordFor === entry.modelData.name ? 54 : 0
height: entry.joining ? 54 : 0
visible: height > 0
clip: true
@@ -141,9 +355,33 @@ Column {
}
}
// ── 802.1X, for the networks a passphrase cannot reach ───────────
// Loaded rather than merely hidden, so closing the form destroys it
// and the password that was typed into it. A hidden form keeps its
// field, which is both a stale value on the way back in and a
// passphrase sitting in a live object for no reason.
Loader {
id: enterpriseLoader
width: parent.width
active: entry.enterprising
visible: enterpriseLoader.active
sourceComponent: EnterpriseJoinForm {
ssid: entry.ssid
busy: NetworkTools.busy
onSubmitted: (eap, identity, secret, caPath) => {
NetworkTools.joinEnterprise(entry.ssid, eap, identity, secret, caPath);
root.enterpriseFor = "";
}
}
}
Text {
width: parent.width
visible: root.failedSsid === entry.modelData.name
visible: root.failedSsid === entry.ssid && entry.ssid !== ""
leftPadding: 2
bottomPadding: 8
text: root.failedText
@@ -155,14 +393,28 @@ Column {
Connections {
target: entry.modelData
function onConnectionFailed(reason): void {
root.failedSsid = entry.modelData.name;
root.failedSsid = entry.ssid;
root.failedText = Connectivity.connectionFailureText(reason);
root.passwordFor = entry.modelData.name;
if (!root.isEnterprise(entry.modelData))
root.passwordFor = entry.ssid;
}
}
}
}
SettingRow {
width: parent.width
visible: root.hiddenCount > 0
|| (root.showAll && Connectivity.networks.length > root.visibleCap)
label: root.showAll
? "Show fewer networks"
: root.hiddenCount + (root.hiddenCount === 1 ? " more network" : " more networks")
detail: root.showAll ? "" : "Weaker signals, folded to keep the list short"
activatable: true
onActivated: root.showAll = !root.showAll
divider: false
}
SettingRow {
width: parent.width
visible: Connectivity.networks.length === 0