Try to tackle crash bug from hyprland locking due to open pipewiressss

This commit is contained in:
David Heinemeier Hansson
2026-05-22 18:59:24 +02:00
parent 07cebed06d
commit 20cfa2f84e
+65 -22
View File
@@ -107,6 +107,15 @@ Panel {
return list return list
} }
// Feed Repeaters with panel-local snapshots instead of the live PipeWire
// model. PipeWire can remove nodes while Quickshell is dispatching the
// removal signal; rebuilding a Repeater from that signal path has crashed
// in Quickshell's PipeWire service. The snapshot timer lets that mutation
// settle first, and closed panels keep their repeaters detached entirely.
property var displayAudioSinks: []
property var displayAudioSources: []
property var displayAudioStreams: []
readonly property real outputVolume: sink && sink.audio ? sink.audio.volume : 0 readonly property real outputVolume: sink && sink.audio ? sink.audio.volume : 0
readonly property bool outputMuted: sink && sink.audio ? sink.audio.muted : false readonly property bool outputMuted: sink && sink.audio ? sink.audio.muted : false
readonly property real inputVolume: source && source.audio ? source.audio.volume : 0 readonly property real inputVolume: source && source.audio ? source.audio.volume : 0
@@ -137,16 +146,16 @@ Panel {
: "transparent" : "transparent"
function sectionCount(section) { function sectionCount(section) {
if (section === "output") return audioSinks.length if (section === "output") return displayAudioSinks.length
if (section === "input") return audioSources.length if (section === "input") return displayAudioSources.length
if (section === "streams") return audioStreams.length if (section === "streams") return displayAudioStreams.length
return 0 return 0
} }
function sectionVisible(section) { function sectionVisible(section) {
if (section === "output") return true if (section === "output") return true
if (section === "input") return audioSources.length > 0 || !!source if (section === "input") return displayAudioSources.length > 0 || !!source
if (section === "streams") return audioStreams.length > 0 if (section === "streams") return displayAudioStreams.length > 0
return false return false
} }
@@ -221,8 +230,8 @@ Panel {
setInputVolume(inputVolume + delta) setInputVolume(inputVolume + delta)
return return
} }
if (focusSection === "streams" && selectedIndex >= 0 && selectedIndex < audioStreams.length) { if (focusSection === "streams" && selectedIndex >= 0 && selectedIndex < displayAudioStreams.length) {
var s = audioStreams[selectedIndex] var s = displayAudioStreams[selectedIndex]
if (s && s.audio) s.audio.volume = Math.max(0, Math.min(1.5, s.audio.volume + delta)) if (s && s.audio) s.audio.volume = Math.max(0, Math.min(1.5, s.audio.volume + delta))
} }
} }
@@ -231,35 +240,62 @@ Panel {
function activateCursor() { function activateCursor() {
if (focusSection === "output") { if (focusSection === "output") {
if (selectedIndex === -1) { toggleOutputMute(); return } if (selectedIndex === -1) { toggleOutputMute(); return }
var sink = audioSinks[selectedIndex] var sink = displayAudioSinks[selectedIndex]
if (sink) setDefaultSink(sink) if (sink) setDefaultSink(sink)
return return
} }
if (focusSection === "input") { if (focusSection === "input") {
if (selectedIndex === -1) { toggleInputMute(); return } if (selectedIndex === -1) { toggleInputMute(); return }
var src = audioSources[selectedIndex] var src = displayAudioSources[selectedIndex]
if (src) setDefaultSource(src) if (src) setDefaultSource(src)
return return
} }
if (focusSection === "streams" && selectedIndex >= 0) { if (focusSection === "streams" && selectedIndex >= 0) {
var st = audioStreams[selectedIndex] var st = displayAudioStreams[selectedIndex]
if (st && st.audio) st.audio.muted = !st.audio.muted if (st && st.audio) st.audio.muted = !st.audio.muted
} }
} }
onOpenedChanged: { onOpenedChanged: {
if (opened) { if (opened) {
refreshDisplayAudioModels()
focusSection = "output" focusSection = "output"
selectedIndex = -1 // first keyboard cursor reveal starts on the output slider selectedIndex = -1 // first keyboard cursor reveal starts on the output slider
cursorActive = false cursorActive = false
Qt.callLater(resetScroll) Qt.callLater(resetScroll)
} else {
clearDisplayAudioModels()
} }
} }
// Clamp / repair the cursor whenever any list refreshes underneath us. // Clamp / repair the cursor whenever any list refreshes underneath us.
onAudioSinksChanged: clampCursor() onAudioSinksChanged: scheduleDisplayAudioModelRefresh()
onAudioSourcesChanged: clampCursor() onAudioSourcesChanged: scheduleDisplayAudioModelRefresh()
onAudioStreamsChanged: clampCursor() onAudioStreamsChanged: scheduleDisplayAudioModelRefresh()
function listSnapshot(list) {
return list && list.slice ? list.slice() : []
}
function refreshDisplayAudioModels() {
if (!opened) return
displayAudioSinks = listSnapshot(audioSinks)
displayAudioSources = listSnapshot(audioSources)
displayAudioStreams = listSnapshot(audioStreams)
clampCursor()
}
function scheduleDisplayAudioModelRefresh() {
if (!opened) return
audioModelRefreshTimer.restart()
}
function clearDisplayAudioModels() {
audioModelRefreshTimer.stop()
displayAudioSinks = []
displayAudioSources = []
displayAudioStreams = []
}
// Keep the keyboard-focused row inside the visible viewport of the // Keep the keyboard-focused row inside the visible viewport of the
// ScrollView. Each cursor target (slider rows, SinkRow, SourceRow, // ScrollView. Each cursor target (slider rows, SinkRow, SourceRow,
@@ -560,8 +596,8 @@ Panel {
if (!streamLabelIsGeneric(label)) return "" if (!streamLabelIsGeneric(label)) return ""
return mprisLabelsFor(function(playerLabel) { return mprisLabelsFor(function(playerLabel) {
for (var i = 0; i < audioStreams.length; i++) { for (var i = 0; i < displayAudioStreams.length; i++) {
var stream = audioStreams[i] var stream = displayAudioStreams[i]
var streamLabel = rawStreamLabel(stream) var streamLabel = rawStreamLabel(stream)
if (!streamLabelIsGeneric(streamLabel) if (!streamLabelIsGeneric(streamLabel)
&& streamRepresentsMprisPlayer(streamLabel, playerLabel)) && streamRepresentsMprisPlayer(streamLabel, playerLabel))
@@ -619,6 +655,13 @@ Panel {
onTriggered: if (!sinkAvailabilityProc.running) sinkAvailabilityProc.running = true onTriggered: if (!sinkAvailabilityProc.running) sinkAvailabilityProc.running = true
} }
Timer {
id: audioModelRefreshTimer
interval: 75
repeat: false
onTriggered: root.refreshDisplayAudioModels()
}
WidgetButton { WidgetButton {
id: button id: button
anchors.fill: parent anchors.fill: parent
@@ -663,8 +706,8 @@ Panel {
if (t === "m" || t === "M") { if (t === "m" || t === "M") {
if (!root.cursorActive) return if (!root.cursorActive) return
if (root.focusSection === "streams" && root.selectedIndex >= 0 if (root.focusSection === "streams" && root.selectedIndex >= 0
&& root.selectedIndex < root.audioStreams.length) { && root.selectedIndex < root.displayAudioStreams.length) {
var s = root.audioStreams[root.selectedIndex] var s = root.displayAudioStreams[root.selectedIndex]
if (s && s.audio) s.audio.muted = !s.audio.muted if (s && s.audio) s.audio.muted = !s.audio.muted
} else if (root.focusSection === "input") { } else if (root.focusSection === "input") {
root.toggleInputMute() root.toggleInputMute()
@@ -811,7 +854,7 @@ Panel {
} }
Repeater { Repeater {
model: root.audioSinks model: root.displayAudioSinks
SinkRow { SinkRow {
required property var modelData required property var modelData
@@ -827,7 +870,7 @@ Panel {
Column { Column {
width: parent.width width: parent.width
spacing: Style.space(6) spacing: Style.space(6)
visible: root.audioSources.length > 0 || !!root.source visible: root.displayAudioSources.length > 0 || !!root.source
Item { Item {
width: parent.width width: parent.width
@@ -912,7 +955,7 @@ Panel {
} }
Repeater { Repeater {
model: root.audioSources model: root.displayAudioSources
SourceRow { SourceRow {
required property var modelData required property var modelData
@@ -928,7 +971,7 @@ Panel {
Column { Column {
width: parent.width width: parent.width
spacing: Style.space(10) spacing: Style.space(10)
visible: root.audioStreams.length > 0 visible: root.displayAudioStreams.length > 0
PanelSectionHeader { PanelSectionHeader {
text: "SOURCES" text: "SOURCES"
@@ -937,7 +980,7 @@ Panel {
} }
Repeater { Repeater {
model: root.audioStreams model: root.displayAudioStreams
StreamRow { StreamRow {
required property var modelData required property var modelData