Files
Panama/config/dot/quickshell/modules/quicksettings/QuickSettingsPanel.qml
T
Gabriel Brown 31668619a4 A shell that comes back, a recording you can hear, a layout you can see
Quickshell segfaulted mid-session -- a Qt image-teardown bug, three cores in
the journal -- and the desktop stayed bar-less until a person noticed and
knew what to type, because the shell ran as a bare compositor child and
panama-crash-watch's report had no notification server left to arrive on.
The shell is now panama-quickshell.service, started per-session by
autostart.lua like every other Panama unit and never enabled globally:
Restart=on-failure turns the same crash into a two-second flicker, verified
by sending the running shell a real SIGSEGV and watching it return, and the
crash report now lands because the restarted shell is serving the bus by the
time the watcher looks. The two contracts that restart the shell learned to
do it through the unit, or the unit's own restart races them with a second
shell.

Recordings can hear: a recorderAudio preference -- none by default, GNOME's
default too, because a screencast that silently captured the microphone is an
incident -- adds system audio or the microphone through PulseAudio's
@DEFAULT_*@ aliases, so the capture follows whatever device Sound settings
has chosen.

The bar shows the active keyboard layout whenever more than one is
configured, mapped from xkb's own registry (evdev.lst) because deriving a
code from a description guesses wrong immediately -- "German" is de, not ge
-- and updated live from Hyprland's activelayout event. One layout, no
indicator, which is GNOME's behavior too.

And presentation mode: Caffeine plus Do Not Disturb as one quick-settings
tile, restoring both exactly as found -- the half you forget to arm before
plugging into a projector is the one that fires a message preview onto the
big screen.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 12:41:36 -04:00

451 lines
17 KiB
QML

