From cede2a16900a6e9c9756401914e36a6b7765a952 Mon Sep 17 00:00:00 2001 From: thomas Date: Thu, 27 Aug 2026 09:18:57 +0100 Subject: [PATCH] Feature: Password input --- .../modules/bar/NetworkIndicator.qml | 193 ++++++++++++------ 1 file changed, 131 insertions(+), 62 deletions(-) diff --git a/dotfiles/.config/quickshell/modules/bar/NetworkIndicator.qml b/dotfiles/.config/quickshell/modules/bar/NetworkIndicator.qml index 2de87df..f2ef2cd 100644 --- a/dotfiles/.config/quickshell/modules/bar/NetworkIndicator.qml +++ b/dotfiles/.config/quickshell/modules/bar/NetworkIndicator.qml @@ -44,7 +44,13 @@ Item { return null } - // Only scan for networks while the panel is actually open. + // Only scan for networks while the panel is actually open. Deliberately + // NOT paused while a password prompt is up: scannerEnabled: false stops + // NetworkManager from refreshing its AP cache at all, so the existing + // list quietly ages out with nothing to replace it — the fix for typing + // being interrupted by scan-driven refreshes lives below instead (the + // password field itself is pulled out of the repeated row so refreshes + // can't touch it), not here. Binding { target: root.wifiDevice property: "scannerEnabled" @@ -135,8 +141,17 @@ Item { } } - // SSID of the row currently showing its inline password field, if any. + // SSID awaiting a password, if any. The input itself lives in a single + // persistent widget below the network list (see passwordEntryInput), + // not inside the per-row NetworkRow delegate — wifiRows is rebuilt from + // scratch on every scan result, and a Repeater over a plain array has no + // way to diff that, so it destroys and recreates every delegate each + // time. Keeping the field outside that churn is what lets typing survive + // a scan landing mid-keystroke. property string passwordSsid: "" + // Cleared whenever the target SSID changes so stale text from a + // cancelled/previous prompt never carries over to the next one. + onPasswordSsidChanged: passwordEntryInput.text = "" // Wired connections don't have a user-facing "name" on the device or // Network object — those just expose the interface name (e.g. "eth0"). @@ -179,6 +194,16 @@ Item { } } + function submitPassword() { + const n = root.networkForSsid(root.passwordSsid) + if (n && passwordEntryInput.text.length > 0) n.connectWithPsk(passwordEntryInput.text) + root.passwordSsid = "" + } + + function cancelPassword() { + root.passwordSsid = "" + } + // --- Live stats -------------------------------------------------- // None of this comes from Quickshell's Networking API — it only knows // about devices/networks, not throughput, ping, or routing. So this @@ -529,13 +554,16 @@ Item { font.variableAxes: Appearance.iconFontAxes font.pixelSize: 16 color: root.connectedDevice ? Colors.foreground : Colors.muted - // Wired/disconnected use their own distinct glyphs; only the WiFi - // case renders one of wifiIcon()'s flat/wide wifi-fan glyphs, so - // only that case needs the vertical-stretch correction below. - readonly property bool isWifiFan: root.connectedDevice !== null && root.connectedDevice.type !== DeviceType.Wired + // Only Wired renders its own distinct glyph; disconnected uses + // "signal_wifi_off" — the actual off companion to the + // network_wifi/signal_wifi fan family used below (not "wifi_off", + // which belongs to a visually different, newer glyph family and + // looked inconsistent next to the connected bars) — so it needs the + // same vertical-stretch correction as the rest of that family. + readonly property bool isWifiFan: root.connectedDevice === null || root.connectedDevice.type !== DeviceType.Wired text: { if (!root.connectedDevice) - return "wifi_off" + return "signal_wifi_off" if (root.connectedDevice.type === DeviceType.Wired) return "settings_ethernet" return root.wifiIcon(root.connectedWifiSignal) @@ -593,20 +621,17 @@ Item { id: netRow required property var net signal activate() + // Still tracked for the highlight below — the actual input lives + // outside this (repeatedly rebuilt) delegate, in the persistent + // password entry widget under the network list. readonly property bool passwordOpen: root.passwordSsid === netRow.net.ssid width: parent ? parent.width : 0 radius: 0 - implicitHeight: rowContent.implicitHeight + 12 + (passwordOpen ? passwordRow.implicitHeight + 6 : 0) - color: netRow.net.connected + implicitHeight: rowContent.implicitHeight + 12 + color: (netRow.net.connected || netRow.passwordOpen) ? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18) : (rowMouse.containsMouse ? Colors.primary : "transparent") - function submit() { - const n = root.networkForSsid(netRow.net.ssid) - if (n && passwordInput.text.length > 0) n.connectWithPsk(passwordInput.text) - root.passwordSsid = "" - } - Column { anchors.fill: parent anchors.margins: 6 @@ -676,53 +701,6 @@ Item { color: rowMouse.containsMouse ? Colors.primaryText : Colors.muted } } - - RowLayout { - id: passwordRow - visible: netRow.passwordOpen - width: parent.width - spacing: 6 - - Rectangle { - Layout.fillWidth: true - implicitHeight: 24 - radius: 0 - color: Colors.background - border.color: Colors.muted - border.width: 1 - - TextInput { - id: passwordInput - anchors.fill: parent - anchors.margins: 6 - echoMode: TextInput.Password - color: Colors.foreground - font.family: Appearance.fontFamily - font.pixelSize: Appearance.fontSizeSmall - clip: true - focus: netRow.passwordOpen - onAccepted: netRow.submit() - // Explicit fallback alongside onAccepted, which only - // fires when acceptableInput is true — covers both - // the main Return key and numpad Enter directly. - Keys.onReturnPressed: netRow.submit() - Keys.onEnterPressed: netRow.submit() - } - } - - Text { - text: "check" - font.family: Appearance.iconFontFamilyRounded - font.variableAxes: Appearance.iconFontAxes - font.pixelSize: 16 - color: Colors.primary - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: netRow.submit() - } - } - } } MouseArea { @@ -1080,6 +1058,97 @@ Item { color: Colors.muted } + // Password entry — a single persistent widget rather + // than living inside the NetworkRow delegate for + // root.passwordSsid, so a wifi scan landing mid-keystroke + // (which rebuilds the whole Known/Other Networks list + // below) can't destroy the focused TextInput and wipe + // out what's been typed. See passwordSsid's own comment. + Rectangle { + width: parent.width + visible: root.passwordSsid !== "" + radius: 0 + color: Colors.background + border.color: Colors.muted + border.width: 1 + implicitHeight: passwordEntryColumn.implicitHeight + 12 + + Column { + id: passwordEntryColumn + anchors.fill: parent + anchors.margins: 6 + spacing: 6 + + Text { + width: parent.width + text: "Password for “" + root.passwordSsid + "”" + elide: Text.ElideRight + font.family: Appearance.fontFamily + font.pixelSize: Appearance.fontSizeSmall + color: Colors.muted + } + + RowLayout { + width: parent.width + spacing: 6 + + Rectangle { + Layout.fillWidth: true + implicitHeight: 24 + radius: 0 + color: Colors.surface + border.color: Colors.muted + border.width: 1 + + TextInput { + id: passwordEntryInput + anchors.fill: parent + anchors.margins: 6 + echoMode: TextInput.Password + color: Colors.foreground + font.family: Appearance.fontFamily + font.pixelSize: Appearance.fontSizeSmall + clip: true + focus: root.passwordSsid !== "" + onAccepted: root.submitPassword() + // Explicit fallback alongside onAccepted, which only + // fires when acceptableInput is true — covers both + // the main Return key and numpad Enter directly. + Keys.onReturnPressed: root.submitPassword() + Keys.onEnterPressed: root.submitPassword() + Keys.onEscapePressed: root.cancelPassword() + } + } + + Text { + text: "check" + font.family: Appearance.iconFontFamilyRounded + font.variableAxes: Appearance.iconFontAxes + font.pixelSize: 16 + color: Colors.primary + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.submitPassword() + } + } + + Text { + text: "close" + font.family: Appearance.iconFontFamilyRounded + font.variableAxes: Appearance.iconFontAxes + font.pixelSize: 16 + color: Colors.muted + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.cancelPassword() + } + } + } + } + } + // Known/Other Networks — capped height, scrolls once // there are more networks than fit rather than growing // the popup indefinitely.