feat(sound): use cosmic-settings-daemon's varlink API

This commit is contained in:
Michael Murphy 2026-06-15 21:09:25 +02:00 • committed by GitHub
parent 8981b0b48e
commit 95eda40878
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 762 additions and 448 deletions

View file

@ -2,12 +2,13 @@
// SPDX-License-Identifier: GPL-3.0-only
mod localize;
mod model;
mod mouse_area;
use crate::localize::localize;
use config::{AudioAppletConfig, amplification_sink, amplification_source};
use cosmic::{
Element, Renderer, Task, Theme, app,
Apply, Element, Renderer, Task, Theme, app,
applet::{
column as applet_column,
cosmic_panel_config::PanelAnchor,
@ -26,10 +27,12 @@ use cosmic::{
surface, theme,
widget::{Row, button, container, divider, icon, space, text, toggler},
};
use cosmic_settings_sound_subscription as css;
use cosmic_settings_audio_client::{self as audio_client, CosmicAudioProxy};
use futures::SinkExt;
use iced::platform_specific::shell::wayland::commands::popup::{destroy_popup, get_popup};
use mpris_subscription::{MprisRequest, MprisUpdate};
use mpris2_zbus::player::PlaybackStatus;
use std::{cell::RefCell, rc::Rc, sync::Arc};
mod config;
mod mpris_subscription;
@ -50,8 +53,10 @@ pub struct Audio {
core: cosmic::app::Core,
/// Track the applet's popup window.
popup: Option<window::Id>,
/// The model from cosmic-settings for managing pipewire devices.
model: css::Model,
/// Varlink connection to `com.system76.CosmicSettings.Audio`.
audio_client: Option<Rc<RefCell<audio_client::Client>>>,
/// Known audio device state
model: model::Model,
/// Whether to expand the revealer of a source or sink device.
is_open: IsOpen,
/// Max slider volume for the sink device, as determined by the amplification property.
@ -72,8 +77,8 @@ pub struct Audio {
impl Audio {
fn output_icon_name(&self) -> &'static str {
let volume = self.model.sink_volume;
let mute = self.model.sink_mute;
let volume = self.model.active_sink.volume;
let mute = self.model.active_sink.mute;
if mute || volume == 0 {
"audio-volume-muted-symbolic"
} else if volume < 33 {
@ -88,8 +93,8 @@ impl Audio {
}
fn input_icon_name(&self) -> &'static str {
let volume = self.model.source_volume;
let mute = self.model.source_mute;
let volume = self.model.active_source.volume;
let mute = self.model.active_source.mute;
if mute || volume == 0 {
"microphone-sensitivity-muted-symbolic"
} else if volume < 33 {
@ -110,8 +115,10 @@ enum IsOpen {
Input,
}
#[derive(Debug, Clone)]
#[derive(Clone, Debug)]
pub enum Message {
/// Connection to `com.system76.CosmicSettings`.
Client(Arc<audio_client::Client>),
Ignore,
SetSinkVolume(u32),
SetSourceVolume(u32),
@ -129,7 +136,7 @@ pub enum Message {
MprisRequest(MprisRequest),
Token(TokenUpdate),
OpenSettings,
Subscription(css::Message),
Subscription(audio_client::Event),
Surface(surface::Action),
}
@ -242,15 +249,9 @@ impl cosmic::Application for Audio {
const APP_ID: &'static str = "com.system76.CosmicAppletAudio";
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task<Message>) {
let mut model = css::Model::default();
model.unplugged_text = "Unplugged".into();
model.hd_audio_text = "HD Audio".into();
model.usb_audio_text = "USB Audio".into();
(
Self {
core,
model,
..Default::default()
},
Task::none(),
@ -318,52 +319,74 @@ impl cosmic::Application for Audio {
}
}
Message::Subscription(message) => {
return self
.model
.update(message)
.map(|message| Message::Subscription(message).into());
self.model.update(message);
}
Message::SetDefaultSink(pos) => {
return self
.model
.set_default_sink(pos)
.map(|message| Message::Subscription(message).into());
if let Some(&pos) = self.model.sinks.sorted_index.get(pos)
&& let Some(&node_id) = self.model.sinks.id.get(pos as usize)
&& let Some(client) = self.audio_client.as_mut()
{
futures::executor::block_on(async {
_ = client.borrow_mut().conn.set_default(node_id, true).await;
});
}
}
Message::SetDefaultSource(pos) => {
return self
.model
.set_default_source(pos)
.map(|message| Message::Subscription(message).into());
if let Some(&pos) = self.model.sources.sorted_index.get(pos)
&& let Some(&node_id) = self.model.sources.id.get(pos as usize)
&& let Some(client) = self.audio_client.as_mut()
{
futures::executor::block_on(async {
_ = client.borrow_mut().conn.set_default(node_id, true).await;
});
}
}
Message::ToggleSinkMute => self.model.toggle_sink_mute(),
Message::ToggleSinkMute => {
if let Some(ref mut client) = self.audio_client {
futures::executor::block_on(async {
_ = client.borrow_mut().conn.sink_mute_toggle().await;
});
}
}
Message::ToggleSourceMute => self.model.toggle_source_mute(),
Message::ToggleSourceMute => {
if let Some(ref mut client) = self.audio_client {
futures::executor::block_on(async {
_ = client.borrow_mut().conn.source_mute_toggle().await;
});
}
}
Message::SetSinkVolume(volume) => {
return self
.model
.set_sink_volume(volume)
.map(|message| Message::Subscription(message).into());
if let Some(ref mut client) = self.audio_client {
self.model.active_sink.volume = volume;
self.model.active_sink.volume_text = volume.to_string();
futures::executor::block_on(async {
_ = client.borrow_mut().conn.set_sink_volume(volume).await;
});
}
}
Message::SetSourceVolume(volume) => {
return self
.model
.set_source_volume(volume)
.map(|message| Message::Subscription(message).into());
if let Some(ref mut client) = self.audio_client {
self.model.active_source.volume = volume;
self.model.active_source.volume_text = volume.to_string();
futures::executor::block_on(async {
_ = client.borrow_mut().conn.set_source_volume(volume).await;
});
}
}
Message::ToggleMediaControlsInTopPanel(enabled) => {
self.config.show_media_controls_in_top_panel = enabled;
if let Ok(helper) =
cosmic::cosmic_config::Config::new(Self::APP_ID, AudioAppletConfig::VERSION)
&& let Err(err) = self.config.write_entry(&helper)
{
if let Err(err) = self.config.write_entry(&helper) {
tracing::error!(?err, "Error writing config");
}
tracing::error!(?err, "Error writing config");
}
}
Message::CloseRequested(id) => {
@ -461,6 +484,12 @@ impl cosmic::Application for Audio {
cosmic::app::Action::Surface(a),
));
}
Message::Client(client) => {
if let Some(client) = Arc::into_inner(client) {
self.audio_client = Some(Rc::new(RefCell::new(client)));
self.model = model::Model::default();
}
}
}
Task::none()
@ -476,7 +505,49 @@ impl cosmic::Application for Audio {
}),
mpris_subscription::mpris_subscription(0).map(Message::Mpris),
activation_token_subscription(0).map(Message::Token),
Subscription::run(|| css::watch().map(Message::Subscription)),
Subscription::run(|| {
iced::stream::channel(
1,
move |mut emitter: futures::channel::mpsc::Sender<_>| async move {
loop {
let mut client = match audio_client::connect().await {
Ok(client) => client,
Err(why) => {
if let zlink::Error::Io(ref why) = why
&& why.kind() == std::io::ErrorKind::NotFound
{
tracing::error!(
"cosmic-settings-daemon varlink service not found."
);
} else {
tracing::error!(
?why,
"failed to connect to cosmic-settings's varlink service"
);
}
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
continue;
}
};
if let Ok(Ok(mut stream)) = client.recv_events().await {
_ = emitter.send(Message::Client(Arc::new(client))).await;
while let Some(message) = stream.next().await {
match message {
Ok(event) => {
_ = emitter.send(Message::Subscription(event)).await;
}
Err(why) => {
tracing::error!(?why, "event error");
}
}
}
}
}
},
)
}),
])
}
@ -497,7 +568,7 @@ impl cosmic::Application for Audio {
return Message::Ignore;
}
let new_volume = (self.model.sink_volume as f64 + (scroll_vector as f64))
let new_volume = (self.model.active_sink.volume as f64 + (scroll_vector as f64))
.clamp(0.0, self.max_sink_volume as f64);
Message::SetSinkVolume(new_volume as u32)
});
@ -551,19 +622,21 @@ impl cosmic::Application for Audio {
space_xxs, space_s, ..
} = theme::active().cosmic().spacing;
let sink = self
let sink: Option<&str> = self
.model
.active_sink()
.and_then(|pos| self.model.sinks().get(pos));
let source = self
.sinks
.active()
.map(|pos| self.model.sinks.sorted_display[pos].as_ref());
let source: Option<&str> = self
.model
.active_source()
.and_then(|pos| self.model.sources().get(pos));
.sources
.active()
.map(|pos| self.model.sources.sorted_display[pos].as_ref());
let mut audio_content = {
let output_slider = slider(
0..=self.max_sink_volume,
self.model.sink_volume,
self.model.active_sink.volume,
Message::SetSinkVolume,
)
.width(Length::FillPortion(5))
@ -571,7 +644,7 @@ impl cosmic::Application for Audio {
let input_slider = slider(
0..=self.max_source_volume,
self.model.source_volume,
self.model.active_source.volume,
Message::SetSourceVolume,
)
.width(Length::FillPortion(5))
@ -590,7 +663,7 @@ impl cosmic::Application for Audio {
.line_height(24)
.on_press(Message::ToggleSinkMute),
output_slider,
container(text(&self.model.sink_volume_text).size(16))
container(text(&self.model.active_sink.volume_text).size(16))
.width(Length::FillPortion(1))
.align_x(Alignment::End)
]
@ -609,7 +682,7 @@ impl cosmic::Application for Audio {
.line_height(24)
.on_press(Message::ToggleSourceMute),
input_slider,
container(text(&self.model.source_volume_text).size(16))
container(text(&self.model.active_source.volume_text).size(16))
.width(Length::FillPortion(1))
.align_x(Alignment::End)
]
@ -624,7 +697,7 @@ impl cosmic::Application for Audio {
Some(sink) => sink.to_owned(),
None => fl!("no-device"),
},
self.model.sinks(),
&self.model.sinks.sorted_display,
Message::OutputToggle,
Message::SetDefaultSink,
),
@ -635,7 +708,7 @@ impl cosmic::Application for Audio {
Some(source) => source.to_owned(),
None => fl!("no-device"),
},
self.model.sources(),
&self.model.sources.sorted_display,
Message::InputToggle,
Message::SetDefaultSource,
)
@ -691,19 +764,15 @@ impl cosmic::Application for Audio {
}
if let Some(play) = self.is_play() {
control_elements.push(
button::icon(
icon::from_name(if play { PLAY } else { PAUSE })
.size(32)
.symbolic(true),
)
.extra_small()
.class(cosmic::theme::Button::AppletIcon)
.on_press(if play {
Message::MprisRequest(MprisRequest::Play)
} else {
Message::MprisRequest(MprisRequest::Pause)
})
.into(),
button::icon(icon::from_name(if play { PLAY } else { PAUSE }).symbolic(true))
.extra_small()
.class(cosmic::theme::Button::AppletIcon)
.on_press(if play {
Message::MprisRequest(MprisRequest::Play)
} else {
Message::MprisRequest(MprisRequest::Pause)
})
.into(),
);
}
if let Some(go_next) = self.go_next(32) {
@ -754,20 +823,21 @@ impl cosmic::Application for Audio {
}
}
fn revealer(
fn revealer<'a>(
open: bool,
title: String,
selected: String,
devices: &[String],
devices: &'a [Arc<str>],
toggle: Message,
mut change: impl FnMut(usize) -> Message + 'static,
) -> widget::Column<'static, Message, crate::Theme, Renderer> {
) -> widget::Column<'a, Message, crate::Theme, Renderer> {
if open {
devices.iter().cloned().enumerate().fold(
devices.iter().enumerate().fold(
column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
|col, (id, name)| {
move |col, (id, name)| {
col.push(
menu_button(text::body(name))
text::body(name.as_ref())
.apply(menu_button)
.on_press(change(id))
.width(Length::Fill)
.padding([8, 48]),
@ -785,9 +855,9 @@ fn revealer_head(
selected: String,
toggle: Message,
) -> cosmic::widget::Button<'static, Message> {
menu_button(column![
text::body(title).width(Length::Fill),
text::caption(selected),
])
.on_press(toggle)
cosmic::widget::column::with_capacity(2)
.push(text::body(title).width(Length::Fill))
.push(text::caption(selected))
.apply(menu_button)
.on_press(toggle)
}