From e42538896fc6fbeeb45b058edc6b8a165eebde52 Mon Sep 17 00:00:00 2001 From: 28allday Date: Sat, 20 Jun 2026 18:49:18 +0100 Subject: [PATCH] =?UTF-8?q?fix(nosignal):=20hardware-test=20round=204=20?= =?UTF-8?q?=E2=80=94=20Wi-Fi=20join,=20multi-monitor=20popouts,=20sudo=20f?= =?UTF-8?q?ocus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the 2026-06-20 Acer muxless-Optimus hardware test: - F-wifi (NsNetwork.qml): the Wi-Fi panel was display-only — no onClicked, no password field — so a secured network could not be joined from the GUI. Wire the existing Network service: click to connect; open/saved networks connect directly; secured-without-profile expands an inline password field (StyledTextField, echoMode Password). Also strip a stray NUL byte that the on-box edit left at the `?? " "` sentinel. HW-verified on the test laptop. - F-multimon (NsShell.qml + NsBar.qml + NsOverlay.qml): bar popouts mirrored on every monitor. Add a `screen` identity to NsShell; pills pass their output name; each NsOverlay only renders when it owns the open panel ("" = unscoped, for the global power modal). HW-verified dual-monitor. - F-sudo (SudoToggleRow.qml): the passwordless-sudo prompt used the shared TUI.float class with no focus rule, so keystrokes missed it and three blank tries tripped pam_faillock with no feedback. Use a dedicated `nosignal-sudo` class (builder ships float/center/pin/stayfocused rules) and call the new `nosignal-sudo-toggle enable-tui` wrapper, which keeps the terminal open on failure so the reason is visible. Add .gitattributes (*.qml/*.conf/*.json text) for clean diffs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitattributes | 3 + modules/nexus/common/SudoToggleRow.qml | 9 +- modules/nsbar/NsBar.qml | 12 +- modules/nsbar/NsOverlay.qml | 11 +- modules/nsbar/panels/NsNetwork.qml | 174 +++++++++++++++++++------ services/NsShell.qml | 16 ++- 6 files changed, 173 insertions(+), 52 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..c2c3d802 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.qml text +*.conf text +*.json text diff --git a/modules/nexus/common/SudoToggleRow.qml b/modules/nexus/common/SudoToggleRow.qml index b8d53d91..3646beda 100644 --- a/modules/nexus/common/SudoToggleRow.qml +++ b/modules/nexus/common/SudoToggleRow.qml @@ -46,7 +46,14 @@ ToggleRow { // --- actions -------------------------------------------------------------- Process { id: enableProc - command: ["kitty", "--class", "TUI.float", "-e", "sudo", "nosignal-sudo-toggle", "enable"] + // Dedicated window class (nosignal-sudo) so the shipped hypr windowrules + // (float/center/pin/stayfocused) force this prompt to GRAB keyboard focus. + // With the shared TUI.float class and no focus rule the prompt did not get + // keystrokes -> three blank tries tripped pam_faillock and the switch just + // snapped back with no feedback (hardware bug 2026-06-20). `enable-tui` + // runs the sudo prompt as the user and keeps the terminal open on failure + // so the reason is visible instead of vanishing. + command: ["kitty", "--class", "nosignal-sudo", "-e", "nosignal-sudo-toggle", "enable-tui"] } Process { id: disableProc diff --git a/modules/nsbar/NsBar.qml b/modules/nsbar/NsBar.qml index 760ecd1a..cc20ede1 100644 --- a/modules/nsbar/NsBar.qml +++ b/modules/nsbar/NsBar.qml @@ -79,7 +79,7 @@ Variants { id: titlePill active: NsShell.open === "overview" - onTriggered: NsShell.toggle("overview", mapToItem(null, width / 2, 0).x) + onTriggered: NsShell.toggle("overview", mapToItem(null, width / 2, 0).x, win.modelData.name) NsIcon { icon: "desktop_windows" @@ -104,7 +104,7 @@ Variants { anchors.centerIn: parent active: NsShell.open === "calendar" - onTriggered: NsShell.toggle("calendar", mapToItem(null, width / 2, 0).x) + onTriggered: NsShell.toggle("calendar", mapToItem(null, width / 2, 0).x, win.modelData.name) BC.Clock {} } @@ -122,7 +122,7 @@ Variants { visible: Players.active !== null active: NsShell.open === "quicksettings" - onTriggered: NsShell.toggle("quicksettings", mapToItem(null, width / 2, 0).x) + onTriggered: NsShell.toggle("quicksettings", mapToItem(null, width / 2, 0).x, win.modelData.name) NsIcon { icon: Players.active?.isPlaying ? "pause" : "music_note" @@ -153,7 +153,7 @@ Variants { id: bellPill active: NsShell.open === "notifications" - onTriggered: NsShell.toggle("notifications", mapToItem(null, width / 2, 0).x) + onTriggered: NsShell.toggle("notifications", mapToItem(null, width / 2, 0).x, win.modelData.name) NsIcon { icon: Notifs.notClosed.length > 0 ? "notifications" : "notifications_none" @@ -204,7 +204,7 @@ Variants { readonly property bool laptop: UPower.displayDevice.isLaptopBattery active: NsShell.open === "quicksettings" - onTriggered: NsShell.toggle("quicksettings", mapToItem(null, width / 2, 0).x) + onTriggered: NsShell.toggle("quicksettings", mapToItem(null, width / 2, 0).x, win.modelData.name) NsIcon { icon: batteryPill.laptop ? Icons.getBatteryIcon(UPower.displayDevice.percentage, [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state)) : "tune" @@ -249,7 +249,7 @@ Variants { property color iconColour: Theme.text active: NsShell.open === panel - onTriggered: NsShell.toggle(panel, mapToItem(null, width / 2, 0).x) + onTriggered: NsShell.toggle(panel, mapToItem(null, width / 2, 0).x, win.modelData.name) NsIcon { icon: p.icon diff --git a/modules/nsbar/NsOverlay.qml b/modules/nsbar/NsOverlay.qml index b861bd3d..2afb60c4 100644 --- a/modules/nsbar/NsOverlay.qml +++ b/modules/nsbar/NsOverlay.qml @@ -19,8 +19,13 @@ Variants { required property ShellScreen modelData + // This overlay owns the panel only when NsShell.screen names this output + // (or is "" = unscoped, e.g. the global power modal → show on all screens). + // Without this, every screen's overlay rendered the panel → mirrored popups. + readonly property bool onThisScreen: NsShell.screen === "" || NsShell.screen === modelData.name + screen: modelData - visible: NsShell.open !== "" + visible: NsShell.open !== "" && ov.onThisScreen color: "transparent" WlrLayershell.namespace: "nspanels" WlrLayershell.layer: WlrLayer.Overlay @@ -48,7 +53,7 @@ Variants { // full-screen modal: Power Menu Loader { anchors.fill: parent - active: NsShell.open === "power" + active: NsShell.open === "power" && ov.onThisScreen sourceComponent: powerC Component { @@ -61,7 +66,7 @@ Variants { Loader { id: loader - active: NsShell.open !== "" && NsShell.open !== "power" + active: NsShell.open !== "" && NsShell.open !== "power" && ov.onThisScreen y: Theme.size.barHeight + 6 x: { if (!item) diff --git a/modules/nsbar/panels/NsNetwork.qml b/modules/nsbar/panels/NsNetwork.qml index 9bb4cbb6..7d095ec4 100644 --- a/modules/nsbar/panels/NsNetwork.qml +++ b/modules/nsbar/panels/NsNetwork.qml @@ -3,12 +3,22 @@ pragma ComponentBehavior: Bound // NoSignal Network popout (off the Wi-Fi pill). Title + switch; list of networks // (signal glyph accent if connected, SSID, Connected label, lock if secured); // connected row = accent.soft. Footer "Network settings". Wired to Network/Nmcli. +// +// FIX 2026-06-20 (hardware test): the shipped panel was display-only — the row +// MouseArea had no onClicked and there was no password field, so a secured Wi-Fi +// network could not be joined from the GUI (user had to drop to nmcli/nmtui). +// This wires the existing Network service: click a row to connect; open or +// already-saved networks connect directly; a secured network with no saved +// profile expands an inline password field (StyledTextField, echoMode Password). +// NsOverlay grants OnDemand keyboard focus while visible, so the field is typable. +// NOTE: authored on the test machine but NOT yet built/cert'd — review in builder. import QtQuick import QtQuick.Layouts import qs.services import qs.utils import qs.components +import qs.components.controls NsPanel { id: root @@ -16,6 +26,29 @@ NsPanel { implicitWidth: 340 anchorMode: "right" + // ssid of the secured row currently showing its inline password field ("" = none) + property string expandedSsid: "" + + // Decide what a click on a network row does. + function activate(m): void { + if (!m) + return; + if (m.active) { + // Already connected -> tapping disconnects. + Network.disconnectFromNetwork(); + root.expandedSsid = ""; + return; + } + if (!m.isSecure || Network.hasSavedProfile(m.ssid)) { + // Open network, or we already hold the PSK -> connect straight away. + Network.connectToNetworkWithPasswordCheck(m.ssid, m.isSecure, () => {}, m.bssid); + root.expandedSsid = ""; + } else { + // Secured + no saved profile -> reveal the inline password field. + root.expandedSsid = (root.expandedSsid === m.ssid) ? "" : m.ssid; + } + } + RowLayout { Layout.fillWidth: true @@ -46,61 +79,126 @@ NsPanel { Repeater { model: Network.networks - delegate: Rectangle { - id: row + delegate: ColumnLayout { + id: rowWrap required property var modelData readonly property bool connected: modelData?.active ?? false + readonly property bool expanded: root.expandedSsid === (modelData?.ssid ?? " ") Layout.fillWidth: true - implicitHeight: 40 - radius: Theme.radius.button - color: connected ? Theme.accentSoft : ma.containsMouse ? Theme.hover : "transparent" + spacing: 2 - RowLayout { - anchors.fill: parent - anchors.leftMargin: 10 - anchors.rightMargin: 10 - spacing: 10 + function doConnect(): void { + const m = rowWrap.modelData; + if (!m || pwField.text.length === 0) + return; + Network.connectToNetwork(m.ssid, pwField.text, m.bssid, () => {}); + pwField.text = ""; + root.expandedSsid = ""; + } - NsIcon { - icon: Icons.getNetworkIcon(row.modelData?.strength ?? 0) - color: row.connected ? Theme.accent : Theme.text - } + Rectangle { + id: row - ColumnLayout { - Layout.fillWidth: true - spacing: 0 + Layout.fillWidth: true + implicitHeight: 40 + radius: Theme.radius.button + color: rowWrap.connected ? Theme.accentSoft : ma.containsMouse ? Theme.hover : "transparent" - StyledText { - text: row.modelData?.ssid ?? "?" - color: Theme.text - elide: Text.ElideRight + RowLayout { + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + spacing: 10 + + NsIcon { + icon: Icons.getNetworkIcon(rowWrap.modelData?.strength ?? 0) + color: rowWrap.connected ? Theme.accent : Theme.text + } + + ColumnLayout { Layout.fillWidth: true - font.family: Theme.font.family - font.pixelSize: Theme.font.bodySmall + spacing: 0 + + StyledText { + text: rowWrap.modelData?.ssid ?? "?" + color: Theme.text + elide: Text.ElideRight + Layout.fillWidth: true + font.family: Theme.font.family + font.pixelSize: Theme.font.bodySmall + } + StyledText { + visible: rowWrap.connected + text: "Connected" + color: Theme.accent + font.family: Theme.font.family + font.pixelSize: Theme.font.meta + } } - StyledText { - visible: row.connected - text: "Connected" - color: Theme.accent - font.family: Theme.font.family - font.pixelSize: Theme.font.meta + + NsIcon { + visible: rowWrap.modelData?.isSecure ?? false + icon: "lock" + color: Theme.textFaint } } - NsIcon { - visible: row.modelData?.isSecure ?? false - icon: "lock" - color: Theme.textFaint + MouseArea { + id: ma + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.activate(rowWrap.modelData) } } - MouseArea { - id: ma - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor + // inline password entry — only for a secured network with no saved profile + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 10 + Layout.rightMargin: 10 + Layout.bottomMargin: 4 + spacing: 8 + visible: rowWrap.expanded + + onVisibleChanged: { + if (visible) + pwField.forceActiveFocus(); + } + + StyledTextField { + id: pwField + + Layout.fillWidth: true + echoMode: TextField.Password + placeholderText: "Password" + onAccepted: rowWrap.doConnect() + } + + Rectangle { + implicitWidth: 72 + implicitHeight: 28 + radius: Theme.radius.button + color: cma.containsMouse ? Theme.accent : Theme.accentSoft + + StyledText { + anchors.centerIn: parent + text: "Connect" + color: Theme.text + font.family: Theme.font.family + font.pixelSize: Theme.font.meta + } + + MouseArea { + id: cma + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: rowWrap.doConnect() + } + } } } } diff --git a/services/NsShell.qml b/services/NsShell.qml index aa07cfc8..bcba6d70 100644 --- a/services/NsShell.qml +++ b/services/NsShell.qml @@ -4,7 +4,11 @@ pragma ComponentBehavior: Bound // NoSignal shell-level popup state. "One panel open at a time" — `open` is the // id of the active panel ("" = none). `anchorX` is the scene x of the trigger // (a bar pill) center, used to position under-trigger / right-anchored popups. -// The NsBar pills call toggle(); the NsOverlay reads `open` and renders the panel. +// `screen` is the name of the output that owns the open panel, so a per-screen +// NsOverlay shows it on ONLY that monitor instead of mirroring on all of them +// (multi-monitor fix, 2026-06-20). "" = no owner → show on all screens (used by +// the full-screen power modal opened from a global keybind, which has no screen). +// The NsBar pills call toggle() with their screen; NsOverlay matches on `screen`. import QtQuick import Quickshell @@ -14,19 +18,23 @@ Singleton { property string open: "" property real anchorX: 0 + property string screen: "" - function toggle(id: string, x: real): void { - if (root.open === id) { + function toggle(id: string, x: real, screen: string): void { + const scr = screen ?? ""; + if (root.open === id && root.screen === scr) { root.open = ""; } else { root.open = id; root.anchorX = x; + root.screen = scr; } } - function show(id: string, x: real): void { + function show(id: string, x: real, screen: string): void { root.open = id; root.anchorX = x; + root.screen = screen ?? ""; } function close(): void {