feat(sound): redesign with separate device profiles page (#1500)

This commit is contained in:
Michael Murphy 2025-11-25 21:46:17 +01:00 committed by GitHub
parent 6ebc2208ed
commit 2c9f60cd5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 3179 additions and 1971 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-a11y-manager-subscription"
version = "0.1.0"
version = "1.0.0-beta6"
edition = "2024"
license = "MPL-2.0"
rust-version.workspace = true

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-accessibility-subscription"
version = "0.1.0"
version = "1.0.0-beta6"
edition = "2024"
license = "MPL-2.0"
rust-version.workspace = true

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-airplane-mode-subscription"
version = "0.1.0"
version = "1.0.0-beta6"
edition = "2024"
license = "MPL-2.0"
rust-version.workspace = true

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-bluetooth-subscription"
version = "0.1.0"
version = "1.0.0-beta6"
edition = "2024"
license = "MPL-2.0"
rust-version.workspace = true

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-network-manager-subscription"
version = "0.1.0"
version = "1.0.0-beta6"
edition = "2024"
license = "MPL-2.0"
rust-version.workspace = true

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-daemon-subscription"
version = "0.1.0"
version = "1.0.0-beta6"
edition = "2024"
rust-version.workspace = true
publish = true

View file

@ -1,19 +1,19 @@
[package]
name = "cosmic-settings-sound-subscription"
version = "1.0.0-beta1"
version = "1.0.0-beta6"
edition = "2024"
rust-version.workspace = true
license = "MPL-2.0"
publish = true
[dependencies]
async-fn-stream = "0.3.2"
cosmic-pipewire = { path = "../../crates/cosmic-pipewire" }
crossbeam-queue = "0.3.12"
futures = "0.3.31"
indexmap = "2.12.0"
intmap = "3.1.2"
libcosmic = { git = "https://github.com/pop-os/libcosmic" }
libpulse-binding = "2.30.1"
log = "0.4.28"
pipewire = "0.8"
rustix = "1.1.2"
tokio = "1.48.0"
numtoa = "1.0.0-alpha1"
rustix = "1.0.8"
tokio = { version = "1.47.1", features = ["process", "rt", "time"] }
tracing = "0.1.41"

File diff suppressed because it is too large Load diff

View file

@ -1,279 +0,0 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
// #![deny(missing_docs)]
pub use pipewire::channel::Sender;
use cosmic::iced_futures::{self, Subscription, stream};
use futures::{SinkExt, executor::block_on};
use pipewire::{
context::Context as PwContext,
main_loop::MainLoop as PwMainLoop,
node::{Node, NodeInfoRef, NodeState},
proxy::{Listener, ProxyT},
types::ObjectType,
};
use std::{
cell::RefCell,
collections::{BTreeMap, HashMap},
rc::Rc,
thread::JoinHandle,
};
pub fn subscription() -> iced_futures::Subscription<DeviceEvent> {
Subscription::run_with_id(
"pipewire",
stream::channel(20, |sender| async {
_ = thread(sender);
futures::future::pending().await
}),
)
}
pub fn thread(
on_event: futures::channel::mpsc::Sender<DeviceEvent>,
) -> (JoinHandle<()>, pipewire::channel::Sender<()>) {
let (pw_tx, pw_rx) = pipewire::channel::channel();
let handle = std::thread::spawn(move || {
devices_from_socket(pw_rx, on_event);
});
(handle, pw_tx)
}
/// Node event`
#[derive(Debug)]
pub enum NodeEvent<'a> {
/// Node info
NodeInfo(u32, &'a NodeInfoRef),
/// Node removal
Remove(u32),
}
/// Device event
#[derive(Clone, Debug)]
pub enum DeviceEvent {
/// A new device was detected.
Add(Device),
/// A device with the given object_id was removed.
Remove(u32),
}
/// Device information
#[must_use]
#[derive(Clone, Debug)]
pub struct Device {
pub object_id: u32,
pub variant: DeviceVariant,
pub media_class: MediaClass,
pub product_name: String,
pub node_name: String,
pub state: DeviceState,
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum DeviceVariant {
Alsa { alsa_card: u32 },
Bluez5 { address: String },
Unknown {},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DeviceState {
Idle,
Running,
Creating,
Suspended,
Error(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MediaClass {
Source,
Sink,
}
impl Device {
/// Attains process info from a pipewire info node.
#[must_use]
pub fn from_node(info: &NodeInfoRef) -> Option<Self> {
let props = info.props()?;
let (variant, product_name) = if let Some(alsa_card) =
props.get("alsa.card").and_then(|v| v.parse::<u32>().ok())
{
let device_profile_description = props.get("device.profile.description")?.to_owned();
let description = props.get("node.description")?;
let description = description
.strip_suffix(&device_profile_description)
.map(str::trim_end)
.unwrap_or(description)
.replace("High Definition Audio", "HD Audio");
(DeviceVariant::Alsa { alsa_card }, description)
} else if let Some(address) = props
.get("api.bluez5.address")
.and_then(|v| v.parse::<String>().ok())
{
(
DeviceVariant::Bluez5 {
address: address.to_owned(),
},
props.get("node.description")?.to_owned(),
)
} else {
(
DeviceVariant::Unknown {},
props.get("node.description")?.to_owned(),
)
};
Some(Device {
object_id: props.get("object.id")?.parse::<u32>().ok()?,
variant,
media_class: match props.get("media.class")? {
"Audio/Sink" => MediaClass::Sink,
"Audio/Source" => MediaClass::Source,
_ => return None,
},
product_name,
node_name: props.get("node.name")?.to_owned(),
state: match info.state() {
NodeState::Idle => DeviceState::Idle,
NodeState::Running => DeviceState::Running,
NodeState::Creating => DeviceState::Creating,
NodeState::Suspended => DeviceState::Suspended,
NodeState::Error(why) => DeviceState::Error(why.to_owned()),
},
})
}
}
/// Monitors the devices from a given ``PipeWire`` socket.
///
/// ``PipeWire`` sockets are found in `/run/user/{{UID}}/pipewire-0`.
pub fn devices_from_socket(
pw_cancel: pipewire::channel::Receiver<()>,
mut on_event: futures::channel::mpsc::Sender<DeviceEvent>,
) {
let mut managed = BTreeMap::new();
let _res = nodes_from_socket(pw_cancel, move |main_loop, event| match event {
NodeEvent::NodeInfo(pw_id, info) => {
if let Some(device) = Device::from_node(info) {
if managed.insert(pw_id, device.object_id).is_none() {
if block_on(on_event.send(DeviceEvent::Add(device))).is_err() {
main_loop.quit();
}
}
}
}
NodeEvent::Remove(pw_id) => {
if let Some(object_id) = managed.remove(&pw_id) {
if block_on(on_event.send(DeviceEvent::Remove(object_id))).is_err() {
main_loop.quit();
}
}
}
});
}
/// Listens to information about nodes, passing that info into a callback.
///
/// # Errors
///
/// Errors if the pipewire connection fails
pub fn nodes_from_socket(
pw_cancel: pipewire::channel::Receiver<()>,
on_event: impl FnMut(&PwMainLoop, NodeEvent) + 'static,
) -> Result<(), Box<dyn std::error::Error>> {
let main_loop = PwMainLoop::new(None)?;
let context = PwContext::new(&main_loop)?;
let core = context.connect(None)?;
// Exit main loop on receivering terminate message.
let _cancel_rx = pw_cancel.attach(main_loop.loop_(), {
let main_loop = main_loop.clone();
move |_| main_loop.quit()
});
let registry = Rc::new(core.get_registry()?);
let registry_weak = Rc::downgrade(&registry);
let proxies = Rc::new(RefCell::new(HashMap::new()));
let on_event = Rc::new(RefCell::new(on_event));
let main_loop_clone = main_loop.clone();
let _registry_listener = registry
.add_listener_local()
.global(move |obj| {
let Some(registry) = registry_weak.upgrade() else {
return;
};
let attached_proxy: Option<(Box<dyn ProxyT>, Box<dyn Listener>)> = match obj.type_ {
ObjectType::Node => {
let Ok(node): Result<Node, _> = registry.bind(obj) else {
return;
};
let on_event_weak = Rc::downgrade(&on_event);
let main_loop = main_loop_clone.clone();
let id = node.upcast_ref().id();
let listener = node
.add_listener_local()
.info(move |info| {
if let Some(on_event) = on_event_weak.upgrade() {
on_event.borrow_mut()(&main_loop, NodeEvent::NodeInfo(id, info));
}
})
.register();
Some((Box::new(node), Box::new(listener)))
}
_ => None,
};
if let Some((proxy_spe, listener)) = attached_proxy {
let proxy = proxy_spe.upcast_ref();
let id = proxy.id();
let (object_type, _object_version) = proxy.get_type();
let proxies_weak = Rc::downgrade(&proxies);
let on_event_weak = Rc::downgrade(&on_event);
let main_loop = main_loop_clone.clone();
let remove_listener = proxy
.add_listener_local()
.removed(move || {
if object_type == ObjectType::Node {
if let Some(on_event) = on_event_weak.upgrade() {
on_event.borrow_mut()(&main_loop, NodeEvent::Remove(id));
}
}
if let Some(proxies) = proxies_weak.upgrade() {
proxies.borrow_mut().remove(&id);
}
})
.register();
proxies
.borrow_mut()
.insert(id, (proxy_spe, listener, remove_listener));
}
})
.register();
main_loop.run();
Ok(())
}

View file

@ -1,752 +0,0 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
// Make sure not to fail if pulse not found, and reconnect?
// change to device shouldn't send osd?
use cosmic::iced_futures::{self, Subscription, stream};
use futures::{SinkExt, executor::block_on};
use libpulse_binding::{
callbacks::ListResult,
channelmap::Map,
context::{
Context, FlagSet, State,
introspect::{CardInfo, CardProfileInfo, Introspector, ServerInfo, SinkInfo, SourceInfo},
subscribe::{Facility, InterestMaskSet, Operation},
},
def::{PortAvailable, Retval},
mainloop::{
api::MainloopApi,
events::io::IoEventInternal,
standard::{IterateResult, Mainloop},
},
volume::{ChannelVolumes, Volume},
};
use std::{
borrow::Cow,
cell::{Cell, RefCell},
convert::Infallible,
io::{Read, Write},
os::{
fd::{FromRawFd, IntoRawFd, RawFd},
raw::c_void,
},
rc::Rc,
str::FromStr,
sync::mpsc,
};
pub fn subscription() -> iced_futures::Subscription<Event> {
Subscription::run_with_id(
"pulse",
stream::channel(20, |sender| async {
std::thread::spawn(move || thread(sender));
futures::future::pending().await
}),
)
}
pub fn thread(sender: futures::channel::mpsc::Sender<Event>) {
let Some(mut main_loop) = Mainloop::new() else {
log::error!("Failed to create PA main loop");
return;
};
let Some(mut context) = Context::new(&main_loop, "cosmic-osd") else {
log::error!("Failed to create PA context");
return;
};
let data = Rc::new(Data {
main_loop: RefCell::new(Mainloop {
_inner: Rc::clone(&main_loop._inner),
}),
introspector: context.introspect(),
sink_volume: Cell::new(None),
sink_mute: Cell::new(None),
source_volume: Cell::new(None),
source_mute: Cell::new(None),
default_sink_name: RefCell::new(None),
default_source_name: RefCell::new(None),
sender: RefCell::new(sender.clone()),
});
let data_clone = data.clone();
context.set_subscribe_callback(Some(Box::new(move |facility, operation, index| {
data_clone.subscribe_cb(facility.unwrap(), operation, index);
})));
let _ = context.connect(None, FlagSet::NOFAIL, None);
loop {
if sender.is_closed() {
return;
}
match main_loop.iterate(false) {
IterateResult::Success(_) => {}
IterateResult::Err(_e) => {
return;
}
IterateResult::Quit(_e) => {
return;
}
}
if context.get_state() == State::Ready {
break;
}
}
// Inspect all available cards on startup
data.introspector.get_card_info_list({
let data_weak = Rc::downgrade(&data);
move |card_info_res| {
if let Some(data) = data_weak.upgrade() {
data.card_info_cb(card_info_res)
}
}
});
data.get_server_info();
context.subscribe(
InterestMaskSet::SERVER | InterestMaskSet::SINK | InterestMaskSet::SOURCE,
|_| {},
);
if let Err((err, retval)) = main_loop.run() {
log::error!("PA main loop returned {:?}, error {}", retval, err);
}
}
#[derive(Clone, Debug)]
pub enum Event {
Balance(Option<f32>),
CardInfo(Card),
DefaultSink(String),
DefaultSource(String),
SinkVolume(u32),
Channels(PulseChannels),
SinkMute(bool),
SourceVolume(u32),
SourceMute(bool),
}
enum Request {
Volume(u32, f32),
Balance(u32, f32),
Quit,
}
#[derive(Debug)]
pub struct PulseChannels {
tx: mpsc::Sender<Request>,
pipe_tx: std::fs::File,
index: u32,
}
impl Clone for PulseChannels {
fn clone(&self) -> Self {
Self {
tx: self.tx.clone(),
pipe_tx: self
.pipe_tx
.try_clone()
.expect("failed to clone PulseChannels pipe writer"),
index: self.index,
}
}
}
/// Data used by the [`handle_balance_io_new`] callback.
struct HandleBalanceData(
Context,
ChannelVolumes,
Map,
std::sync::mpsc::Receiver<Request>,
);
/// Callback for creating an IO event source [`MainloopApi::io_new`].
extern "C" fn handle_balance_io_new(
api: *const MainloopApi,
event: *mut IoEventInternal,
reader_fd: RawFd,
_flags: libpulse_binding::mainloop::events::io::FlagSet,
data: *mut c_void,
) {
// Take ownership of the data and borrow its contents.
let mut data = unsafe { Box::<HandleBalanceData>::from_raw(data as _) };
let HandleBalanceData(ctx, volumes, map, rx) = data.as_mut();
// Return early if the context is not ready, and give the data back.
if ctx.get_state() != State::Ready {
let _ = Box::leak(data);
return;
}
// If the first byte cannot be read, destroy this event source with its reader and data.
let mut buf = [0u8; 1];
let mut reader = unsafe { std::fs::File::from_raw_fd(reader_fd) };
if reader.read_exact(&mut buf).is_err() {
(unsafe { &*api })
.io_free
.as_ref()
.expect("io_free function is missing")(event);
return;
}
// Give ownership of the reader back.
_ = reader.into_raw_fd();
while let Ok(req) = rx.try_recv() {
match req {
Request::Volume(index, volume_scale) => {
let mut intro = ctx.introspect();
let new_scale = Volume((volume_scale * Volume::NORMAL.0 as f32).round() as u32);
if let Some(v) = volumes.scale(new_scale) {
_ = intro.set_sink_volume_by_index(
index,
v,
Some(Box::new(|success| {
if !success {
tracing::error!("Failed to set sink balance");
}
})),
);
}
}
Request::Balance(index, new_balance) => {
if map.can_balance() {
if let Some(v) = volumes.set_balance(&map, new_balance) {
let mut intro = ctx.introspect();
_ = intro.set_sink_volume_by_index(
index,
v,
Some(Box::new(|success| {
if !success {
tracing::error!("Failed to set sink balance");
}
})),
);
}
}
}
Request::Quit => unsafe { &*api }
.quit
.as_ref()
.expect("quit function missing")(api, 0),
}
}
let _ = Box::leak(data);
}
impl PulseChannels {
fn new(
volumes: ChannelVolumes,
map: Map,
api: &MainloopApi,
index: u32,
ctx: Context,
) -> PulseChannels {
let (reader, writer) = rustix::pipe::pipe_with(rustix::pipe::PipeFlags::CLOEXEC)
.expect("failed to crate pipe");
let (tx, rx) = mpsc::channel::<Request>();
// Create IO event source object for handling speaker balance.
let event_source = api.io_new.as_ref().unwrap()(
api as *const _,
reader.into_raw_fd(),
libpulse_binding::mainloop::events::io::FlagSet::INPUT,
Some(handle_balance_io_new),
Box::into_raw(Box::new(HandleBalanceData(ctx, volumes, map, rx))) as *mut c_void,
);
if let Some(enable) = api.io_enable.as_ref() {
enable(
event_source,
libpulse_binding::mainloop::events::io::FlagSet::INPUT,
);
}
Self {
tx,
pipe_tx: std::fs::File::from(writer),
index,
}
}
/// Change the active index.
#[inline]
pub fn set_index(&mut self, index: u32) {
self.index = index;
}
/// Set the speaker balance of the active sink.
pub fn set_balance(&mut self, balance: f32) {
if let Err(err) = self.tx.send(Request::Balance(self.index, balance)) {
tracing::error!(?err, "Failed to send new balance to channel");
} else {
self.pipe_tx
.write_all(&[1])
.expect("PulseChannels pipe write failed");
}
}
/// Set the volume of the active sink.
pub fn set_volume(&mut self, volume: f32) {
if let Err(err) = self.tx.send(Request::Volume(self.index, volume)) {
tracing::error!(?err, "Failed to send new volume to channel");
} else {
self.pipe_tx
.write_all(&[1])
.expect("PulseChannels pipe write failed");
}
}
/// Request the pulse thread to quit.
pub fn quit(mut self) {
_ = self.tx.send(Request::Quit);
_ = self.pipe_tx.write_all(&[1]);
}
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct Card {
pub object_id: u32,
pub name: String,
pub product_name: String,
pub variant: DeviceVariant,
pub ports: Vec<CardPort>,
pub profiles: Vec<CardProfile>,
pub active_profile: Option<CardProfile>,
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct CardPort {
pub name: String,
pub description: String,
pub direction: Direction,
pub port_type: PortType,
pub profile_port: u32,
pub priority: u32,
pub profiles: Vec<CardProfile>,
pub availability: Availability,
}
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub enum Availability {
Unknown,
No,
Yes,
}
impl From<PortAvailable> for Availability {
fn from(pa: PortAvailable) -> Self {
match pa {
PortAvailable::Unknown => Availability::Unknown,
PortAvailable::No => Availability::No,
PortAvailable::Yes => Availability::Yes,
}
}
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct CardProfile {
pub name: String,
pub description: String,
pub available: bool,
pub n_sinks: u32,
pub n_sources: u32,
pub priority: u32,
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum DeviceVariant {
Alsa { alsa_card: u32 },
Bluez5 { address: String },
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum Direction {
Input,
Output,
Both,
}
#[derive(Default, Clone, Debug, Hash, Eq, PartialEq)]
pub enum PortType {
Mic,
Speaker,
Headphones,
Headset,
Digital,
#[default]
Unknown,
}
impl FromStr for PortType {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"mic" => Ok(PortType::Mic),
"speaker" => Ok(PortType::Speaker),
"headphones" => Ok(PortType::Headphones),
"headset" => Ok(PortType::Headset),
"digital" => Ok(PortType::Digital),
_ => Ok(PortType::Unknown),
}
}
}
struct Data {
main_loop: RefCell<Mainloop>,
default_sink_name: RefCell<Option<String>>,
default_source_name: RefCell<Option<String>>,
sink_volume: Cell<Option<u32>>,
sink_mute: Cell<Option<bool>>,
source_volume: Cell<Option<u32>>,
source_mute: Cell<Option<bool>>,
introspector: Introspector,
sender: RefCell<futures::channel::mpsc::Sender<Event>>,
}
impl Data {
fn card_info_cb(self: &Rc<Self>, card_info: ListResult<&CardInfo>) {
if let ListResult::Item(card_info) = card_info {
let Some(object_id) = card_info
.proplist
.get_str("object.id")
.and_then(|v| v.parse::<u32>().ok())
else {
return;
};
let variant = if let Some(alsa_card) = card_info
.proplist
.get_str("alsa.card")
.and_then(|v| v.parse::<u32>().ok())
{
DeviceVariant::Alsa { alsa_card }
} else if let Some(address) = card_info.proplist.get_str("api.bluez5.address") {
DeviceVariant::Bluez5 { address }
} else {
return;
};
let card = Card {
name: card_info
.name
.as_ref()
.map(Cow::to_string)
.unwrap_or_default(),
product_name: card_info
.proplist
.get_str("device.product.name")
.unwrap_or_default(),
object_id,
variant,
ports: card_info
.ports
.iter()
.map(|port| CardPort {
name: port.name.as_ref().map(Cow::to_string).unwrap_or_default(),
description: port
.description
.as_ref()
.map(Cow::to_string)
.unwrap_or_default(),
direction: match port.direction.bits() {
x if x == libpulse_binding::direction::FlagSet::INPUT.bits() => {
Direction::Input
}
x if x == libpulse_binding::direction::FlagSet::OUTPUT.bits() => {
Direction::Output
}
_ => Direction::Both,
},
port_type: port
.proplist
.get_str("port.type")
.as_deref()
.map(|s| PortType::from_str(s).unwrap())
.unwrap_or_default(),
profile_port: port
.proplist
.get_str("card.profile.port")
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(0),
priority: port.priority,
profiles: collect_profiles(&port.profiles),
availability: port.available.into(),
})
.collect(),
profiles: collect_profiles(&card_info.profiles),
active_profile: card_info.active_profile.as_deref().map(CardProfile::from),
};
if block_on(self.sender.borrow_mut().send(Event::CardInfo(card))).is_err() {
self.main_loop.borrow_mut().quit(Retval(0));
}
}
}
fn server_info_cb(self: &Rc<Self>, server_info: &ServerInfo) {
let new_default_sink_name = server_info
.default_sink_name
.as_ref()
.map(|x| x.clone().into_owned());
let mut default_sink_name = self.default_sink_name.borrow_mut();
if new_default_sink_name != *default_sink_name {
if let Some(name) = &new_default_sink_name {
_ = block_on(
self.sender
.borrow_mut()
.send(Event::DefaultSink(name.clone())),
);
self.get_sink_info_by_name(name);
}
*default_sink_name = new_default_sink_name;
}
let new_default_source_name = server_info
.default_source_name
.as_ref()
.map(|x| x.clone().into_owned());
let mut default_source_name = self.default_source_name.borrow_mut();
if new_default_source_name != *default_source_name {
if let Some(name) = &new_default_source_name {
_ = block_on(
self.sender
.borrow_mut()
.send(Event::DefaultSource(name.clone())),
);
self.get_source_info_by_name(name);
}
*default_source_name = new_default_source_name;
}
}
fn get_server_info(self: &Rc<Self>) {
let data = self.clone();
self.introspector
.get_server_info(move |server_info| data.server_info_cb(server_info));
}
fn sink_info_cb(&self, sink_info_res: ListResult<&SinkInfo>) {
if let ListResult::Item(sink_info) = sink_info_res {
if sink_info.name.as_deref() != self.default_sink_name.borrow().as_deref() {
return;
}
let balance = (sink_info.channel_map.can_balance()
&& sink_info.base_volume.is_normal())
.then(|| sink_info.volume.get_balance(&sink_info.channel_map));
let volume = sink_info.volume.max().0 / (Volume::NORMAL.0 / 100);
if self.sink_mute.get() != Some(sink_info.mute) {
self.sink_mute.set(Some(sink_info.mute));
if block_on(
self.sender
.borrow_mut()
.send(Event::SinkMute(sink_info.mute)),
)
.is_err()
{
self.main_loop.borrow_mut().quit(Retval(0));
}
}
if self.sink_volume.get() != Some(volume) {
self.sink_volume.set(Some(volume));
if block_on(self.sender.borrow_mut().send(Event::SinkVolume(volume))).is_err() {
self.main_loop.borrow_mut().quit(Retval(0));
}
}
if block_on(self.sender.borrow_mut().send(Event::Balance(balance))).is_err() {
self.main_loop.borrow_mut().quit(Retval(0));
}
let mut main_loop = self.main_loop.borrow_mut();
let api = main_loop.get_api();
if let Some(mut ctx) = Context::new(&*main_loop, "balance") {
let _ = ctx.connect(None, FlagSet::NOFAIL, None);
let channels = PulseChannels::new(
sink_info.volume,
sink_info.channel_map,
api,
sink_info.index,
ctx,
);
if block_on(self.sender.borrow_mut().send(Event::Channels(channels))).is_err() {
main_loop.quit(Retval(0));
}
}
}
}
fn source_info_cb(&self, source_info_res: ListResult<&SourceInfo>) {
if let ListResult::Item(source_info) = source_info_res {
if source_info.name.as_deref() != self.default_source_name.borrow().as_deref() {
return;
}
let volume = source_info.volume.max().0 / (Volume::NORMAL.0 / 100);
if self.source_mute.get() != Some(source_info.mute) {
self.source_mute.set(Some(source_info.mute));
if block_on(
self.sender
.borrow_mut()
.send(Event::SourceMute(source_info.mute)),
)
.is_err()
{
self.main_loop.borrow_mut().quit(Retval(0));
}
}
if self.source_volume.get() != Some(volume) {
self.source_volume.set(Some(volume));
if block_on(self.sender.borrow_mut().send(Event::SourceVolume(volume))).is_err() {
self.main_loop.borrow_mut().quit(Retval(0));
}
}
}
}
fn get_card_info_by_index(self: &Rc<Self>, index: u32) {
let data = self.clone();
self.introspector
.get_card_info_by_index(index, move |card_info_res| {
data.card_info_cb(card_info_res);
});
}
fn get_sink_info_by_index(self: &Rc<Self>, index: u32) {
let data = self.clone();
self.introspector.get_sink_info_by_index(
index,
move |sink_info_res: ListResult<&SinkInfo<'_>>| {
if let ListResult::Item(ref info) = sink_info_res {
if let Some(card_index) = info.card {
let data_clone = data.clone();
data.introspector.get_card_info_by_index(
card_index,
move |card_info_res| {
data_clone.card_info_cb(card_info_res);
},
);
}
}
data.sink_info_cb(sink_info_res);
},
);
}
fn get_sink_info_by_name(self: &Rc<Self>, name: &str) {
let data = self.clone();
self.introspector
.get_sink_info_by_name(name, move |sink_info_res| {
if let ListResult::Item(ref info) = sink_info_res {
if let Some(card_index) = info.card {
let data_clone = data.clone();
data.introspector.get_card_info_by_index(
card_index,
move |card_info_res| {
data_clone.card_info_cb(card_info_res);
},
);
}
}
data.sink_info_cb(sink_info_res);
});
}
fn get_source_info_by_index(self: &Rc<Self>, index: u32) {
let data = self.clone();
self.introspector
.get_source_info_by_index(index, move |source_info_res| {
if let ListResult::Item(ref info) = source_info_res {
if let Some(card_index) = info.card {
let data_clone = data.clone();
data.introspector.get_card_info_by_index(
card_index,
move |card_info_res| {
data_clone.card_info_cb(card_info_res);
},
);
}
}
data.source_info_cb(source_info_res);
});
}
fn get_source_info_by_name(self: &Rc<Self>, name: &str) {
let data = self.clone();
self.introspector
.get_source_info_by_name(name, move |source_info_res| {
if let ListResult::Item(ref info) = source_info_res {
if let Some(card_index) = info.card {
let data_clone = data.clone();
data.introspector.get_card_info_by_index(
card_index,
move |card_info_res| {
data_clone.card_info_cb(card_info_res);
},
);
}
}
data.source_info_cb(source_info_res);
});
}
fn subscribe_cb(
self: &Rc<Self>,
facility: Facility,
_operation: Option<Operation>,
index: u32,
) {
match facility {
Facility::Server => {
self.get_server_info();
}
Facility::Sink => {
self.get_sink_info_by_index(index);
}
Facility::Source => {
self.get_source_info_by_index(index);
}
Facility::Card => {
self.get_card_info_by_index(index);
}
_ => {}
}
}
}
fn collect_profiles(profiles: &[CardProfileInfo]) -> Vec<CardProfile> {
profiles.iter().map(CardProfile::from).collect()
}
impl From<&CardProfileInfo<'_>> for CardProfile {
fn from(profile: &CardProfileInfo) -> Self {
CardProfile {
name: profile
.name
.as_ref()
.map(Cow::to_string)
.unwrap_or_default(),
description: profile
.description
.as_ref()
.map(Cow::to_string)
.unwrap_or_default(),
available: profile.available,
n_sinks: profile.n_sinks,
n_sources: profile.n_sources,
priority: profile.priority,
}
}
}

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-upower-subscription"
version = "0.1.0"
version = "1.0.0-beta6"
edition = "2024"
license = "MPL-2.0"
rust-version.workspace = true