improv(sound): reduce codegen, use subscription, and fix threads not exiting on page close

This commit is contained in:
Michael Aaron Murphy 2025-07-22 06:22:36 +02:00
parent d41cdc5dc2
commit 6a29294e90
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
6 changed files with 141 additions and 99 deletions

2
Cargo.lock generated
View file

@ -1789,7 +1789,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings-subscriptions" name = "cosmic-settings-subscriptions"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/pop-os/cosmic-settings-subscriptions#da6ba802b1117f739fb3170aeb654c46fd2f08c3" source = "git+https://github.com/pop-os/cosmic-settings-subscriptions#d9e9639062df73623d04396d7b4473511d5812a4"
dependencies = [ dependencies = [
"bluez-zbus", "bluez-zbus",
"cosmic-dbus-a11y", "cosmic-dbus-a11y",

View file

@ -4,7 +4,7 @@ default-members = ["cosmic-settings"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
rust-version = "1.80.0" rust-version = "1.85.0"
[workspace.dependencies] [workspace.dependencies]
cosmic-randr = { git = "https://github.com/pop-os/cosmic-randr" } cosmic-randr = { git = "https://github.com/pop-os/cosmic-randr" }
@ -32,6 +32,9 @@ git = "https://github.com/pop-os/cosmic-panel"
[workspace.dependencies.cosmic-randr-shell] [workspace.dependencies.cosmic-randr-shell]
git = "https://github.com/pop-os/cosmic-randr" git = "https://github.com/pop-os/cosmic-randr"
[workspace.dependencies.cosmic-settings-subscriptions]
git = "https://github.com/pop-os/cosmic-settings-subscriptions"
[workspace.dependencies.sctk] [workspace.dependencies.sctk]
git = "https://github.com/smithay/client-toolkit/" git = "https://github.com/smithay/client-toolkit/"
package = "smithay-client-toolkit" package = "smithay-client-toolkit"

View file

@ -26,6 +26,7 @@ cosmic-randr-shell.workspace = true
cosmic-randr = { workspace = true, optional = true } cosmic-randr = { workspace = true, optional = true }
cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true }
cosmic-settings-page = { path = "../page" } cosmic-settings-page = { path = "../page" }
cosmic-settings-subscriptions = { workspace = true, optional = true }
cosmic-settings-system = { path = "../pages/system", optional = true } cosmic-settings-system = { path = "../pages/system", optional = true }
cosmic-settings-wallpaper = { path = "../pages/wallpapers" } cosmic-settings-wallpaper = { path = "../pages/wallpapers" }
cosmic-settings-daemon-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } cosmic-settings-daemon-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true }
@ -90,10 +91,6 @@ num-traits = "0.2"
num-derive = "0.4" num-derive = "0.4"
pwhash = "1" pwhash = "1"
[dependencies.cosmic-settings-subscriptions]
git = "https://github.com/pop-os/cosmic-settings-subscriptions"
optional = true
[dependencies.icu] [dependencies.icu]
version = "1.5.0" version = "1.5.0"
features = ["experimental", "compiled_data", "icu_datetime_experimental"] features = ["experimental", "compiled_data", "icu_datetime_experimental"]

View file

