nosignal-shell/modules/nsbar/NsBar.qml
28allday e42538896f 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>
2026-06-20 18:49:18 +01:00

260 lines
9.5 KiB
QML

pragma ComponentBehavior: Bound
// NoSignal slim top bar (interface redesign 2026-06-14).
// Flat 38px translucent-glass PanelWindow at the top edge. Three zones:
// left : logo · | · workspaces · | · window-title
// center: clock
// right : media · bell · wifi · bt · volume · battery · | · power
// Each interactive module is a BarPill (hover bg; active = accent.soft + accent
// fg). `openPanel` is the single shell-level "one panel open at a time" string;
// clicking a pill toggles it. The popup panels themselves are built next — for
// now a click just highlights the pill. Reuses the existing service singletons.
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Bluetooth
import Quickshell.Services.UPower
import Quickshell.Services.SystemTray
import qs.services
import qs.utils
import qs.components
import qs.modules.bar.components as BC
import qs.modules.bar.components.workspaces as WS
Variants {
model: Screens.screens
PanelWindow {
id: win
required property ShellScreen modelData
screen: modelData
color: "transparent"
WlrLayershell.namespace: "nsbar" // for Hyprland blur layerrule
WlrLayershell.layer: WlrLayer.Top
anchors.top: true
anchors.left: true
anchors.right: true
implicitHeight: Theme.size.barHeight
exclusiveZone: Theme.size.barHeight
readonly property int hPad: 12
readonly property int gap: 6
// ── Glass surface + 1px bottom border ──────────────────────────────
Rectangle {
anchors.fill: parent
color: Theme.barBg
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 1
color: Theme.barBorder
}
}
// ── Left zone ──────────────────────────────────────────────────────
RowLayout {
anchors.left: parent.left
anchors.leftMargin: win.hPad
anchors.verticalCenter: parent.verticalCenter
spacing: win.gap
BC.OsIcon {}
Divider {}
NsWorkspaces {}
Divider {}
BarPill {
id: titlePill
active: NsShell.open === "overview"
onTriggered: NsShell.toggle("overview", mapToItem(null, width / 2, 0).x, win.modelData.name)
NsIcon {
icon: "desktop_windows"
color: titlePill.active ? Theme.accent : Theme.textMuted
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: Hypr.activeToplevel?.title ?? "Desktop"
elide: Text.ElideRight
width: Math.min(implicitWidth, 220)
color: titlePill.active ? Theme.accent : Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
}
}
}
// ── Center zone: clock ─────────────────────────────────────────────
BarPill {
id: clockPill
anchors.centerIn: parent
active: NsShell.open === "calendar"
onTriggered: NsShell.toggle("calendar", mapToItem(null, width / 2, 0).x, win.modelData.name)
BC.Clock {}
}
// ── Right zone ─────────────────────────────────────────────────────
RowLayout {
anchors.right: parent.right
anchors.rightMargin: win.hPad
anchors.verticalCenter: parent.verticalCenter
spacing: win.gap
// Media / now-playing
BarPill {
id: mediaPill
visible: Players.active !== null
active: NsShell.open === "quicksettings"
onTriggered: NsShell.toggle("quicksettings", mapToItem(null, width / 2, 0).x, win.modelData.name)
NsIcon {
icon: Players.active?.isPlaying ? "pause" : "music_note"
color: mediaPill.active ? Theme.accent : Theme.green
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: Players.active?.trackTitle || "Awake"
elide: Text.ElideRight
width: Math.min(implicitWidth, 90)
color: mediaPill.active ? Theme.accent : Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
}
}
// System tray — only shown when an app actually registers a tray
// icon, otherwise the pill just opened an empty panel (F-T1).
IconPill {
icon: "widgets"
panel: "tray"
visible: SystemTray.items.values.length > 0
}
// Notifications bell (with unread dot overlaid on the icon)
BarPill {
id: bellPill
active: NsShell.open === "notifications"
onTriggered: NsShell.toggle("notifications", mapToItem(null, width / 2, 0).x, win.modelData.name)
NsIcon {
icon: Notifs.notClosed.length > 0 ? "notifications" : "notifications_none"
color: bellPill.active ? Theme.accent : Theme.text
Rectangle {
visible: Notifs.notClosed.length > 0
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -1
anchors.topMargin: 1
implicitWidth: 6
implicitHeight: 6
radius: 0
color: Theme.accent
}
}
}
// Wi-Fi
IconPill {
icon: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off"
panel: "network"
}
// Bluetooth
IconPill {
icon: {
if (!Bluetooth.defaultAdapter?.enabled)
return "bluetooth_disabled";
if (Bluetooth.devices.values.some(d => d.connected))
return "bluetooth_connected";
return "bluetooth";
}
panel: "bluetooth"
}
// Volume
IconPill {
icon: Icons.getVolumeIcon(Audio.volume, Audio.muted)
panel: "audio"
}
// Battery / Quick Settings — always present (control-centre trigger):
// battery + % on laptops, a "tune" icon on desktops.
BarPill {
id: batteryPill
readonly property bool laptop: UPower.displayDevice.isLaptopBattery
active: NsShell.open === "quicksettings"
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"
color: batteryPill.active ? Theme.accent : !batteryPill.laptop || !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? Theme.text : Theme.danger
fill: 1
}
StyledText {
visible: batteryPill.laptop
anchors.verticalCenter: parent.verticalCenter
text: Math.round(UPower.displayDevice.percentage * 100) + "%"
color: batteryPill.active ? Theme.accent : Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
}
}
Divider {}
// Power
IconPill {
icon: "power_settings_new"
panel: "power"
iconColour: Theme.danger
}
}
// ── Reusable bits ──────────────────────────────────────────────────
component Divider: Rectangle {
Layout.alignment: Qt.AlignVCenter
implicitWidth: 1
implicitHeight: 16
color: Theme.barBorder
}
// Icon-only pill bound to the shell openPanel state
component IconPill: BarPill {
id: p
property string icon
property string panel
property color iconColour: Theme.text
active: NsShell.open === panel
onTriggered: NsShell.toggle(panel, mapToItem(null, width / 2, 0).x, win.modelData.name)
NsIcon {
icon: p.icon
color: p.active ? Theme.accent : p.iconColour
}
}
}
}