Files
Panama/config/dot/quickshell/modules/settings/DesktopPage.qml
T
Gabriel Brown 32fab59d24 Let a sound device be heard, and the Dock's icons be sized
Nine outputs named after their chipsets cannot be told apart by reading, so each
one gets a Test button that plays a short sample out of that device. Targeted by
node name rather than by making it the default first, because finding out which
is which should not move where everything else is playing.

That belongs in its own service rather than in AudioDevices. sound-page-contract
forbids Process, pactl and wpctl in the files that own device state, and it is
right to: shelling out there races the PipeWire service that owns those same
objects. Playback is a different thing -- pw-play opens its own stream and
mutates no device, so there is nothing to race -- but the rule's letter covered
it, and weakening a guard to fit a new case is how guards stop meaning anything.
SoundTest exists so AudioDevices stays native bindings only.

Worth recording next to the call: pw-play falls back to the default output for a
target it cannot find, rather than failing. A stale node name would play from
the wrong device and look exactly like a successful test, which is why the name
is taken straight from the live node.

The Dock's icon size was a constant in Theme. It goes through the preference
schema like everything else, so validation, search, the generated docs and the
write sweep all pick it up without being told about it separately -- and two
contracts duly failed until docs/settings.md and the per-page commands were
regenerated.

Dock position is deliberately not here. It is not a setting but a rework: the
dock is anchored bottom, and the reveal strip, tooltip placement, intellihide
and the qs-dock rule in hypr/rules.lua all assume that. Doing it properly means
changing compositor rules on a machine somebody uses daily, which is not
something to start as a side effect of adding a slider.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-20 00:03:21 -04:00

174 lines
6.4 KiB
QML

// Desktop & Dock.
//
// The dock timings used to be shown here as text -- "Instant", "250 ms" -- even
// though they were already stored, mutable integers. They are controls now.
// The window-layout card keeps text rows because those really are facts about
// how Panama tiles rather than settings: the adjustable parts of window
// appearance live on the Appearance page, next to the preview that explains
// them.
import QtQuick
import qs.config
import qs.services
SettingsPage {
id: root
title: "Desktop & Dock"
lede: "Keep the shell instant, spatial, and out of your way."
SettingsCard {
title: "Dock"
ToggleRow { setting: "dockAutohide" }
SliderRow { setting: "dockRevealDelayMs"; zeroLabel: "Instant" }
SliderRow { setting: "dockHideDelayMs"; zeroLabel: "Instant" }
SliderRow { setting: "dockIconSize"; divider: false }
}
SettingsCard {
title: "Pinned applications"
subtitle: "What sits in the Dock whether or not it is running. Order here is the order on screen."
DockPinsEditor {
id: pins
width: parent.width
}
}
SettingsCard {
title: "Pin another application"
DockAppPicker {
width: parent.width
pinned: pins.pinned
onPicked: id => pins.add(id)
}
}
SettingsCard {
title: "Window layout"
subtitle: "Follows the Forge mental model, with native Hyprland tiling."
// These were two read-only rows reporting "Tiling" and "Dynamic", which
// described settings rather than facts -- both are ordinary Hyprland
// options that simply had no controls. TextRow's own documentation says
// a setting the user could reasonably change does not belong in it.
ChoiceRow { setting: "windowLayout" }
ToggleRow { setting: "preserveSplit" }
ChoiceRow { setting: "forceSplit" }
ToggleRow { setting: "windowSnapping" }
ActionRow {
label: "Gaps, corners, and effects"
detail: "Adjusted on the Appearance page, beside a live preview"
action: "Open Appearance"
divider: false
onTriggered: ShellState.openSettings("appearance")
}
}
// GNOME's Multitasking panel, in Hyprland's terms.
// Only meaningful when the layout above is Master and stack. Hidden
// otherwise, because a card of settings that do nothing under the layout
// you are actually running is worse than not offering the layout at all.
SettingsCard {
visible: DesktopPreferences.get("windowLayout") === "master"
title: "Master and stack"
subtitle: "How the master area behaves. These apply only while the tiling layout above is Master and stack."
SliderRow { setting: "masterFactor" }
ChoiceRow { setting: "masterOrientation" }
ChoiceRow { setting: "masterNewStatus" }
ToggleRow { setting: "masterNewOnTop"; divider: false }
}
SettingsCard {
title: "Window edges"
subtitle: "How the pointer grabs a window's border, and how floating windows behave near each other and the screen edge."
ToggleRow { setting: "resizeOnBorder" }
SliderRow { setting: "borderGrabArea"; zeroLabel: "Border only" }
ToggleRow { setting: "hoverIconOnBorder" }
SliderRow { setting: "snapWindowGap"; zeroLabel: "Touching" }
SliderRow { setting: "snapMonitorGap"; zeroLabel: "Touching" }
ToggleRow { setting: "snapRespectGaps"; divider: false }
}
// Hyprland's own interruptions. Panama turns all four off, which is a
// defensible default and was not previously a decision anyone could
// reverse without editing looks.lua.
SettingsCard {
title: "Hyprland notices"
subtitle: "Panama hides all of these by default. They are the compositor's own, not Panama's."
ToggleRow { setting: "hyprlandLogo" }
ToggleRow { setting: "hyprlandSplash" }
ToggleRow { setting: "hyprlandUpdateNews" }
ToggleRow { setting: "hyprlandDonationNag"; divider: false }
}
SettingsCard {
title: "Workspaces & focus"
subtitle: "Hyprland's workspaces are created and destroyed as you use them, so there is no fixed count to set."
ToggleRow { setting: "workspaceBackAndForth" }
ToggleRow { setting: "allowWorkspaceCycles" }
ToggleRow { setting: "focusOnActivate" }
ToggleRow { setting: "mouseMoveFocusesMonitor"; divider: false }
}
SettingsCard {
title: "Focus"
SliderRow { setting: "focusDurationMinutes"; divider: false }
}
SettingsCard {
title: "Snapshots"
subtitle: SettingsBackup.lastError !== ""
? SettingsBackup.lastError
: "Your whole desktop configuration is one file, so a snapshot is a copy of it. Restoring also snapshots what it replaces, so it is itself undoable."
ActionRow {
label: "Back up current settings"
detail: SettingsBackup.snapshots.length === 0
? "No snapshots yet"
: SettingsBackup.snapshots.length + (SettingsBackup.snapshots.length === 1 ? " snapshot kept" : " snapshots kept") + ", newest first"
action: "Back up now"
enabled: !SettingsBackup.busy
divider: SettingsBackup.snapshots.length > 0
onTriggered: SettingsBackup.save()
}
Repeater {
id: snapshotRows
model: SettingsBackup.snapshots
ActionRow {
required property var modelData
required property int index
label: modelData.when
detail: modelData.keys + " settings"
action: "Restore"
enabled: !SettingsBackup.busy
divider: index < snapshotRows.count - 1
onTriggered: SettingsBackup.restore(modelData.name)
}
}
}
SettingsCard {
title: "Reset"
subtitle: "Restores the appearance, dock, clock, focus, and display policy, and clears your Home accessory arrangement. Pinned applications, files, and paired devices are not changed."
ActionRow {
label: "Restore defaults"
detail: "Applies immediately, including to the compositor"
action: "Restore defaults"
divider: false
onTriggered: SystemSettings.restoreDefaults()
}
}
}