@ -1,10 +1,10 @@
// Copyright 2023 System76 <info@system76.com> // Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use std::{collections::BTreeMap, time::Duration}; use std::{collections::BTreeMap, sync::Arc, time::Duration};
use cosmic::{ use cosmic::{
Element, Task, Apply, Element, Task,
iced::{Alignment, Length, window}, iced::{Alignment, Length, window},
surface, surface,
widget::{self, settings}, widget::{self, settings},
@ -53,10 +53,35 @@ pub enum Message {
SourceVolumeApply(NodeId), SourceVolumeApply(NodeId),
/// Toggle the mute status of the input output. /// Toggle the mute status of the input output.
SourceMuteToggle, SourceMuteToggle,
/// On init of the subscription, channels for closing background threads are given to the app.
SubHandle(Arc<SubscriptionHandle>),
/// Surface Action /// Surface Action
Surface(surface::Action), Surface(surface::Action),
} }
pub struct SubscriptionHandle {
cancel_tx: futures::channel::oneshot::Sender<()>,
pipewire: pipewire::Sender<()>,
}
impl std::fmt::Debug for SubscriptionHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("SubscriptionHandle")
}
}
impl From<Message> for crate::pages::Message {
fn from(message: Message) -> Self {
crate::pages::Message::Sound(message)
}
}
impl From<Message> for crate::Message {
fn from(message: Message) -> Self {
crate::Message::PageMessage(message.into())
}
}
#[derive(Debug)] #[derive(Debug)]
struct Card { struct Card {
devices: IndexMap<NodeId, Device>, devices: IndexMap<NodeId, Device>,
@ -79,8 +104,7 @@ pub enum DeviceId {
#[derive(Default)] #[derive(Default)]
pub struct Page { pub struct Page {
entity: page::Entity, entity: page::Entity,
pipewire_thread: Option<(tokio::sync::oneshot::Sender<()>, pipewire::Sender<()>)>, subscription_handle: Option<SubscriptionHandle>,
pulse_thread: Option<tokio::sync::oneshot::Sender<()>>,
sink_channels: Option<pulse::PulseChannels>, sink_channels: Option<pulse::PulseChannels>,
devices: BTreeMap<DeviceId, Card>, devices: BTreeMap<DeviceId, Card>,
@ -146,10 +170,6 @@ pub struct Page {
} }
impl page::Page<crate::pages::Message> for Page { impl page::Page<crate::pages::Message> for Page {
fn set_id(&mut self, entity: page::Entity) {
self.entity = entity;
}
fn content( fn content(
&self, &self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>, sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
@ -163,74 +183,80 @@ impl page::Page<crate::pages::Message> for Page {
.description(fl!("sound", "desc")) .description(fl!("sound", "desc"))
} }
fn on_enter(&mut self) -> Task<crate::pages::Message> { fn subscription(
let mut tasks = Vec::with_capacity(2); &self,
if self.pulse_thread.is_none() { _core: &cosmic::Core,
let (tx, mut rx) = futures::channel::mpsc::channel(1); ) -> cosmic::iced::Subscription<crate::pages::Message> {
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>(); cosmic::iced::Subscription::run(|| {
async_fn_stream::fn_stream(|emitter| async move {
let (cancel_tx, mut cancel_rx) = futures::channel::oneshot::channel::<()>();
// Listen to events from the pulse thread until the tx channel is closed. let (tx, mut pulse_rx) = futures::channel::mpsc::channel(1);
_ = std::thread::spawn(move || { let _pulse_handle = std::thread::spawn(move || {
pulse::thread(tx); pulse::thread(tx);
}); });
// Forward events from the pulse thread to the application until let (tx, mut pw_rx) = futures::channel::mpsc::channel(1);
// the application requests to stop listening to the pulse thread. let (_pipewire_handle, pipewire_terminate) = pipewire::thread(tx);
tasks.push(Task::stream(async_fn_stream::fn_stream(
|emitter| async move { emitter
let forwarder = std::pin::pin!(async move { .emit(
while let Some(event) = rx.next().await { Message::SubHandle(Arc::new(SubscriptionHandle {
let event = crate::pages::Message::Sound(Message::Pulse(event)); cancel_tx,
emitter.emit(event).await; pipewire: pipewire_terminate,
}))
.into(),
)
.await;
loop {
futures::select! {
event = pulse_rx.next() => {
let Some(event) = event else {
break;
};
emitter
.emit(crate::pages::Message::from(Message::Pulse(event)))
.await;
} }
});
futures::future::select(std::pin::pin!(cancel_rx), forwarder).await; event = pw_rx.next() => {
}, let Some(event) = event else {
))); break;
};
self.pulse_thread = Some(cancel_tx); emitter
} .emit(crate::pages::Message::from(Message::Pipewire(event)))
.await;
if self.pipewire_thread.is_none() {
let (tx, mut rx) = futures::channel::mpsc::channel(1);
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
// Listen to events from the pipewire thread until the tx channel is closed.
let (_handle, terminate) = pipewire::thread(tx);
// Forward events from the pipewire thread to the application until
// the application requests to stop listening to the pulse thread.
tasks.push(Task::stream(async_fn_stream::fn_stream(
|emitter| async move {
let forwarder = std::pin::pin!(async move {
while let Some(event) = rx.next().await {
let event = crate::pages::Message::Sound(Message::Pipewire(event));
emitter.emit(event).await;
} }
});
futures::future::select(std::pin::pin!(cancel_rx), forwarder).await; _ = cancel_rx => break,
}, }
))); }
self.pipewire_thread = Some((cancel_tx, terminate)); drop(pulse_rx);
} drop(pw_rx);
cosmic::task::batch(tasks) futures::future::pending::<crate::pages::Message>().await;
})
})
} }
fn on_leave(&mut self) -> Task<crate::pages::Message> { fn on_leave(&mut self) -> Task<crate::pages::Message> {
if let Some(cancellation) = self.pulse_thread.take() { if let Some(handle) = self.subscription_handle.take() {
_ = cancellation.send(()); _ = handle.cancel_tx.send(());
_ = handle.pipewire.send(());
} }
if let Some((cancellation, terminate)) = self.pipewire_thread.take() { if let Some(channel) = self.sink_channels.take() {
_ = cancellation.send(()); channel.quit();
_ = terminate.send(());
} }
*self = Page::default(); *self = Page {
entity: self.entity,
..Page::default()
};
Task::none() Task::none()
} }
@ -338,9 +364,9 @@ impl Page {
let mut command = None; let mut command = None;
if let Some(&node_id) = self.source_pw_ids.get(self.active_source.unwrap_or(0)) { if let Some(&node_id) = self.source_pw_ids.get(self.active_source.unwrap_or(0)) {
command = Some(cosmic::task::future(async move { command = Some(cosmic::Task::future(async move {
tokio::time::sleep(Duration::from_millis(64)).await; tokio::time::sleep(Duration::from_millis(64)).await;
crate::pages::Message::Sound(Message::SourceVolumeApply(node_id)) Message::SourceVolumeApply(node_id).into()
})); }));
} }
@ -366,9 +392,9 @@ impl Page {
let mut command = None; let mut command = None;
if let Some(&node_id) = self.sink_pw_ids.get(self.active_sink.unwrap_or(0)) { if let Some(&node_id) = self.sink_pw_ids.get(self.active_sink.unwrap_or(0)) {
command = Some(cosmic::task::future(async move { command = Some(cosmic::Task::future(async move {
tokio::time::sleep(Duration::from_millis(64)).await; tokio::time::sleep(Duration::from_millis(64)).await;
crate::pages::Message::Sound(Message::SinkVolumeApply(node_id)) Message::SinkVolumeApply(node_id).into()
})); }));
} }
@ -390,9 +416,9 @@ impl Page {
.get(self.active_sink.unwrap_or(0)) .get(self.active_sink.unwrap_or(0))
.is_none() .is_none()
{ {
command = Some(cosmic::task::future(async move { command = Some(cosmic::Task::future(async move {
tokio::time::sleep(Duration::from_millis(64)).await; tokio::time::sleep(Duration::from_millis(64)).await;
crate::pages::Message::Sound(Message::SinkBalanceApply) Message::SinkBalanceApply.into()
})); }));
} }
@ -597,12 +623,10 @@ impl Page {
.insert(device_id.clone(), Some(profile.clone())); .insert(device_id.clone(), Some(profile.clone()));
self.changing_sink_profile = true; self.changing_sink_profile = true;
return cosmic::task::future(async move { return cosmic::Task::future(async move {
pactl_set_card_profile(name, profile).await; pactl_set_card_profile(name, profile).await;
Message::SinkProfileSelect(device_id) Message::SinkProfileSelect(device_id).into()
}) });
.map(crate::pages::Message::Sound)
.map(crate::app::Message::PageMessage);
} }
} }
} }
@ -627,12 +651,10 @@ impl Page {
.insert(device_id.clone(), Some(profile.clone())); .insert(device_id.clone(), Some(profile.clone()));
self.changing_source_profile = true; self.changing_source_profile = true;
return cosmic::task::future(async move { return cosmic::Task::future(async move {
pactl_set_card_profile(name, profile).await; pactl_set_card_profile(name, profile).await;
Message::SourceProfileSelect(device_id) Message::SourceProfileSelect(device_id).into()
}) });
.map(crate::pages::Message::Sound)
.map(crate::app::Message::PageMessage);
} }
} }
} }
@ -651,6 +673,12 @@ impl Page {
Message::Surface(a) => { Message::Surface(a) => {
return cosmic::task::message(crate::app::Message::Surface(a)); return cosmic::task::message(crate::app::Message::Surface(a));
} }
Message::SubHandle(handle) => {
if let Some(handle) = Arc::into_inner(handle) {
self.subscription_handle = Some(handle);
}
}
} }
Task::none() Task::none()
} }
@ -676,7 +704,7 @@ fn input() -> Section<crate::pages::Message> {
} else { } else {
"audio-input-microphone-symbolic" "audio-input-microphone-symbolic"
})) }))
.on_press(Message::SourceMuteToggle), .on_press(Message::SourceMuteToggle.into()),
) )
.push( .push(
widget::text::body(&page.source_volume_text) widget::text::body(&page.source_volume_text)
@ -685,8 +713,10 @@ fn input() -> Section<crate::pages::Message> {
) )
.push(widget::horizontal_space().width(8)) .push(widget::horizontal_space().width(8))
.push( .push(
widget::slider(0..=150, page.source_volume, Message::SourceVolumeChanged) widget::slider(0..=150, page.source_volume, |change| {
.breakpoints(&[100]), Message::SourceVolumeChanged(change).into()
})
.breakpoints(&[100]),
); );
let devices = widget::dropdown::popup_dropdown( let devices = widget::dropdown::popup_dropdown(
&page.sources, &page.sources,
@ -694,8 +724,10 @@ fn input() -> Section<crate::pages::Message> {
Message::SourceChanged, Message::SourceChanged,
window::Id::RESERVED, window::Id::RESERVED,
Message::Surface, Message::Surface,
|a| crate::app::Message::PageMessage(crate::pages::Message::Sound(a)), |a| crate::Message::from(a),
); )
.apply(Element::from)
.map(crate::pages::Message::from);
let mut controls = settings::section() let mut controls = settings::section()
.title(&section.title) .title(&section.title)
@ -712,13 +744,15 @@ fn input() -> Section<crate::pages::Message> {
Message::SourceProfileChanged, Message::SourceProfileChanged,
window::Id::RESERVED, window::Id::RESERVED,
Message::Surface, Message::Surface,
|a| crate::app::Message::PageMessage(crate::pages::Message::Sound(a)), |a| crate::Message::from(a),
); )
.apply(Element::from)
.map(crate::pages::Message::from);
controls = controls.add(settings::item(&*section.descriptions[profile], dropdown)); controls = controls.add(settings::item(&*section.descriptions[profile], dropdown));
} }
Element::from(controls).map(crate::pages::Message::Sound) Element::from(controls)
}) })
} }
@ -746,7 +780,7 @@ fn output() -> Section<crate::pages::Message> {
} else { } else {
widget::icon::from_name("audio-volume-high-symbolic") widget::icon::from_name("audio-volume-high-symbolic")
}) })
.on_press(Message::SinkMuteToggle), .on_press(Message::SinkMuteToggle.into()),
) )
.push( .push(
widget::text::body(&page.sink_volume_text) widget::text::body(&page.sink_volume_text)
@ -755,8 +789,10 @@ fn output() -> Section<crate::pages::Message> {
) )
.push(widget::horizontal_space().width(8)) .push(widget::horizontal_space().width(8))
.push( .push(
widget::slider(0..=150, page.sink_volume, Message::SinkVolumeChanged) widget::slider(0..=150, page.sink_volume, |change| {
.breakpoints(&[100]), Message::SinkVolumeChanged(change).into()
})
.breakpoints(&[100]),
); );
let devices = widget::dropdown::popup_dropdown( let devices = widget::dropdown::popup_dropdown(
@ -765,8 +801,10 @@ fn output() -> Section<crate::pages::Message> {
Message::SinkChanged, Message::SinkChanged,
window::Id::RESERVED, window::Id::RESERVED,
Message::Surface, Message::Surface,
|a| crate::app::Message::PageMessage(crate::pages::Message::Sound(a)), |a| crate::Message::from(a),
); )
.apply(Element::from)
.map(crate::pages::Message::from);
let mut controls = settings::section() let mut controls = settings::section()
.title(&section.title) .title(&section.title)
@ -783,8 +821,10 @@ fn output() -> Section<crate::pages::Message> {
Message::SinkProfileChanged, Message::SinkProfileChanged,
window::Id::RESERVED, window::Id::RESERVED,
Message::Surface, Message::Surface,
|a| crate::app::Message::PageMessage(crate::pages::Message::Sound(a)), |a| crate::Message::from(a),
); )
.apply(Element::from)
.map(crate::pages::Message::from);
controls = controls.add(settings::item(&*section.descriptions[profile], dropdown)); controls = controls.add(settings::item(&*section.descriptions[profile], dropdown));
} }
@ -803,7 +843,7 @@ fn output() -> Section<crate::pages::Message> {
widget::slider( widget::slider(
0..=200, 0..=200,
((sink_balance + 1.).max(0.) * 100.).round() as u32, ((sink_balance + 1.).max(0.) * 100.).round() as u32,
Message::SinkBalanceChanged, |change| Message::SinkBalanceChanged(change).into(),
) )
.breakpoints(&[100]), .breakpoints(&[100]),
) )
@ -816,7 +856,7 @@ fn output() -> Section<crate::pages::Message> {
)); ));
} }
Element::from(controls).map(crate::pages::Message::Sound) Element::from(controls)
}) })
} }

View file

@ -3,6 +3,7 @@ name = "cosmic-settings-system"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-only" license = "GPL-3.0-only"
rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View file

@ -2,6 +2,7 @@
name = "cosmic-settings-wallpaper" name = "cosmic-settings-wallpaper"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html