nosignal-shell/modules/nsbar/NsBar.qml
28allday 53c5833de0 fix(nosignal): hardware-test feedback round 2 — weather, dashboard, tray
From the 2026-06-15 on-box hardware test (all 6 prior fixes confirmed; these
are the new findings, baked into the fork source rather than shipped as
pacman-hook patches, per the Phase-2 "we own the shell" approach):

- F-W1 (Weather.qml): fetch at startup. Stock only called reload() on a config
  CHANGE, so a saved location blanked after reboot and an empty/auto location
  never populated on a fresh boot. Add Component.onCompleted: reload() and
  switch the hourly Timer to reload() (re-detects auto/IP too).
- F-W3 (Weather.qml): 7-day forecast showed "undefined°" in °F mode — only
  maxTempC/minTempC were stored. Store rounded maxTempF/minTempF (and round °C).
- F-W2 (Weather.qml): empty/auto-IP location never populated — the in-shell
  ipinfo.io fetch didn't set loc (provider-specific; the city path works via
  the same Requests.get). Repair attempt: defensive JSON parse + error callback
  + fallback to geojs.io. Compiled Requests module not inspectable, so this is
  best-effort; a saved city still works regardless.
- F-D1a (modules/dashboard/*.qml): square the 25 card radii bound to the
  compiled Tokens.rounding.* Material defaults (which ignore shell.json
  rounding.scale=0) so the dashboard matches the square redesign.
- F-D1b (Glyphs.qml): remap the weather glyphs to the Material-weather family
  (every codepoint verified present in JetBrainsMono Nerd Font Mono); the old
  Font-Awesome codepoints were absent (rendered "?") and thunderstorm /
  snowing_heavy were unmapped.
- F-T1 (NsBar.qml): gate the system-tray pill on SystemTray.items count so it
  only shows when an app registers a tray icon (was opening an empty panel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:04:28 +01:00

260 lines
9.3 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)
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)
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)
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)
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)
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)
NsIcon {
icon: p.icon
color: p.active ? Theme.accent : p.iconColour
}
}
}
}