Turning on a WireGuard profile whose server was unreachable used to cost the whole network stack, and the only way out was nmcli typed into a terminal. Quickshell's Networking module has no VPN surface, so this arrives as the one sanctioned nmcli exception: a helper that lists, raises and lowers profiles, a service that watches NetworkManager for changes made anywhere, a quick settings tile (left-click toggles the most recently used profile, right-click picks among them), and a bar glyph while a tunnel is up. The safety property is in the helper, where it cannot be skipped: activation waits a bounded 25 seconds, and a failure is rolled back down and reported instead of leaving a black-hole default route. The contract pins exactly that, against a stateful stub NetworkManager. Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
152 lines
6.3 KiB
QML
152 lines
6.3 KiB
QML
// GNOME's system menu button: a small run of status glyphs — network, volume,
|
|
// bluetooth, caffeine — that opens the quick settings panel when clicked.
|
|
//
|
|
// This widget only *reports* state. The quick settings panel itself lives
|
|
// elsewhere; clicking here just raises `requestQuickSettings`.
|
|
|
|
import Quickshell
|
|
import Quickshell.Bluetooth
|
|
import Quickshell.Networking
|
|
import Quickshell.Services.Pipewire
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
import qs.widgets
|
|
|
|
Pill {
|
|
id: root
|
|
|
|
signal requestQuickSettings
|
|
|
|
// Set by the shell when an idle inhibitor is held. Lives outside this
|
|
// widget because the inhibitor itself is owned by the quick settings side.
|
|
property bool idleInhibited: false
|
|
|
|
onActivated: root.requestQuickSettings()
|
|
|
|
// Right-click opens the settings that govern this widget. Network and Bluetooth, which is most of what these glyphs report.
|
|
onSecondaryActivated: ShellState.openSettings("connectivity")
|
|
|
|
// ── Audio ───────────────────────────────────────────────────────────────
|
|
// Without a tracker, volume and muted silently read as zero/false.
|
|
PwObjectTracker {
|
|
objects: [Pipewire.defaultAudioSink]
|
|
}
|
|
|
|
// defaultAudioSink is transiently null while pipewire settles.
|
|
readonly property real volume: Pipewire.defaultAudioSink?.audio?.volume ?? 0
|
|
readonly property bool muted: Pipewire.defaultAudioSink?.audio?.muted ?? false
|
|
|
|
// ── Network ─────────────────────────────────────────────────────────────
|
|
readonly property var devices: Networking.devices ? Networking.devices.values : []
|
|
|
|
readonly property var wiredDevice: root.devices.find(d => d.type === DeviceType.Wired && d.connected) ?? null
|
|
readonly property var wifiDevice: root.devices.find(d => d.type === DeviceType.Wifi) ?? null
|
|
|
|
readonly property var wifiNetwork: {
|
|
const networks = root.wifiDevice?.networks ? root.wifiDevice.networks.values : [];
|
|
return networks.find(n => n.connected) ?? null;
|
|
}
|
|
|
|
// ── Bluetooth ───────────────────────────────────────────────────────────
|
|
readonly property bool bluetoothOn: Bluetooth.defaultAdapter?.enabled ?? false
|
|
readonly property bool bluetoothConnected: {
|
|
const devices = Bluetooth.devices ? Bluetooth.devices.values : [];
|
|
return devices.some(d => d.connected);
|
|
}
|
|
|
|
StatusGlyph {
|
|
glyph: {
|
|
if (root.wiredDevice)
|
|
return "\u{F0200}"; // md-ethernet
|
|
if (!root.wifiNetwork)
|
|
return "\u{F092D}"; // md-wifi_strength_off
|
|
|
|
// NetworkManager reports strength 0-100; some backends normalize to
|
|
// 0-1, so accept either rather than showing one bar forever.
|
|
const raw = root.wifiNetwork.signalStrength ?? 0;
|
|
const strength = raw <= 1 ? raw * 100 : raw;
|
|
if (strength >= 75)
|
|
return "\u{F0928}"; // md-wifi_strength_4
|
|
if (strength >= 50)
|
|
return "\u{F0925}"; // md-wifi_strength_3
|
|
if (strength >= 25)
|
|
return "\u{F0922}"; // md-wifi_strength_2
|
|
return "\u{F091F}"; // md-wifi_strength_1
|
|
}
|
|
|
|
color: {
|
|
if (root.wiredDevice)
|
|
return Theme.fg;
|
|
if (!root.wifiNetwork)
|
|
return Theme.fgMuted;
|
|
return Networking.connectivity === NetworkConnectivity.Full ? Theme.fg : Theme.warn;
|
|
}
|
|
}
|
|
|
|
// A tunnel that is up changes what every connection means, so it earns a
|
|
// permanent glyph while active -- and its absence is the resting state,
|
|
// same shape as Bluetooth below.
|
|
StatusGlyph {
|
|
visible: Vpn.anyActive
|
|
glyph: "\u{F0582}" // md-vpn
|
|
color: Theme.accent
|
|
}
|
|
|
|
StatusGlyph {
|
|
glyph: {
|
|
if (root.muted || root.volume <= 0)
|
|
return "\u{F075F}"; // md-volume_mute
|
|
if (root.volume < 0.34)
|
|
return "\u{F057F}"; // md-volume_low
|
|
if (root.volume < 0.67)
|
|
return "\u{F0580}"; // md-volume_medium
|
|
return "\u{F057E}"; // md-volume_high
|
|
}
|
|
color: root.muted ? Theme.fgMuted : Theme.fg
|
|
}
|
|
|
|
StatusGlyph {
|
|
// Bluetooth off is the resting state on this machine, so the glyph is
|
|
// simply absent rather than shown crossed out.
|
|
visible: root.bluetoothOn
|
|
glyph: root.bluetoothConnected ? "\u{F00B1}" : "\u{F00AF}" // md-bluetooth_connect / md-bluetooth
|
|
color: root.bluetoothConnected ? Theme.accent : Theme.fg
|
|
}
|
|
|
|
StatusGlyph {
|
|
visible: root.idleInhibited
|
|
glyph: "\u{F0176}" // md-coffee
|
|
color: Theme.warn
|
|
}
|
|
|
|
StatusGlyph {
|
|
visible: KdeConnect.phoneReachable
|
|
glyph: "\u{F03F2}" // md-cellphone-link
|
|
color: Theme.cyan
|
|
}
|
|
|
|
// Battery. Absent entirely on a desktop: `available` is false until a
|
|
// battery has actually been read, so this is not a zero that looks like a
|
|
// flat cell. Same shape as the graphics field in VitalsWidget, which gates
|
|
// on both the preference and the hardware.
|
|
StatusGlyph {
|
|
visible: Settings.showBattery && Battery.available
|
|
glyph: {
|
|
if (Battery.charging)
|
|
return "\u{F0084}"; // md-battery_charging
|
|
const level = Math.round(Battery.percent / 10) * 10;
|
|
if (level >= 100) return "\u{F0079}"; // md-battery
|
|
if (level <= 0) return "\u{F008E}"; // md-battery_outline
|
|
// md-battery_10 .. md-battery_90 are consecutive from F007A.
|
|
return String.fromCodePoint(0xF007A + (level / 10) - 1);
|
|
}
|
|
color: {
|
|
if (Battery.critical) return Theme.danger;
|
|
if (Battery.low) return Theme.warn;
|
|
if (Battery.charging) return Theme.ok;
|
|
return Theme.fg;
|
|
}
|
|
}
|
|
}
|