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
This commit is contained in:
Gabriel Brown
2026-08-20 00:03:21 -04:00
parent f6b970da21
commit 32fab59d24
7 changed files with 72 additions and 10 deletions
@@ -91,6 +91,12 @@ Singleton {
label: "Automatically hide the Dock",
detail: "Reveal it at the bottom edge when a workspace is occupied"
},
{
key: "dockIconSize", type: "int", def: 48, min: 32, max: 80, step: 4,
unit: "px", group: "dock",
label: "Icon size",
detail: "How large the Dock's application icons are drawn"
},
{
key: "dockRevealDelayMs", type: "int", def: 0, min: 0, max: 1000, step: 25,
unit: "ms",
+1 -1
View File
@@ -126,7 +126,7 @@ Singleton {
readonly property int barGap: 6 // breathing room below the bar for popovers
readonly property int barSideMargin: 10 // inset for floating popovers
readonly property int dockIconSize: 48
readonly property int dockIconSize: DesktopPreferences.get("dockIconSize")
readonly property int dockPadding: 8
readonly property int dockGap: 8
readonly property int dockRadius: 20
@@ -22,7 +22,8 @@ SettingsPage {
ToggleRow { setting: "dockAutohide" }
SliderRow { setting: "dockRevealDelayMs"; zeroLabel: "Instant" }
SliderRow { setting: "dockHideDelayMs"; zeroLabel: "Instant"; divider: false }
SliderRow { setting: "dockHideDelayMs"; zeroLabel: "Instant" }
SliderRow { setting: "dockIconSize"; divider: false }
}
SettingsCard {
@@ -89,13 +89,27 @@ Rectangle {
}
}
SettingsButton {
id: useButton
Row {
anchors.verticalCenter: parent.verticalCenter
width: root.selected ? 78 : 64
text: root.selected ? "Default" : "Use"
enabled: !root.selected
onClicked: AudioDevices.select(root.output, root.node)
spacing: 7
// Outputs only. Testing an input would mean recording and playing
// it back, which is a different thing than this button implies.
SettingsButton {
anchors.verticalCenter: parent.verticalCenter
visible: root.output
text: "Test"
onClicked: SoundTest.play(root.node)
}
SettingsButton {
id: useButton
anchors.verticalCenter: parent.verticalCenter
width: root.selected ? 78 : 64
text: root.selected ? "Default" : "Use"
enabled: !root.selected
onClicked: AudioDevices.select(root.output, root.node)
}
}
}
@@ -0,0 +1,40 @@
pragma Singleton
// Playing a short sound out of one chosen output.
//
// Deliberately not part of AudioDevices, which owns device state through the
// Quickshell PipeWire bindings and must not shell out -- doing so there would
// race the service that owns those same objects. This spawns a short-lived
// playback client instead: it creates its own stream and mutates no device, so
// there is nothing for it to race.
//
// It exists because nine outputs named after their chipsets cannot be told
// apart by reading. The only way to know which is which is to hear one.
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
// A short, unmistakable, front-and-centre sample that ships with the
// freedesktop sound theme, so nothing has to be bundled.
readonly property string sample:
"/usr/share/sounds/freedesktop/stereo/audio-channel-front-center.oga"
readonly property bool playing: player.running
// Targeted by node name taken straight from the live node. pw-play falls
// back to the default output for a target it cannot find, so a stale name
// would play out of the wrong device and look like the test had worked.
function play(node: var): void {
const target = String(node?.name ?? "");
if (target === "" || player.running)
return;
player.command = ["pw-play", "--target", target, root.sample];
player.running = true;
}
Process { id: player }
}