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
451 lines
17 KiB
QML
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")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|