fix(nosignal): hardware-test round 4 — Wi-Fi join, multi-monitor popouts, sudo focus
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) <noreply@anthropic.com>
This commit is contained in:
parent
2cc089b41f
commit
e42538896f
6 changed files with 173 additions and 52 deletions
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
*.qml text
|
||||||
|
*.conf text
|
||||||
|
*.json text
|
||||||
|
|
@ -46,7 +46,14 @@ ToggleRow {
|
||||||
// --- actions --------------------------------------------------------------
|
// --- actions --------------------------------------------------------------
|
||||||
Process {
|
Process {
|
||||||
id: enableProc
|
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 {
|
Process {
|
||||||
id: disableProc
|
id: disableProc
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ Variants {
|
||||||
id: titlePill
|
id: titlePill
|
||||||
|
|
||||||
active: NsShell.open === "overview"
|
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 {
|
NsIcon {
|
||||||
icon: "desktop_windows"
|
icon: "desktop_windows"
|
||||||
|
|
@ -104,7 +104,7 @@ Variants {
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
active: NsShell.open === "calendar"
|
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 {}
|
BC.Clock {}
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +122,7 @@ Variants {
|
||||||
|
|
||||||
visible: Players.active !== null
|
visible: Players.active !== null
|
||||||
active: NsShell.open === "quicksettings"
|
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 {
|
NsIcon {
|
||||||
icon: Players.active?.isPlaying ? "pause" : "music_note"
|
icon: Players.active?.isPlaying ? "pause" : "music_note"
|
||||||
|
|
@ -153,7 +153,7 @@ Variants {
|
||||||
id: bellPill
|
id: bellPill
|
||||||
|
|
||||||
active: NsShell.open === "notifications"
|
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 {
|
NsIcon {
|
||||||
icon: Notifs.notClosed.length > 0 ? "notifications" : "notifications_none"
|
icon: Notifs.notClosed.length > 0 ? "notifications" : "notifications_none"
|
||||||
|
|
@ -204,7 +204,7 @@ Variants {
|
||||||
|
|
||||||
readonly property bool laptop: UPower.displayDevice.isLaptopBattery
|
readonly property bool laptop: UPower.displayDevice.isLaptopBattery
|
||||||
active: NsShell.open === "quicksettings"
|
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 {
|
NsIcon {
|
||||||
icon: batteryPill.laptop ? Icons.getBatteryIcon(UPower.displayDevice.percentage, [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state)) : "tune"
|
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
|
property color iconColour: Theme.text
|
||||||
|
|
||||||
active: NsShell.open === panel
|
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 {
|
NsIcon {
|
||||||
icon: p.icon
|
icon: p.icon
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,13 @@ Variants {
|
||||||
|
|
||||||
required property ShellScreen modelData
|
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
|
screen: modelData
|
||||||
visible: NsShell.open !== ""
|
visible: NsShell.open !== "" && ov.onThisScreen
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
WlrLayershell.namespace: "nspanels"
|
WlrLayershell.namespace: "nspanels"
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
|
@ -48,7 +53,7 @@ Variants {
|
||||||
// full-screen modal: Power Menu
|
// full-screen modal: Power Menu
|
||||||
Loader {
|
Loader {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: NsShell.open === "power"
|
active: NsShell.open === "power" && ov.onThisScreen
|
||||||
sourceComponent: powerC
|
sourceComponent: powerC
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
|
|
@ -61,7 +66,7 @@ Variants {
|
||||||
Loader {
|
Loader {
|
||||||
id: loader
|
id: loader
|
||||||
|
|
||||||
active: NsShell.open !== "" && NsShell.open !== "power"
|
active: NsShell.open !== "" && NsShell.open !== "power" && ov.onThisScreen
|
||||||
y: Theme.size.barHeight + 6
|
y: Theme.size.barHeight + 6
|
||||||
x: {
|
x: {
|
||||||
if (!item)
|
if (!item)
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,22 @@ pragma ComponentBehavior: Bound
|
||||||
// NoSignal Network popout (off the Wi-Fi pill). Title + switch; list of networks
|
// NoSignal Network popout (off the Wi-Fi pill). Title + switch; list of networks
|
||||||
// (signal glyph accent if connected, SSID, Connected label, lock if secured);
|
// (signal glyph accent if connected, SSID, Connected label, lock if secured);
|
||||||
// connected row = accent.soft. Footer "Network settings". Wired to Network/Nmcli.
|
// 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
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import qs.services
|
import qs.services
|
||||||
import qs.utils
|
import qs.utils
|
||||||
import qs.components
|
import qs.components
|
||||||
|
import qs.components.controls
|
||||||
|
|
||||||
NsPanel {
|
NsPanel {
|
||||||
id: root
|
id: root
|
||||||
|
|
@ -16,6 +26,29 @@ NsPanel {
|
||||||
implicitWidth: 340
|
implicitWidth: 340
|
||||||
anchorMode: "right"
|
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 {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
|
@ -46,16 +79,32 @@ NsPanel {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Network.networks
|
model: Network.networks
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: ColumnLayout {
|
||||||
id: row
|
id: rowWrap
|
||||||
|
|
||||||
required property var modelData
|
required property var modelData
|
||||||
readonly property bool connected: modelData?.active ?? false
|
readonly property bool connected: modelData?.active ?? false
|
||||||
|
readonly property bool expanded: root.expandedSsid === (modelData?.ssid ?? " ")
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
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 = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: row
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: 40
|
implicitHeight: 40
|
||||||
radius: Theme.radius.button
|
radius: Theme.radius.button
|
||||||
color: connected ? Theme.accentSoft : ma.containsMouse ? Theme.hover : "transparent"
|
color: rowWrap.connected ? Theme.accentSoft : ma.containsMouse ? Theme.hover : "transparent"
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
@ -64,8 +113,8 @@ NsPanel {
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
NsIcon {
|
NsIcon {
|
||||||
icon: Icons.getNetworkIcon(row.modelData?.strength ?? 0)
|
icon: Icons.getNetworkIcon(rowWrap.modelData?.strength ?? 0)
|
||||||
color: row.connected ? Theme.accent : Theme.text
|
color: rowWrap.connected ? Theme.accent : Theme.text
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
|
@ -73,7 +122,7 @@ NsPanel {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: row.modelData?.ssid ?? "?"
|
text: rowWrap.modelData?.ssid ?? "?"
|
||||||
color: Theme.text
|
color: Theme.text
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
@ -81,7 +130,7 @@ NsPanel {
|
||||||
font.pixelSize: Theme.font.bodySmall
|
font.pixelSize: Theme.font.bodySmall
|
||||||
}
|
}
|
||||||
StyledText {
|
StyledText {
|
||||||
visible: row.connected
|
visible: rowWrap.connected
|
||||||
text: "Connected"
|
text: "Connected"
|
||||||
color: Theme.accent
|
color: Theme.accent
|
||||||
font.family: Theme.font.family
|
font.family: Theme.font.family
|
||||||
|
|
@ -90,7 +139,7 @@ NsPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
NsIcon {
|
NsIcon {
|
||||||
visible: row.modelData?.isSecure ?? false
|
visible: rowWrap.modelData?.isSecure ?? false
|
||||||
icon: "lock"
|
icon: "lock"
|
||||||
color: Theme.textFaint
|
color: Theme.textFaint
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +150,55 @@ NsPanel {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.activate(rowWrap.modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ pragma ComponentBehavior: Bound
|
||||||
// NoSignal shell-level popup state. "One panel open at a time" — `open` is the
|
// 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
|
// 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.
|
// (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 QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
|
@ -14,19 +18,23 @@ Singleton {
|
||||||
|
|
||||||
property string open: ""
|
property string open: ""
|
||||||
property real anchorX: 0
|
property real anchorX: 0
|
||||||
|
property string screen: ""
|
||||||
|
|
||||||
function toggle(id: string, x: real): void {
|
function toggle(id: string, x: real, screen: string): void {
|
||||||
if (root.open === id) {
|
const scr = screen ?? "";
|
||||||
|
if (root.open === id && root.screen === scr) {
|
||||||
root.open = "";
|
root.open = "";
|
||||||
} else {
|
} else {
|
||||||
root.open = id;
|
root.open = id;
|
||||||
root.anchorX = x;
|
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.open = id;
|
||||||
root.anchorX = x;
|
root.anchorX = x;
|
||||||
|
root.screen = screen ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(): void {
|
function close(): void {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue