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 --------------------------------------------------------------
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,16 +79,32 @@ 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
|
||||
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
|
||||
implicitHeight: 40
|
||||
radius: Theme.radius.button
|
||||
color: connected ? Theme.accentSoft : ma.containsMouse ? Theme.hover : "transparent"
|
||||
color: rowWrap.connected ? Theme.accentSoft : ma.containsMouse ? Theme.hover : "transparent"
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
|
@ -64,8 +113,8 @@ NsPanel {
|
|||
spacing: 10
|
||||
|
||||
NsIcon {
|
||||
icon: Icons.getNetworkIcon(row.modelData?.strength ?? 0)
|
||||
color: row.connected ? Theme.accent : Theme.text
|
||||
icon: Icons.getNetworkIcon(rowWrap.modelData?.strength ?? 0)
|
||||
color: rowWrap.connected ? Theme.accent : Theme.text
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
|
@ -73,7 +122,7 @@ NsPanel {
|
|||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
text: row.modelData?.ssid ?? "?"
|
||||
text: rowWrap.modelData?.ssid ?? "?"
|
||||
color: Theme.text
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
|
|
@ -81,7 +130,7 @@ NsPanel {
|
|||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
StyledText {
|
||||
visible: row.connected
|
||||
visible: rowWrap.connected
|
||||
text: "Connected"
|
||||
color: Theme.accent
|
||||
font.family: Theme.font.family
|
||||
|
|
@ -90,7 +139,7 @@ NsPanel {
|
|||
}
|
||||
|
||||
NsIcon {
|
||||
visible: row.modelData?.isSecure ?? false
|
||||
visible: rowWrap.modelData?.isSecure ?? false
|
||||
icon: "lock"
|
||||
color: Theme.textFaint
|
||||
}
|
||||
|
|
@ -101,6 +150,55 @@ NsPanel {
|
|||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
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
|
||||
// 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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue