Desktop & Dock becomes Shell — Bar, Dock, Control Center, Tiling, Workspaces — the home for everything Quickshell draws. The settings- management cluster moves to System as Sync & Backup, Appearance's Shell tab dissolves, and 24-hour time finally lives on Date & Time, which always owned it. The bar gets what it never had: a way to survive the wallpaper. A second neutral text family (follow theme, or forced light or dark), a one-layer shadow under every glyph, and a gradient scrim for wallpapers nothing else survives — all off by default, pixel-identical until asked. Widgets earn toggles (weather, media, clipboard, calendar countdown), the vitals cluster stops leaving a dead pill behind, and Control Center's sections learn to step aside. The dock graduates from MVP: a context menu with window rows, pin, unpin, quit and new-window; scroll an icon to cycle its windows; drag to reorder on the dock itself; hover previews with one-shot captures; and "Add App to Dock" in the launcher. Three real bugs died en route — menus that slid away with the autohide, a readonly-property crash on every menu open, and a drag that drifted half a slot per icon on side docks. The pinned-apps editor in Settings becomes a drag strip. 166 contracts; the full suite is green except two live display and switcher tests that cannot run behind a locked session — re-verified on unlock. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
189 lines
7.7 KiB
QML
189 lines
7.7 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.barFg;
|
|
if (!root.wifiNetwork)
|
|
return Theme.barFgMuted;
|
|
return Networking.connectivity === NetworkConnectivity.Full ? Theme.barFg : Theme.warn;
|
|
}
|
|
}
|
|
|
|
// The active layout, only where there is a choice to indicate: a machine
|
|
// with one layout knows what its keys say. GNOME shows the same short
|
|
// code in the same corner, which is the muscle memory this preserves.
|
|
Text {
|
|
visible: KeyboardLayout.multiple
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: KeyboardLayout.shortLabel
|
|
color: Theme.barFg
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.weight: Font.Medium
|
|
font.letterSpacing: 0.5
|
|
}
|
|
|
|
// 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.barFgMuted : Theme.barFg
|
|
}
|
|
|
|
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.barFg
|
|
}
|
|
|
|
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.
|
|
//
|
|
// One color for the icon and the number beside it: two different colors
|
|
// would read as two indicators.
|
|
readonly property color batteryColor: {
|
|
if (Battery.critical) return Theme.danger;
|
|
if (Battery.low) return Theme.warn;
|
|
if (Battery.charging) return Theme.ok;
|
|
return Theme.barFg;
|
|
}
|
|
|
|
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: root.batteryColor
|
|
}
|
|
|
|
// The exact number, for the people who ask the icon to be more specific --
|
|
// GNOME's "Show Battery Percentage", living under the same gates as the
|
|
// icon it annotates.
|
|
Text {
|
|
visible: Settings.showBattery && Settings.showBatteryPercent && Battery.available
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: Math.round(Battery.percent) + "%"
|
|
color: root.batteryColor
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Theme.durFast
|
|
}
|
|
}
|
|
}
|
|
}
|