// The contents of the quick settings popover — GNOME's system menu, in order:
// toggle grid, detail lists, sliders, footer.
//
// Kept separate from QuickSettings.qml (the PanelWindow) so it can be dropped
// into a FloatingWindow for testing: layer-shell surfaces cannot map outside a
// wlroots compositor.
import QtQuick
import Quickshell
import Quickshell.Widgets
import Quickshell.Networking
import Quickshell.Bluetooth
import Quickshell.Services.Pipewire
import qs.config
import qs.services
import qs.widgets
Item {
id: root
implicitWidth: Theme.controlCenterWidth
implicitHeight: content.implicitHeight + Theme.popoverPadding * 2
// "" | "wifi" | "vpn" | "bluetooth" | "sink" | "source". Only one detail
// list is open at a time, so the panel never grows past the screen.
property string expandedSection: ""
function expand(name: string): void {
root.expandedSection = root.expandedSection === name ? "" : name;
}
// Collapse everything — called when the popover closes so it reopens in a
// known state.
function reset(): void {
root.expandedSection = "";
}
// ── System state ────────────────────────────────────────────────────────
readonly property var wifiDevice: {
for (const device of Networking.devices.values) {
if (device.type === DeviceType.Wifi)
return device;
}
return null;
}
readonly property var wiredDevice: {
for (const device of Networking.devices.values) {
if (device.type === DeviceType.Wired && device.connected)
return device;
}
return null;
}
readonly property string ssid: {
if (!root.wifiDevice || !root.wifiDevice.networks)
return "";
for (const network of root.wifiDevice.networks.values) {
if (network.connected)
return network.name;
}
return "";
}
readonly property var btAdapter: Bluetooth.defaultAdapter
readonly property string btSummary: {
if (!root.btAdapter)
return "Unavailable";
if (!root.btAdapter.enabled)
return "Off";
for (const device of Bluetooth.devices.values) {
if (device.connected)
return device.name || device.address;
}
return "On";
}
readonly property int cellWidth: (content.width - Theme.itemSpacing) / 2
Column {
id: content
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.popoverPadding
spacing: Theme.itemSpacing
// ── Toggle grid ─────────────────────────────────────────────────────
Grid {
columns: 2
spacing: Theme.itemSpacing
Toggle {
width: root.cellWidth
icon: Networking.wifiEnabled ? "network-wireless-symbolic" : "network-wireless-offline-symbolic"
label: "Wi-Fi"
active: Networking.wifiEnabled
enabled: Networking.wifiHardwareEnabled
sublabel: {
if (!Networking.wifiEnabled)
return "Off";
return root.ssid !== "" ? root.ssid : "Not connected";
}
onToggled: Networking.wifiEnabled = !Networking.wifiEnabled
onExpanded: root.expand("wifi")
}
// Only present when there is a wired device at all -- most machines
// this runs on have one, but a laptop without a dock does not, and
// an Ethernet tile there would be a control for absent hardware.
Toggle {
width: root.cellWidth
visible: Connectivity.wiredDevice !== null
icon: Connectivity.wiredOn
? "network-wired-symbolic"
: "network-wired-disconnected-symbolic"
label: "Ethernet"
active: Connectivity.wiredOn
enabled: Connectivity.wiredAvailable
sublabel: {
if (Connectivity.wiredOn)
return Connectivity.wiredDevice.linkSpeed > 0
? Connectivity.wiredDevice.linkSpeed + " Mb/s" : "Connected";
return "Off";
}
onToggled: Connectivity.setWired(!Connectivity.wiredOn)
}
// Only when a VPN profile is saved at all: a machine with none has
// nothing to toggle, and the tile would be a control for absent
// configuration -- same reasoning as the Ethernet tile above.
Toggle {
width: root.cellWidth
visible: Vpn.available
icon: "network-vpn-symbolic"
label: "VPN"
active: Vpn.anyActive
enabled: !Vpn.busy
sublabel: {
if (Vpn.busy)
return "Working…";
return Vpn.anyActive ? Vpn.activeSummary : "Off";
}
onToggled: Vpn.toggle()
onExpanded: root.expand("vpn")
}
Toggle {
width: root.cellWidth
icon: root.btAdapter && root.btAdapter.enabled ? "bluetooth-active-symbolic" : "bluetooth-disabled-symbolic"
label: "Bluetooth"
active: root.btAdapter !== null && root.btAdapter.enabled
enabled: root.btAdapter !== null
sublabel: root.btSummary
onToggled: {
if (root.btAdapter)
root.btAdapter.enabled = !root.btAdapter.enabled;
}
onExpanded: root.expand("bluetooth")
}
Toggle {
width: root.cellWidth
icon: "preferences-desktop-screensaver-symbolic"
label: "Caffeine"
sublabel: Caffeine.enabled ? "Staying awake" : "Off"
active: Caffeine.enabled
onToggled: Caffeine.toggle()
}
// Caffeine plus Do Not Disturb as one switch, for the projector:
// the half you forget to arm is the one that fires a message
// preview onto the big screen. Restores both exactly as found.
Toggle {
width: root.cellWidth
icon: "video-display-symbolic"
label: "Presentation"
sublabel: PresentationMode.active ? "Awake · notifications held" : "Off"
active: PresentationMode.active
onToggled: PresentationMode.toggle()
}
Toggle {
width: root.cellWidth
icon: ColorScheme.dark ? "weather-clear-night-symbolic" : "weather-clear-symbolic"
label: "Appearance"
sublabel: ColorScheme.dark ? "Dark" : "Light"
// "active" reads as the non-default state across this grid, and
// dark is what Panama ships, so light is the lit one.
active: !ColorScheme.dark
// Writes the preference and stops. ColorScheme propagates it to
// GTK, the portal, the terminals, the launcher, btop, tmux and
// the lock screen; nothing here needs to know that list.
onToggled: SystemSettings.commitPreference("colorScheme",
ColorScheme.dark ? "light" : "dark")
}
Toggle {
width: root.cellWidth
icon: "night-light-symbolic"
label: "Night Light"
active: NightLight.active
sublabel: {
if (NightLight.automatic)
return NightLight.active ? "Scheduled · on" : "Scheduled";
return NightLight.active ? NightLight.temperature + "K" : "Off";
}
onToggled: NightLight.toggle()
// No detail list to open, so the secondary action flips the
// schedule on and off instead.
onExpanded: NightLight.automatic = !NightLight.automatic
}
}
RowButton {
width: content.width
icon: "preferences-system-time-symbolic"
iconFallback: "appointment-soon-symbolic"
label: FocusSession.active ? "Focus · " + FocusSession.workspaceLabel : "Start focus session"
sublabel: FocusSession.active ? FocusSession.remainingText + (FocusSession.paused ? " · paused" : " remaining") : Settings.focusDurationMinutes + " minutes · current workspace"
selected: FocusSession.active
onClicked: {
ShellState.close();
FocusSession.reveal();
}
}
// ── Detail lists ────────────────────────────────────────────────────
Section {
width: content.width
expanded: root.expandedSection === "wifi"
WifiList {
anchors.left: parent.left
anchors.right: parent.right
device: root.wifiDevice
active: root.expandedSection === "wifi"
}
}
Section {
width: content.width
expanded: root.expandedSection === "vpn"
VpnList {
anchors.left: parent.left
anchors.right: parent.right
}
}
Section {
width: content.width
expanded: root.expandedSection === "bluetooth"
BluetoothList {
anchors.left: parent.left
anchors.right: parent.right
active: root.expandedSection === "bluetooth"
}
}
Rectangle {
width: content.width
height: 1
color: Theme.alpha(Theme.fg, 0.1)
}
// ── Power ───────────────────────────────────────────────────────────
// Only where a power-profiles daemon is actually running. A desktop
// without one should not show a control that cannot do anything.
RowButton {
visible: PowerProfiles.available
width: content.width
icon: {
switch (PowerProfiles.active) {
case "power-saver": return "power-profile-power-saver-symbolic";
case "performance": return "power-profile-performance-symbolic";
default: return "power-profile-balanced-symbolic";
}
}
iconFallback: "preferences-system-power-symbolic"
label: "Power profile"
sublabel: PowerProfiles.degraded !== ""
? PowerProfiles.label(PowerProfiles.active) + " · limited"
: PowerProfiles.label(PowerProfiles.active)
selected: root.expandedSection === "power"
onClicked: root.expand("power")
}
Section {
width: content.width
expanded: root.expandedSection === "power"
PowerProfileList {
anchors.left: parent.left
anchors.right: parent.right
}
}
// ── Sliders ─────────────────────────────────────────────────────────
AudioSlider {
width: content.width
node: Pipewire.defaultAudioSink
output: true
expanded: root.expandedSection === "sink"
onToggleExpanded: root.expand("sink")
}
Section {
width: content.width
expanded: root.expandedSection === "sink"
AudioDeviceList {
anchors.left: parent.left
anchors.right: parent.right
output: true
}
}
AudioSlider {
width: content.width
node: Pipewire.defaultAudioSource
output: false
expanded: root.expandedSection === "source"
onToggleExpanded: root.expand("source")
}
Section {
width: content.width
expanded: root.expandedSection === "source"
AudioDeviceList {
anchors.left: parent.left
anchors.right: parent.right
output: false
}
}
BrightnessControl {
width: content.width
}
// ── Connected life ─────────────────────────────────────────────────
HomeControls {
width: content.width
expanded: root.expandedSection === "home"
onToggleExpanded: root.expand("home")
}
PhoneControls {
width: content.width
expanded: root.expandedSection === "phone"
onToggleExpanded: root.expand("phone")
}
Rectangle {
width: content.width
height: 1
color: Theme.alpha(Theme.fg, 0.1)
}
// ── Footer ──────────────────────────────────────────────────────────
Item {
width: content.width
height: 36
// The account's own picture, the same file the lock screen and the
// login screen read. A generic glyph sat here while a real avatar
// was already set, which made the desktop look like it did not know
// whose it was.
Item {
id: avatar
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
width: 22
height: 22
ClippingRectangle {
anchors.fill: parent
radius: width / 2
color: "transparent"
visible: UserAccounts.avatarUrl !== ""
Image {
anchors.fill: parent
source: UserAccounts.avatarUrl
fillMode: Image.PreserveAspectCrop
// Replaced in place when the picture changes, so the
// cache has to be told to let go of the old one.
cache: false
asynchronous: true
sourceSize.width: 44
sourceSize.height: 44
}
}
ThemedIcon {
anchors.centerIn: parent
visible: UserAccounts.avatarUrl === ""
size: 20
icon: "avatar-default-symbolic"
iconFallback: "user-info-symbolic"
}
}
Text {
anchors.left: avatar.right
anchors.leftMargin: 10
anchors.right: actions.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
text: UserAccounts.me
? UserAccounts.displayName(UserAccounts.me)
: (Quickshell.env("USER") || "user")
color: Theme.fg
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.Medium
}
Row {
id: actions
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 4
IconButton {
size: 32
iconSize: 17
icon: "preferences-system-symbolic"
onClicked: {
ShellState.close();
ShellState.openSettings("home");
}
}
IconButton {
size: 32
iconSize: 17
icon: "system-shutdown-symbolic"
onClicked: ShellState.open("powermenu")
}
}
}
}
}