From 786a9802548871e6cf60553de366921382a522ac Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 13 Jun 2022 17:54:05 -0700 Subject: [PATCH 1/6] WIP using `libpulse_binding` directly in audio applet --- Cargo.lock | 35 +++-- applets/cosmic-applet-audio/Cargo.toml | 3 +- applets/cosmic-applet-audio/src/input.rs | 28 ++-- applets/cosmic-applet-audio/src/main.rs | 68 +++++---- applets/cosmic-applet-audio/src/output.rs | 28 ++-- applets/cosmic-applet-audio/src/pa.rs | 160 ++++++++++++++++++++-- applets/cosmic-applet-audio/src/volume.rs | 3 +- 7 files changed, 243 insertions(+), 82 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7acee1c..30fa82d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -315,13 +315,14 @@ version = "0.1.0" dependencies = [ "async-io", "freedesktop-desktop-entry", + "futures", "futures-util", "gtk4", "libcosmic-widgets", "libpulse-binding", + "libpulse-glib-binding", "mpris2-zbus", "once_cell", - "pulsectl-rs", "relm4-macros 0.4.4", "tokio", "tracker", @@ -1541,6 +1542,29 @@ dependencies = [ "winapi", ] +[[package]] +name = "libpulse-glib-binding" +version = "2.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0e7a964c9f7e95d4f073affc19adfda009fa0d55e8831dbb66c78be1d0e6e5" +dependencies = [ + "glib", + "glib-sys", + "libpulse-binding", + "libpulse-mainloop-glib-sys", +] + +[[package]] +name = "libpulse-mainloop-glib-sys" +version = "1.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f61c4064926cc77ea14bb206a21ce1d5a06e175e5c0ce078804bb6c4527b28" +dependencies = [ + "glib-sys", + "libpulse-sys", + "pkg-config", +] + [[package]] name = "libpulse-sys" version = "1.19.3" @@ -2046,15 +2070,6 @@ dependencies = [ "libc", ] -[[package]] -name = "pulsectl-rs" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06a988bceed1981b2c5fc4a3da0e4e073fdaff8e6bd022b089f54bc573dc3cfc" -dependencies = [ - "libpulse-binding", -] - [[package]] name = "quick-error" version = "1.2.3" diff --git a/applets/cosmic-applet-audio/Cargo.toml b/applets/cosmic-applet-audio/Cargo.toml index 5b85e326..993321a6 100644 --- a/applets/cosmic-applet-audio/Cargo.toml +++ b/applets/cosmic-applet-audio/Cargo.toml @@ -5,10 +5,11 @@ edition = "2021" license = "GPL-3.0-or-later" [dependencies] +futures = "0.3.21" futures-util = "0.3.21" libcosmic-widgets = { git = "https://github.com/pop-os/libcosmic" } libpulse-binding = "2.26.0" -pulsectl-rs = "0.3.2" +libpulse-glib-binding = "2.25.0" tracker = "0.1.1" freedesktop-desktop-entry = "0.5.0" mpris2-zbus = { git = "https://github.com/pop-os/mpris2-zbus" } diff --git a/applets/cosmic-applet-audio/src/input.rs b/applets/cosmic-applet-audio/src/input.rs index d7a8cebd..c75d2cbe 100644 --- a/applets/cosmic-applet-audio/src/input.rs +++ b/applets/cosmic-applet-audio/src/input.rs @@ -1,19 +1,21 @@ use gtk4::{prelude::*, Button, Label, ListBox}; use libcosmic_widgets::{relm4::RelmContainerExt, LabeledItem}; -use pulsectl::controllers::{types::DeviceInfo, DeviceControl, SourceController}; use std::rc::Rc; -fn get_inputs() -> Vec { - SourceController::create() - .expect("failed to create input controller") - .list_devices() +use crate::pa::{Source, PA}; + +pub async fn get_inputs(pa: &PA) -> Vec { + // XXX handle error + pa.get_source_info_list() + .await .expect("failed to list input devices") } -pub fn refresh_default_input(label: &Label) -> DeviceInfo { - let default_input = SourceController::create() - .expect("failed to create input controller") - .get_default_device() +pub async fn refresh_default_input(pa: &PA, label: &Label) -> Source { + // XXX handle error + let default_input = pa + .get_default_source() + .await .expect("failed to get default input"); label.set_text(match &default_input.description { Some(name) => name.as_str(), @@ -22,12 +24,11 @@ pub fn refresh_default_input(label: &Label) -> DeviceInfo { default_input } -pub fn refresh_input_widgets(inputs: &ListBox) { +pub async fn refresh_input_widgets(pa: &PA, inputs: &ListBox) { while let Some(row) = inputs.row_at_index(0) { inputs.remove(&row); } - for input in get_inputs() { - let input = Rc::new(input.clone()); + for input in get_inputs(pa).await { let name = match &input.name { Some(name) => name.to_owned(), None => continue, // Why doesn't this have a name? Whatever, it's invalid. @@ -40,10 +41,13 @@ pub fn refresh_input_widgets(inputs: &ListBox) { set_child: set_current_input_device = &Button { set_label: "Switch", connect_clicked: move |_| { + // XXX Need mutable borrow? Is this a problem for async? + /* SourceController::create() .expect("failed to create input controller") .set_default_device(&name) .expect("failed to set default device"); + */ } } } diff --git a/applets/cosmic-applet-audio/src/main.rs b/applets/cosmic-applet-audio/src/main.rs index edd398cc..068f15f4 100644 --- a/applets/cosmic-applet-audio/src/main.rs +++ b/applets/cosmic-applet-audio/src/main.rs @@ -8,9 +8,11 @@ mod input; mod now_playing; mod output; mod pa; +use pa::PA; mod task; mod volume; +use futures::{channel::mpsc, stream::StreamExt}; use gtk4::{ gio::ApplicationFlags, glib::{self, clone, MainContext, PRIORITY_DEFAULT}, @@ -24,7 +26,7 @@ use libpulse_binding::{ }; use mpris2_zbus::metadata::Metadata; use once_cell::sync::Lazy; -use pulsectl::Handler; +use std::rc::Rc; use tokio::runtime::Runtime; static RT: Lazy = Lazy::new(|| Runtime::new().expect("failed to build tokio runtime")); @@ -39,35 +41,29 @@ fn main() { } fn app(application: &Application) { - let handler = - Handler::connect("com.system76.cosmic.applets.audio").expect("failed to connect to pulse"); - task::spawn_local(clone!(@strong handler.mainloop as main_loop => async move { - pa::drive_main_loop(main_loop).await - })); - let (refresh_output_tx, refresh_output_rx) = MainContext::channel::<()>(PRIORITY_DEFAULT); - let (refresh_input_tx, refresh_input_rx) = MainContext::channel::<()>(PRIORITY_DEFAULT); - let (now_playing_tx, now_playing_rx) = MainContext::channel::>(PRIORITY_DEFAULT); - handler - .context - .borrow_mut() + // XXX handle no pulseaudio daemon? + let mut pa = PA::new().unwrap(); + let (refresh_output_tx, mut refresh_output_rx) = mpsc::unbounded(); + let (refresh_input_tx, mut refresh_input_rx) = mpsc::unbounded(); + let (now_playing_tx, mut now_playing_rx) = mpsc::unbounded::>(); + pa.context .set_subscribe_callback(Some(Box::new(clone!(@strong refresh_output_tx, @strong refresh_input_tx => move |facility, operation, _idx| { if !matches!(operation, Some(Operation::Changed)) { return; } match facility { Some(Facility::Sink) => { - refresh_output_tx.send(()).expect("failed to send output refresh message"); + refresh_output_tx.unbounded_send(()).expect("failed to send output refresh message"); } Some(Facility::Source) => { - refresh_input_tx.send(()).expect("failed to send output refresh message"); + refresh_input_tx.unbounded_send(()).expect("failed to send output refresh message"); } _ => {} } })))); - handler - .context - .borrow_mut() + pa.context .subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE, |_| {}); + let pa = Rc::new(pa); view! { window = ApplicationWindow { set_application: Some(application), @@ -153,29 +149,27 @@ fn app(application: &Application) { } } } - refresh_input_rx.attach( - None, - clone!(@weak inputs, @weak current_input, @weak input_volume => @default-return Continue(true), move |_| { - input::refresh_input_widgets(&inputs); - let default_input = input::refresh_default_input(¤t_input); - volume::update_volume(&default_input, &input_volume); - Continue(true) + glib::MainContext::default().spawn_local( + clone!(@weak inputs, @weak current_input, @weak input_volume, @strong pa => async move { + while let Some(()) = refresh_input_rx.next().await { + input::refresh_input_widgets(&pa, &inputs); + let default_input = input::refresh_default_input(&pa, ¤t_input); + // XXX volume::update_volume(&default_input, &input_volume); + } }), ); - refresh_output_rx.attach( - None, - clone!(@weak outputs, @weak current_output, @weak output_volume => @default-return Continue(true), move |_| { - output::refresh_output_widgets(&outputs); - let default_output = output::refresh_default_output(¤t_output); - volume::update_volume(&default_output, &output_volume); - Continue(true) - }), - ); - now_playing_rx.attach( - None, - clone!(@weak playing_apps => @default-return Continue(true), move |all_metadata| { - Continue(true) + glib::MainContext::default().spawn_local( + clone!(@weak outputs, @weak current_output, @weak output_volume, @strong pa => async move { + while let Some(()) = refresh_output_rx.next().await { + output::refresh_output_widgets(&pa, &outputs); + let default_output = output::refresh_default_output(&pa, ¤t_output); + // XXX volume::update_volume(&default_output, &output_volume); + } }), ); + glib::MainContext::default().spawn_local(clone!(@weak playing_apps => async move { + while let Some(all_metadata) = now_playing_rx.next().await { + } + })); window.show(); } diff --git a/applets/cosmic-applet-audio/src/output.rs b/applets/cosmic-applet-audio/src/output.rs index 28840abd..ef9ae407 100644 --- a/applets/cosmic-applet-audio/src/output.rs +++ b/applets/cosmic-applet-audio/src/output.rs @@ -1,19 +1,21 @@ use gtk4::{prelude::*, Button, Label, ListBox}; use libcosmic_widgets::{relm4::RelmContainerExt, LabeledItem}; -use pulsectl::controllers::{types::DeviceInfo, DeviceControl, SinkController}; use std::rc::Rc; -fn get_outputs() -> Vec { - SinkController::create() - .expect("failed to create output controller") - .list_devices() +use crate::pa::{Sink, PA}; + +pub async fn get_outputs(pa: &PA) -> Vec { + // XXX handle error + pa.get_sink_info_list() + .await .expect("failed to list output devices") } -pub fn refresh_default_output(label: &Label) -> DeviceInfo { - let default_output = SinkController::create() - .expect("failed to create output controller") - .get_default_device() +pub async fn refresh_default_output(pa: &PA, label: &Label) -> Sink { + // XXX handle error + let default_output = pa + .get_default_sink() + .await .expect("failed to get default output"); label.set_text(match &default_output.description { Some(name) => name.as_str(), @@ -22,12 +24,11 @@ pub fn refresh_default_output(label: &Label) -> DeviceInfo { default_output } -pub fn refresh_output_widgets(outputs: &ListBox) { +pub async fn refresh_output_widgets(pa: &PA, outputs: &ListBox) { while let Some(row) = outputs.row_at_index(0) { outputs.remove(&row); } - for output in get_outputs() { - let output = Rc::new(output.clone()); + for output in get_outputs(pa).await { let name = match &output.name { Some(name) => name.to_owned(), None => continue, // Why doesn't this have a name? Whatever, it's invalid. @@ -40,10 +41,13 @@ pub fn refresh_output_widgets(outputs: &ListBox) { set_child: set_current_input_device = &Button { set_label: "Switch", connect_clicked: move |_| { + // XXX Need mutable borrow? Is this a problem for async? + /* SinkController::create() .expect("failed to create output controller") .set_default_device(&name) .expect("failed to set default device"); + */ } } } diff --git a/applets/cosmic-applet-audio/src/pa.rs b/applets/cosmic-applet-audio/src/pa.rs index 9a4b1d58..ac828b8f 100644 --- a/applets/cosmic-applet-audio/src/pa.rs +++ b/applets/cosmic-applet-audio/src/pa.rs @@ -1,12 +1,154 @@ -use async_io::Timer; -use futures_util::StreamExt; -use libpulse_binding::mainloop::standard::Mainloop; -use std::{cell::RefCell, rc::Rc, time::Duration}; +use futures::{channel::oneshot, future::poll_fn, task::Poll}; +use libpulse_binding::{ + callbacks::ListResult, + context::{introspect::SinkInfo, Context}, +}; +use libpulse_glib_binding::Mainloop; +use std::rc::Rc; -pub async fn drive_main_loop(main_loop: Rc>) { - let mut timer = Timer::interval(Duration::from_millis(100)); - loop { - main_loop.borrow_mut().iterate(false); - timer.next().await; +pub struct Sink { + pub name: Option, + pub description: Option, +} + +pub struct Source { + pub name: Option, + pub description: Option, +} + +pub struct ServerInfo { + pub default_sink_name: Option, + pub default_source_name: Option, +} + +pub struct PA { + main_loop: Mainloop, + pub context: Context, +} + +impl PA { + pub fn new() -> Option { + let main_loop = Mainloop::new(None)?; + let context = Context::new(&main_loop, "com.system76.cosmic.applets.audio")?; + Some(Self { main_loop, context }) + } + + pub async fn get_server_info(&self) -> ServerInfo { + let (sender, receiver) = oneshot::channel(); + let mut sender = Some(sender); + self.context.introspect().get_server_info(move |info| { + sender.take().unwrap().send(ServerInfo { + default_sink_name: info.default_sink_name.clone().map(|x| x.into_owned()), + default_source_name: info.default_source_name.clone().map(|x| x.into_owned()), + }); + }); + receiver.await.unwrap() + } + + pub async fn get_sink_info_list(&self) -> Result, ()> { + let (sender, receiver) = oneshot::channel(); + let mut sender = Some(sender); + let mut items = Some(Vec::new()); + self.context + .introspect() + .get_sink_info_list(move |result| match result { + ListResult::Item(item) => items.as_mut().unwrap().push(Sink { + name: item.name.clone().map(|x| x.into_owned()), + description: item.description.clone().map(|x| x.into_owned()), + }), + ListResult::End => { + sender.take().unwrap().send(Ok(items.take().unwrap())); + } + ListResult::Error => { + sender.take().unwrap().send(Err(())); + } + }); + receiver.await.unwrap() + } + + pub async fn get_default_sink(&self) -> Result { + let name = match self.get_server_info().await.default_sink_name { + Some(name) => name, + None => { + return Err(()); + } + }; + let (sender, receiver) = oneshot::channel(); + let mut sender = Some(sender); + let mut sink = None; + self.context + .introspect() + .get_sink_info_by_name(&name, move |result| match result { + ListResult::Item(item) => { + sink = Some(Sink { + name: item.name.clone().map(|x| x.into_owned()), + description: item.description.clone().map(|x| x.into_owned()), + }); + } + ListResult::End => { + sender.take().unwrap().send(sink.take().ok_or(())); + } + ListResult::Error => { + sender.take().unwrap().send(Err(())); + } + }); + receiver.await.unwrap() + } + + /* + // XXX async wait and handle error + pub fn set_default_sink(&mut self, name: &str) { + self.context.set_default_sink(name, |_| {}); + } + */ + + pub async fn get_source_info_list(&self) -> Result, ()> { + let (sender, receiver) = oneshot::channel(); + let mut sender = Some(sender); + let mut items = Some(Vec::new()); + self.context + .introspect() + .get_source_info_list(move |result| match result { + ListResult::Item(item) => items.as_mut().unwrap().push(Source { + name: item.name.clone().map(|x| x.into_owned()), + description: item.description.clone().map(|x| x.into_owned()), + }), + ListResult::End => { + sender.take().unwrap().send(Ok(items.take().unwrap())); + } + ListResult::Error => { + sender.take().unwrap().send(Err(())); + } + }); + receiver.await.unwrap() + } + + pub async fn get_default_source(&self) -> Result { + let name = match self.get_server_info().await.default_source_name { + Some(name) => name, + None => { + return Err(()); + } + }; + let (sender, receiver) = oneshot::channel(); + let mut sender = Some(sender); + let mut source = None; + self.context + .introspect() + .get_source_info_by_name(&name, move |result| match result { + ListResult::Item(item) => { + source = Some(Source { + name: item.name.clone().map(|x| x.into_owned()), + description: item.description.clone().map(|x| x.into_owned()), + }); + } + ListResult::End => { + sender.take().unwrap().send(source.take().ok_or(())); + } + ListResult::Error => { + sender.take().unwrap().send(Err(())); + } + }); + receiver.await.unwrap() } } diff --git a/applets/cosmic-applet-audio/src/volume.rs b/applets/cosmic-applet-audio/src/volume.rs index 6ee94127..c52020b2 100644 --- a/applets/cosmic-applet-audio/src/volume.rs +++ b/applets/cosmic-applet-audio/src/volume.rs @@ -1,7 +1,8 @@ use gtk4::{prelude::*, Scale}; use libpulse_binding::volume::Volume; -use pulsectl::controllers::types::DeviceInfo; +/* pub fn update_volume(device: &DeviceInfo, scale: &Scale) { scale.set_value((device.volume.avg().0 as f64 / Volume::NORMAL.0 as f64) * 100.); } +*/ From 7212fe545b833e945d9de01b982bdcf9be3eb509 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 13 Jun 2022 18:37:52 -0700 Subject: [PATCH 2/6] Fix updating volume --- applets/cosmic-applet-audio/src/input.rs | 6 ++-- applets/cosmic-applet-audio/src/main.rs | 36 ++++++++++++++++------- applets/cosmic-applet-audio/src/output.rs | 6 ++-- applets/cosmic-applet-audio/src/pa.rs | 29 +++++++++--------- applets/cosmic-applet-audio/src/volume.rs | 4 +-- 5 files changed, 49 insertions(+), 32 deletions(-) diff --git a/applets/cosmic-applet-audio/src/input.rs b/applets/cosmic-applet-audio/src/input.rs index c75d2cbe..8c1dc561 100644 --- a/applets/cosmic-applet-audio/src/input.rs +++ b/applets/cosmic-applet-audio/src/input.rs @@ -2,16 +2,16 @@ use gtk4::{prelude::*, Button, Label, ListBox}; use libcosmic_widgets::{relm4::RelmContainerExt, LabeledItem}; use std::rc::Rc; -use crate::pa::{Source, PA}; +use crate::pa::{DeviceInfo, PA}; -pub async fn get_inputs(pa: &PA) -> Vec { +pub async fn get_inputs(pa: &PA) -> Vec { // XXX handle error pa.get_source_info_list() .await .expect("failed to list input devices") } -pub async fn refresh_default_input(pa: &PA, label: &Label) -> Source { +pub async fn refresh_default_input(pa: &PA, label: &Label) -> DeviceInfo { // XXX handle error let default_input = pa .get_default_source() diff --git a/applets/cosmic-applet-audio/src/main.rs b/applets/cosmic-applet-audio/src/main.rs index 068f15f4..7d997433 100644 --- a/applets/cosmic-applet-audio/src/main.rs +++ b/applets/cosmic-applet-audio/src/main.rs @@ -21,12 +21,15 @@ use gtk4::{ Orientation, PositionType, Revealer, RevealerTransitionType, Scale, SelectionMode, Separator, }; use libpulse_binding::{ - context::subscribe::{Facility, InterestMaskSet, Operation}, + context::{ + subscribe::{Facility, InterestMaskSet, Operation}, + FlagSet, State, + }, volume::Volume, }; use mpris2_zbus::metadata::Metadata; use once_cell::sync::Lazy; -use std::rc::Rc; +use std::{cell::RefCell, rc::Rc}; use tokio::runtime::Runtime; static RT: Lazy = Lazy::new(|| Runtime::new().expect("failed to build tokio runtime")); @@ -61,9 +64,20 @@ fn app(application: &Application) { _ => {} } })))); - pa.context - .subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE, |_| {}); - let pa = Rc::new(pa); + let pa = Rc::new(RefCell::new(pa)); + pa.borrow_mut() + .context + .set_state_callback(Some(Box::new(clone!(@strong pa => move || { + let mut pa = pa.borrow_mut(); + if pa.context.get_state() == State::Ready { + pa.context + .subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE, |_| {}); + } + })))); + pa.borrow_mut() + .context + .connect(None, FlagSet::empty(), None) + .unwrap(); view! { window = ApplicationWindow { set_application: Some(application), @@ -152,18 +166,20 @@ fn app(application: &Application) { glib::MainContext::default().spawn_local( clone!(@weak inputs, @weak current_input, @weak input_volume, @strong pa => async move { while let Some(()) = refresh_input_rx.next().await { - input::refresh_input_widgets(&pa, &inputs); - let default_input = input::refresh_default_input(&pa, ¤t_input); - // XXX volume::update_volume(&default_input, &input_volume); + let pa = pa.borrow(); + input::refresh_input_widgets(&pa, &inputs).await; + let default_input = input::refresh_default_input(&pa, ¤t_input).await; + volume::update_volume(&default_input, &input_volume); } }), ); glib::MainContext::default().spawn_local( clone!(@weak outputs, @weak current_output, @weak output_volume, @strong pa => async move { while let Some(()) = refresh_output_rx.next().await { + let pa = pa.borrow(); output::refresh_output_widgets(&pa, &outputs); - let default_output = output::refresh_default_output(&pa, ¤t_output); - // XXX volume::update_volume(&default_output, &output_volume); + let default_output = output::refresh_default_output(&pa, ¤t_output).await; + volume::update_volume(&default_output, &output_volume); } }), ); diff --git a/applets/cosmic-applet-audio/src/output.rs b/applets/cosmic-applet-audio/src/output.rs index ef9ae407..6684414e 100644 --- a/applets/cosmic-applet-audio/src/output.rs +++ b/applets/cosmic-applet-audio/src/output.rs @@ -2,16 +2,16 @@ use gtk4::{prelude::*, Button, Label, ListBox}; use libcosmic_widgets::{relm4::RelmContainerExt, LabeledItem}; use std::rc::Rc; -use crate::pa::{Sink, PA}; +use crate::pa::{DeviceInfo, PA}; -pub async fn get_outputs(pa: &PA) -> Vec { +pub async fn get_outputs(pa: &PA) -> Vec { // XXX handle error pa.get_sink_info_list() .await .expect("failed to list output devices") } -pub async fn refresh_default_output(pa: &PA, label: &Label) -> Sink { +pub async fn refresh_default_output(pa: &PA, label: &Label) -> DeviceInfo { // XXX handle error let default_output = pa .get_default_sink() diff --git a/applets/cosmic-applet-audio/src/pa.rs b/applets/cosmic-applet-audio/src/pa.rs index ac828b8f..4063cfd0 100644 --- a/applets/cosmic-applet-audio/src/pa.rs +++ b/applets/cosmic-applet-audio/src/pa.rs @@ -2,18 +2,15 @@ use futures::{channel::oneshot, future::poll_fn, task::Poll}; use libpulse_binding::{ callbacks::ListResult, context::{introspect::SinkInfo, Context}, + volume::ChannelVolumes, }; use libpulse_glib_binding::Mainloop; use std::rc::Rc; -pub struct Sink { - pub name: Option, - pub description: Option, -} - -pub struct Source { +pub struct DeviceInfo { pub name: Option, pub description: Option, + pub volume: ChannelVolumes, } pub struct ServerInfo { @@ -45,16 +42,17 @@ impl PA { receiver.await.unwrap() } - pub async fn get_sink_info_list(&self) -> Result, ()> { + pub async fn get_sink_info_list(&self) -> Result, ()> { let (sender, receiver) = oneshot::channel(); let mut sender = Some(sender); let mut items = Some(Vec::new()); self.context .introspect() .get_sink_info_list(move |result| match result { - ListResult::Item(item) => items.as_mut().unwrap().push(Sink { + ListResult::Item(item) => items.as_mut().unwrap().push(DeviceInfo { name: item.name.clone().map(|x| x.into_owned()), description: item.description.clone().map(|x| x.into_owned()), + volume: item.volume, }), ListResult::End => { sender.take().unwrap().send(Ok(items.take().unwrap())); @@ -66,7 +64,7 @@ impl PA { receiver.await.unwrap() } - pub async fn get_default_sink(&self) -> Result { + pub async fn get_default_sink(&self) -> Result { let name = match self.get_server_info().await.default_sink_name { Some(name) => name, None => { @@ -80,9 +78,10 @@ impl PA { .introspect() .get_sink_info_by_name(&name, move |result| match result { ListResult::Item(item) => { - sink = Some(Sink { + sink = Some(DeviceInfo { name: item.name.clone().map(|x| x.into_owned()), description: item.description.clone().map(|x| x.into_owned()), + volume: item.volume, }); } ListResult::End => { @@ -102,16 +101,17 @@ impl PA { } */ - pub async fn get_source_info_list(&self) -> Result, ()> { + pub async fn get_source_info_list(&self) -> Result, ()> { let (sender, receiver) = oneshot::channel(); let mut sender = Some(sender); let mut items = Some(Vec::new()); self.context .introspect() .get_source_info_list(move |result| match result { - ListResult::Item(item) => items.as_mut().unwrap().push(Source { + ListResult::Item(item) => items.as_mut().unwrap().push(DeviceInfo { name: item.name.clone().map(|x| x.into_owned()), description: item.description.clone().map(|x| x.into_owned()), + volume: item.volume, }), ListResult::End => { sender.take().unwrap().send(Ok(items.take().unwrap())); @@ -123,7 +123,7 @@ impl PA { receiver.await.unwrap() } - pub async fn get_default_source(&self) -> Result { + pub async fn get_default_source(&self) -> Result { let name = match self.get_server_info().await.default_source_name { Some(name) => name, None => { @@ -137,9 +137,10 @@ impl PA { .introspect() .get_source_info_by_name(&name, move |result| match result { ListResult::Item(item) => { - source = Some(Source { + source = Some(DeviceInfo { name: item.name.clone().map(|x| x.into_owned()), description: item.description.clone().map(|x| x.into_owned()), + volume: item.volume, }); } ListResult::End => { diff --git a/applets/cosmic-applet-audio/src/volume.rs b/applets/cosmic-applet-audio/src/volume.rs index c52020b2..0cc163f1 100644 --- a/applets/cosmic-applet-audio/src/volume.rs +++ b/applets/cosmic-applet-audio/src/volume.rs @@ -1,8 +1,8 @@ use gtk4::{prelude::*, Scale}; use libpulse_binding::volume::Volume; -/* +use crate::pa::DeviceInfo; + pub fn update_volume(device: &DeviceInfo, scale: &Scale) { scale.set_value((device.volume.avg().0 as f64 / Volume::NORMAL.0 as f64) * 100.); } -*/ From 9f3803fedcccb6bbef5d1b02f174cd24415024bb Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 13 Jun 2022 18:43:35 -0700 Subject: [PATCH 3/6] Refresh inputs/outputs at start --- applets/cosmic-applet-audio/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/applets/cosmic-applet-audio/src/main.rs b/applets/cosmic-applet-audio/src/main.rs index 7d997433..7bed5f88 100644 --- a/applets/cosmic-applet-audio/src/main.rs +++ b/applets/cosmic-applet-audio/src/main.rs @@ -72,6 +72,8 @@ fn app(application: &Application) { if pa.context.get_state() == State::Ready { pa.context .subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE, |_| {}); + refresh_output_tx.unbounded_send(()).expect("failed to send output refresh message"); + refresh_input_tx.unbounded_send(()).expect("failed to send output refresh message"); } })))); pa.borrow_mut() From a43145746829ad80de0f0eef76595dd85dfbb00e Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Thu, 23 Jun 2022 12:46:38 -0700 Subject: [PATCH 4/6] Refactor `PA` to handle interior mutability / ref counting itself --- applets/cosmic-applet-audio/src/main.rs | 39 +++++-------- applets/cosmic-applet-audio/src/pa.rs | 78 ++++++++++++++++++++----- 2 files changed, 79 insertions(+), 38 deletions(-) diff --git a/applets/cosmic-applet-audio/src/main.rs b/applets/cosmic-applet-audio/src/main.rs index 7bed5f88..41d7ca43 100644 --- a/applets/cosmic-applet-audio/src/main.rs +++ b/applets/cosmic-applet-audio/src/main.rs @@ -29,7 +29,6 @@ use libpulse_binding::{ }; use mpris2_zbus::metadata::Metadata; use once_cell::sync::Lazy; -use std::{cell::RefCell, rc::Rc}; use tokio::runtime::Runtime; static RT: Lazy = Lazy::new(|| Runtime::new().expect("failed to build tokio runtime")); @@ -45,12 +44,12 @@ fn main() { fn app(application: &Application) { // XXX handle no pulseaudio daemon? - let mut pa = PA::new().unwrap(); + let pa = PA::new().unwrap(); let (refresh_output_tx, mut refresh_output_rx) = mpsc::unbounded(); let (refresh_input_tx, mut refresh_input_rx) = mpsc::unbounded(); let (now_playing_tx, mut now_playing_rx) = mpsc::unbounded::>(); - pa.context - .set_subscribe_callback(Some(Box::new(clone!(@strong refresh_output_tx, @strong refresh_input_tx => move |facility, operation, _idx| { + pa + .set_subscribe_callback(clone!(@strong refresh_output_tx, @strong refresh_input_tx => move |facility, operation, _idx| { if !matches!(operation, Some(Operation::Changed)) { return; } @@ -63,23 +62,19 @@ fn app(application: &Application) { } _ => {} } - })))); - let pa = Rc::new(RefCell::new(pa)); - pa.borrow_mut() - .context - .set_state_callback(Some(Box::new(clone!(@strong pa => move || { - let mut pa = pa.borrow_mut(); - if pa.context.get_state() == State::Ready { - pa.context - .subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE, |_| {}); - refresh_output_tx.unbounded_send(()).expect("failed to send output refresh message"); - refresh_input_tx.unbounded_send(()).expect("failed to send output refresh message"); - } - })))); - pa.borrow_mut() - .context - .connect(None, FlagSet::empty(), None) - .unwrap(); + })); + pa.set_state_callback(move |pa, state| { + if state == State::Ready { + pa.subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE); + refresh_output_tx + .unbounded_send(()) + .expect("failed to send output refresh message"); + refresh_input_tx + .unbounded_send(()) + .expect("failed to send output refresh message"); + } + }); + pa.connect().unwrap(); // XXX unwrap view! { window = ApplicationWindow { set_application: Some(application), @@ -168,7 +163,6 @@ fn app(application: &Application) { glib::MainContext::default().spawn_local( clone!(@weak inputs, @weak current_input, @weak input_volume, @strong pa => async move { while let Some(()) = refresh_input_rx.next().await { - let pa = pa.borrow(); input::refresh_input_widgets(&pa, &inputs).await; let default_input = input::refresh_default_input(&pa, ¤t_input).await; volume::update_volume(&default_input, &input_volume); @@ -178,7 +172,6 @@ fn app(application: &Application) { glib::MainContext::default().spawn_local( clone!(@weak outputs, @weak current_output, @weak output_volume, @strong pa => async move { while let Some(()) = refresh_output_rx.next().await { - let pa = pa.borrow(); output::refresh_output_widgets(&pa, &outputs); let default_output = output::refresh_default_output(&pa, ¤t_output).await; volume::update_volume(&default_output, &output_volume); diff --git a/applets/cosmic-applet-audio/src/pa.rs b/applets/cosmic-applet-audio/src/pa.rs index 4063cfd0..c672fe30 100644 --- a/applets/cosmic-applet-audio/src/pa.rs +++ b/applets/cosmic-applet-audio/src/pa.rs @@ -1,11 +1,19 @@ use futures::{channel::oneshot, future::poll_fn, task::Poll}; use libpulse_binding::{ callbacks::ListResult, - context::{introspect::SinkInfo, Context}, + context::{ + introspect::{Introspector, SinkInfo}, + subscribe::{Facility, InterestMaskSet, Operation}, + Context, FlagSet, State, + }, + error::PAErr, volume::ChannelVolumes, }; use libpulse_glib_binding::Mainloop; -use std::rc::Rc; +use std::{ + cell::{Ref, RefCell}, + rc::Rc, +}; pub struct DeviceInfo { pub name: Option, @@ -18,22 +26,66 @@ pub struct ServerInfo { pub default_source_name: Option, } -pub struct PA { +struct PAInner { main_loop: Mainloop, - pub context: Context, + pub context: RefCell, } +#[derive(Clone)] +pub struct PA(Rc); + impl PA { pub fn new() -> Option { let main_loop = Mainloop::new(None)?; let context = Context::new(&main_loop, "com.system76.cosmic.applets.audio")?; - Some(Self { main_loop, context }) + Some(Self(Rc::new(PAInner { + main_loop, + context: RefCell::new(context), + }))) + } + + pub fn set_state_callback(&self, cb: F) { + let pa = self.clone(); // TODO: weak ref? + self.0 + .context + .borrow_mut() + .set_state_callback(Some(Box::new(move || { + let state = pa.0.context.borrow().get_state(); + cb(&pa, state); + }))); + } + + // TODO: builder pattern? + pub fn set_subscribe_callback, Option, u32) + 'static>( + &self, + cb: F, + ) { + self.0 + .context + .borrow_mut() + .set_subscribe_callback(Some(Box::new(cb))); + } + + pub fn subscribe(&self, mask: InterestMaskSet) { + // XXX cb; operation; async + self.0.context.borrow_mut().subscribe(mask, |_| {}); + } + + pub fn connect(&self) -> Result<(), PAErr> { + self.0 + .context + .borrow_mut() + .connect(None, FlagSet::empty(), None) + } + + fn introspect(&self) -> Introspector { + self.0.context.borrow().introspect() } pub async fn get_server_info(&self) -> ServerInfo { let (sender, receiver) = oneshot::channel(); let mut sender = Some(sender); - self.context.introspect().get_server_info(move |info| { + self.introspect().get_server_info(move |info| { sender.take().unwrap().send(ServerInfo { default_sink_name: info.default_sink_name.clone().map(|x| x.into_owned()), default_source_name: info.default_source_name.clone().map(|x| x.into_owned()), @@ -46,8 +98,7 @@ impl PA { let (sender, receiver) = oneshot::channel(); let mut sender = Some(sender); let mut items = Some(Vec::new()); - self.context - .introspect() + self.introspect() .get_sink_info_list(move |result| match result { ListResult::Item(item) => items.as_mut().unwrap().push(DeviceInfo { name: item.name.clone().map(|x| x.into_owned()), @@ -74,8 +125,7 @@ impl PA { let (sender, receiver) = oneshot::channel(); let mut sender = Some(sender); let mut sink = None; - self.context - .introspect() + self.introspect() .get_sink_info_by_name(&name, move |result| match result { ListResult::Item(item) => { sink = Some(DeviceInfo { @@ -97,7 +147,7 @@ impl PA { /* // XXX async wait and handle error pub fn set_default_sink(&mut self, name: &str) { - self.context.set_default_sink(name, |_| {}); + self.0.context.set_default_sink(name, |_| {}); } */ @@ -105,8 +155,7 @@ impl PA { let (sender, receiver) = oneshot::channel(); let mut sender = Some(sender); let mut items = Some(Vec::new()); - self.context - .introspect() + self.introspect() .get_source_info_list(move |result| match result { ListResult::Item(item) => items.as_mut().unwrap().push(DeviceInfo { name: item.name.clone().map(|x| x.into_owned()), @@ -133,8 +182,7 @@ impl PA { let (sender, receiver) = oneshot::channel(); let mut sender = Some(sender); let mut source = None; - self.context - .introspect() + self.introspect() .get_source_info_by_name(&name, move |result| match result { ListResult::Item(item) => { source = Some(DeviceInfo { From 2e04938bbd73b323aece6675a699a566d8bb135e Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Thu, 23 Jun 2022 13:04:54 -0700 Subject: [PATCH 5/6] audio: Fix setting default source/sink --- applets/cosmic-applet-audio/src/input.rs | 14 ++++---------- applets/cosmic-applet-audio/src/output.rs | 14 ++++---------- applets/cosmic-applet-audio/src/pa.rs | 10 ++++++---- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/applets/cosmic-applet-audio/src/input.rs b/applets/cosmic-applet-audio/src/input.rs index 8c1dc561..4975f700 100644 --- a/applets/cosmic-applet-audio/src/input.rs +++ b/applets/cosmic-applet-audio/src/input.rs @@ -1,4 +1,4 @@ -use gtk4::{prelude::*, Button, Label, ListBox}; +use gtk4::{glib::clone, prelude::*, Button, Label, ListBox}; use libcosmic_widgets::{relm4::RelmContainerExt, LabeledItem}; use std::rc::Rc; @@ -40,15 +40,9 @@ pub async fn refresh_input_widgets(pa: &PA, inputs: &ListBox) { .unwrap_or(&name), set_child: set_current_input_device = &Button { set_label: "Switch", - connect_clicked: move |_| { - // XXX Need mutable borrow? Is this a problem for async? - /* - SourceController::create() - .expect("failed to create input controller") - .set_default_device(&name) - .expect("failed to set default device"); - */ - } + connect_clicked: clone!(@strong pa => move |_| { + pa.set_default_source(&name); + }) } } } diff --git a/applets/cosmic-applet-audio/src/output.rs b/applets/cosmic-applet-audio/src/output.rs index 6684414e..f05bd94f 100644 --- a/applets/cosmic-applet-audio/src/output.rs +++ b/applets/cosmic-applet-audio/src/output.rs @@ -1,4 +1,4 @@ -use gtk4::{prelude::*, Button, Label, ListBox}; +use gtk4::{glib::clone, prelude::*, Button, Label, ListBox}; use libcosmic_widgets::{relm4::RelmContainerExt, LabeledItem}; use std::rc::Rc; @@ -40,15 +40,9 @@ pub async fn refresh_output_widgets(pa: &PA, outputs: &ListBox) { .unwrap_or(&name), set_child: set_current_input_device = &Button { set_label: "Switch", - connect_clicked: move |_| { - // XXX Need mutable borrow? Is this a problem for async? - /* - SinkController::create() - .expect("failed to create output controller") - .set_default_device(&name) - .expect("failed to set default device"); - */ - } + connect_clicked: clone!(@strong pa => move |_| { + pa.set_default_sink(&name); + }) } } } diff --git a/applets/cosmic-applet-audio/src/pa.rs b/applets/cosmic-applet-audio/src/pa.rs index c672fe30..bef28b7e 100644 --- a/applets/cosmic-applet-audio/src/pa.rs +++ b/applets/cosmic-applet-audio/src/pa.rs @@ -144,12 +144,14 @@ impl PA { receiver.await.unwrap() } - /* // XXX async wait and handle error - pub fn set_default_sink(&mut self, name: &str) { - self.0.context.set_default_sink(name, |_| {}); + pub fn set_default_sink(&self, name: &str) { + self.0.context.borrow_mut().set_default_sink(name, |_| {}); + } + + pub fn set_default_source(&self, name: &str) { + self.0.context.borrow_mut().set_default_source(name, |_| {}); } - */ pub async fn get_source_info_list(&self) -> Result, ()> { let (sender, receiver) = oneshot::channel(); From 1d40fe9a23804bb3af87f839ba1e3703118dd9ef Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 27 Jun 2022 18:36:28 -0700 Subject: [PATCH 6/6] audio: Use relm4 next branch --- Cargo.lock | 143 +----------------------- Cargo.toml | 2 +- applets/cosmic-applet-audio/Cargo.toml | 7 +- applets/cosmic-applet-audio/src/app.rs | 121 ++++++++++---------- applets/cosmic-applet-audio/src/main.rs | 19 ++-- applets/cosmic-applet-audio/src/pa.rs | 5 + debian/rules | 3 +- justfile | 3 +- 8 files changed, 86 insertions(+), 217 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30fa82d1..9f3efacc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -309,26 +309,6 @@ dependencies = [ "xdg", ] -[[package]] -name = "cosmic-applet-audio" -version = "0.1.0" -dependencies = [ - "async-io", - "freedesktop-desktop-entry", - "futures", - "futures-util", - "gtk4", - "libcosmic-widgets", - "libpulse-binding", - "libpulse-glib-binding", - "mpris2-zbus", - "once_cell", - "relm4-macros 0.4.4", - "tokio", - "tracker", - "zbus", -] - [[package]] name = "cosmic-applet-graphics" version = "0.1.0" @@ -610,15 +590,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "dirs" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" -dependencies = [ - "dirs-sys", -] - [[package]] name = "dirs" version = "4.0.0" @@ -815,19 +786,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9d758e60b45e8d749c89c1b389ad8aee550f86aa12e2b9298b546dda7a82ab1" -[[package]] -name = "freedesktop-desktop-entry" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45157175a725e81f3f594382430b6b78af5f8f72db9bd51b94f0785f80fc6d29" -dependencies = [ - "dirs 3.0.2", - "gettext-rs", - "memchr", - "thiserror", - "xdg", -] - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1058,26 +1016,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "gettext-rs" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364" -dependencies = [ - "gettext-sys", - "locale_config", -] - -[[package]] -name = "gettext-sys" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d" -dependencies = [ - "cc", - "temp-dir", -] - [[package]] name = "gio" version = "0.15.11" @@ -1528,56 +1466,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "libpulse-binding" -version = "2.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17be42160017e0ae993c03bfdab4ecb6f82ce3f8d515bd8da8fdf18d10703663" -dependencies = [ - "bitflags", - "libc", - "libpulse-sys", - "num-derive", - "num-traits", - "winapi", -] - -[[package]] -name = "libpulse-glib-binding" -version = "2.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0e7a964c9f7e95d4f073affc19adfda009fa0d55e8831dbb66c78be1d0e6e5" -dependencies = [ - "glib", - "glib-sys", - "libpulse-binding", - "libpulse-mainloop-glib-sys", -] - -[[package]] -name = "libpulse-mainloop-glib-sys" -version = "1.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f61c4064926cc77ea14bb206a21ce1d5a06e175e5c0ce078804bb6c4527b28" -dependencies = [ - "glib-sys", - "libpulse-sys", - "pkg-config", -] - -[[package]] -name = "libpulse-sys" -version = "1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991e6bd0efe2a36e6534e136e7996925e4c1a8e35b7807fe533f2beffff27c30" -dependencies = [ - "libc", - "num-derive", - "num-traits", - "pkg-config", - "winapi", -] - [[package]] name = "locale_config" version = "0.3.0" @@ -1672,18 +1560,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "mpris2-zbus" -version = "0.1.0" -source = "git+https://github.com/pop-os/mpris2-zbus#bcc8481ea7ccfc08aa870f28272d9093db3b1ba9" -dependencies = [ - "serde", - "thiserror", - "time 0.3.9", - "zbus", - "zvariant", -] - [[package]] name = "nanorand" version = "0.7.0" @@ -1759,17 +1635,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-derive" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "num-integer" version = "0.1.45" @@ -2501,12 +2366,6 @@ dependencies = [ "version-compare", ] -[[package]] -name = "temp-dir" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab" - [[package]] name = "tempfile" version = "3.3.0" @@ -2971,7 +2830,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" dependencies = [ - "dirs 4.0.0", + "dirs", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c085b67b..02de8a98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,5 @@ [workspace] members = [ - "applets/cosmic-applet-audio", "applets/cosmic-applet-graphics", "applets/cosmic-applet-network", "applets/cosmic-applet-notifications", @@ -13,5 +12,6 @@ members = [ ] exclude = [ + "applets/cosmic-applet-audio", "applets/cosmic-applet-battery", ] diff --git a/applets/cosmic-applet-audio/Cargo.toml b/applets/cosmic-applet-audio/Cargo.toml index 993321a6..acdda3e1 100644 --- a/applets/cosmic-applet-audio/Cargo.toml +++ b/applets/cosmic-applet-audio/Cargo.toml @@ -7,7 +7,7 @@ license = "GPL-3.0-or-later" [dependencies] futures = "0.3.21" futures-util = "0.3.21" -libcosmic-widgets = { git = "https://github.com/pop-os/libcosmic" } +libcosmic-widgets = { git = "https://github.com/pop-os/libcosmic", branch = "relm4-next" } libpulse-binding = "2.26.0" libpulse-glib-binding = "2.25.0" tracker = "0.1.1" @@ -15,9 +15,10 @@ freedesktop-desktop-entry = "0.5.0" mpris2-zbus = { git = "https://github.com/pop-os/mpris2-zbus" } zbus = "2.1.1" tokio = { version = "1.17.0", features = ["full"] } -relm4-macros = "0.4.4" +relm4 = { git = "https://github.com/relm4/relm4", branch = "next", features = ["macros"] } +relm4-macros = { git = "https://github.com/relm4/relm4", branch = "next" } once_cell = "1.10.0" -gtk4 = { version = "0.4.7", features = ["v4_2"] } +gtk4 = { git = "https://github.com/gtk-rs/gtk4-rs", features = ["v4_2"] } async-io = "1.6.0" [features] diff --git a/applets/cosmic-applet-audio/src/app.rs b/applets/cosmic-applet-audio/src/app.rs index 00188c65..988b45f4 100644 --- a/applets/cosmic-applet-audio/src/app.rs +++ b/applets/cosmic-applet-audio/src/app.rs @@ -2,17 +2,10 @@ use crate::icons::{parse_desktop_icons, DesktopApplication}; use futures_util::StreamExt; use libcosmic_widgets::LabeledItem; use libpulse_binding::{ - context::subscribe::{Facility, InterestMaskSet, Operation}, + context::{subscribe::{Facility, InterestMaskSet, Operation}, State}, volume::Volume, }; use mpris2_zbus::media_player::MediaPlayer; -use pulsectl::{ - controllers::{ - types::{ApplicationInfo, DeviceInfo}, - AppControl, DeviceControl, SinkController, SourceController, - }, - Handler, -}; use relm4::{ component, gtk::{ @@ -22,12 +15,14 @@ use relm4::{ Align, Box as GtkBox, Button, Image, Label, ListBox, Orientation, PositionType, Revealer, RevealerTransitionType, Scale, Separator, Window, }, - view, ComponentParts, RelmContainerExt, Sender, SimpleComponent, + view, ComponentParts, ComponentSender, RelmContainerExt, Sender, SimpleComponent, send, }; use std::{collections::HashMap, rc::Rc}; use tracker::track; use zbus::Connection; +use crate::pa::{DeviceInfo, PA}; + pub enum AppInput { Inputs, Outputs, @@ -59,7 +54,7 @@ pub struct App { #[do_not_track] desktop_icons: HashMap, #[do_not_track] - handler: Handler, + pa: PA, } impl Default for App { @@ -74,15 +69,8 @@ impl Default for App { let outputs = output_controller.list_devices().unwrap_or_default(); let now_playing = Vec::new(); let desktop_icons = parse_desktop_icons(); - let handler = Handler::connect("com.system76.cosmic.applets.audio") - .expect("failed to connect to pulse"); - relm4::spawn_local(clone!(@weak handler.mainloop as main_loop => async move { - let mut timer = async_io::Timer::interval(std::time::Duration::from_millis(100)); - loop { - main_loop.borrow_mut().iterate(false); - timer.next().await; - } - })); + // XXX handle no pulseaudio daemon? + let pa = PA::new().unwrap(); Self { default_input, inputs, @@ -90,7 +78,7 @@ impl Default for App { outputs, now_playing, desktop_icons, - handler, + pa, tracker: 0, } } @@ -142,23 +130,27 @@ impl App { } fn subscribe_for_updates(&self, input: &Sender) { - let mut context = self.handler.context.borrow_mut(); let input_clone = input.clone(); - context.set_subscribe_callback(Some(Box::new(move |facility, operation, _idx| { + self.pa.set_subscribe_callback(move |facility, operation, _idx| { if !matches!(operation, Some(Operation::Changed)) { return; } match facility { Some(Facility::Sink) => { - send!(input_clone, AppInput::OutputVolume); + input_clone.send(AppInput::OutputVolume); } Some(Facility::Source) => { - send!(input_clone, AppInput::InputVolume); + input_clone.send(AppInput::InputVolume); } _ => {} } - }))); - context.subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE, |_| {}); + }); + self.pa.set_state_callback(move |pa, state| { + if state == State::Ready { + pa.subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE); + } + }); + } fn refresh_input_list(&mut self) { let mut input_controller = @@ -277,7 +269,8 @@ impl App { append: media_buttons = &GtkBox { set_halign: Align::End, append: pause_button = &Button { - set_child: pause_button_img = Some(&Image) { + #[wrap(Some)] + set_child: pause_button_img = &Image { set_icon_name: Some("media-playback-pause-symbolic"), set_pixel_size: 24, }, @@ -309,89 +302,95 @@ impl SimpleComponent for App { set_default_width: 400, set_default_height: 300, - &GtkBox { + GtkBox { set_orientation: Orientation::Vertical, set_spacing: 24, - &GtkBox { + GtkBox { set_orientation: Orientation::Horizontal, set_spacing: 16, - &Image { + Image { set_icon_name: Some("audio-speakers-symbolic"), }, append: output_volume = &Scale::with_range(Orientation::Horizontal, 0., 100., 1.) { set_format_value_func: |_, value| { format!("{:.0}%", value) }, - set_value: watch! { model.default_output.as_ref().map(|info| (info.volume.avg().0 as f64 / Volume::NORMAL.0 as f64) * 100.).unwrap_or(0.) }, + #[watch] + set_value: model.default_output.as_ref().map(|info| (info.volume.avg().0 as f64 / Volume::NORMAL.0 as f64) * 100.).unwrap_or(0.), set_value_pos: PositionType::Right, set_hexpand: true } }, - &GtkBox { + GtkBox { set_orientation: Orientation::Horizontal, set_spacing: 16, - &Image { + Image { set_icon_name: Some("audio-input-microphone-symbolic"), }, append: input_volume = &Scale::with_range(Orientation::Horizontal, 0., 100., 1.) { set_format_value_func: |_, value| { format!("{:.0}%", value) }, - set_value: watch! { - model.default_input - .as_ref() - .map(|info| (info.volume.avg().0 as f64 / Volume::NORMAL.0 as f64) * 100.) - .unwrap_or(0.) - }, + #[watch] + set_value: model.default_input + .as_ref() + .map(|info| (info.volume.avg().0 as f64 / Volume::NORMAL.0 as f64) * 100.) + .unwrap_or(0.), set_value_pos: PositionType::Right, set_hexpand: true } }, - &Separator { + Separator { set_orientation: Orientation::Horizontal, }, - &GtkBox { + GtkBox { set_orientation: Orientation::Vertical, - &Button { - set_child: current_output = Some(&Label) { - set_text: watch! { model.get_default_output_name() } + Button { + #[wrap(Some)] + set_child: current_output = &Label { + #[watch] + set_text: model.get_default_output_name() }, - connect_clicked(input, outputs_revealer) => move |_| { - send!(input, AppInput::Outputs); + connect_clicked[sender, outputs_revealer] => move |_| { + sender.input(AppInput::Outputs); outputs_revealer.set_reveal_child(!outputs_revealer.reveals_child()); } }, append: outputs_revealer = &Revealer { set_transition_type: RevealerTransitionType::SlideDown, - set_child: outputs = Some(&ListBox) { + #[wrap(Some)] + set_child: outputs = &ListBox { set_selection_mode: gtk::SelectionMode::None, set_activate_on_single_click: true } } }, - &Separator { + Separator { set_orientation: Orientation::Horizontal, }, - &GtkBox { + GtkBox { set_orientation: Orientation::Vertical, - &Button { - set_child: current_input = Some(&Label) { - set_text: watch! { model.get_default_input_name() } + Button { + #[wrap(Some)] + set_child: current_input = &Label { + #[watch] + set_text: model.get_default_input_name() }, - connect_clicked(input, inputs_revealer) => move |_| { - send!(input, AppInput::Inputs); + connect_clicked[sender, inputs_revealer] => move |_| { + sender.input(AppInput::Inputs); inputs_revealer.set_reveal_child(!inputs_revealer.reveals_child()); } }, append: inputs_revealer = &Revealer { set_transition_type: RevealerTransitionType::SlideDown, - set_child: inputs = Some(&ListBox) { + #[wrap(Some)] + set_child: inputs = &ListBox { set_selection_mode: gtk::SelectionMode::None, set_activate_on_single_click: true } } }, - &Separator { + Separator { set_orientation: Orientation::Horizontal, }, append: playing_apps = &ListBox { @@ -401,15 +400,14 @@ impl SimpleComponent for App { } } - fn init_parts( + fn init( _init_params: Self::InitParams, root: &Self::Root, - input: &Sender, - _output: &Sender, + sender: &ComponentSender, ) -> ComponentParts { let model = App::default(); let widgets = view_output!(); - model.subscribe_for_updates(input); + model.subscribe_for_updates(&sender.input); ComponentParts { model, widgets } } @@ -417,8 +415,7 @@ impl SimpleComponent for App { fn update( &mut self, msg: Self::Input, - _input: &Sender, - _output: &Sender, + _sender: &ComponentSender, ) { self.reset(); match msg { diff --git a/applets/cosmic-applet-audio/src/main.rs b/applets/cosmic-applet-audio/src/main.rs index 41d7ca43..4c4f7fe0 100644 --- a/applets/cosmic-applet-audio/src/main.rs +++ b/applets/cosmic-applet-audio/src/main.rs @@ -82,7 +82,8 @@ fn app(application: &Application) { set_default_width: 400, set_default_height: 300, - set_child: window_box = Some(&GtkBox) { + #[wrap(Some)] + set_child: window_box = &GtkBox { set_orientation: Orientation::Vertical, set_spacing: 24, append: output_box = &GtkBox { @@ -119,14 +120,16 @@ fn app(application: &Application) { append: output_list_box = &GtkBox { set_orientation: Orientation::Vertical, append: current_output_button = &Button { - set_child: current_output = Some(&Label) {}, - connect_clicked(outputs_revealer) => move |_| { + #[wrap(Some)] + set_child: current_output = &Label {}, + connect_clicked[outputs_revealer] => move |_| { outputs_revealer.set_reveal_child(!outputs_revealer.reveals_child()); } }, append: outputs_revealer = &Revealer { set_transition_type: RevealerTransitionType::SlideDown, - set_child: outputs = Some(&ListBox) { + #[wrap(Some)] + set_child: outputs = &ListBox { set_selection_mode: SelectionMode::None, set_activate_on_single_click: true } @@ -138,14 +141,16 @@ fn app(application: &Application) { append: input_list_box = &GtkBox { set_orientation: Orientation::Vertical, append: current_input_button = &Button { - set_child: current_input = Some(&Label) {}, - connect_clicked(inputs_revealer) => move |_| { + #[wrap(Some)] + set_child: current_input = &Label {}, + connect_clicked[inputs_revealer] => move |_| { inputs_revealer.set_reveal_child(!inputs_revealer.reveals_child()); } }, append: inputs_revealer = &Revealer { set_transition_type: RevealerTransitionType::SlideDown, - set_child: inputs = Some(&ListBox) { + #[wrap(Some)] + set_child: inputs = &ListBox { set_selection_mode: SelectionMode::None, set_activate_on_single_click: true } diff --git a/applets/cosmic-applet-audio/src/pa.rs b/applets/cosmic-applet-audio/src/pa.rs index bef28b7e..093ef39e 100644 --- a/applets/cosmic-applet-audio/src/pa.rs +++ b/applets/cosmic-applet-audio/src/pa.rs @@ -19,6 +19,7 @@ pub struct DeviceInfo { pub name: Option, pub description: Option, pub volume: ChannelVolumes, + pub index: u32, } pub struct ServerInfo { @@ -104,6 +105,7 @@ impl PA { name: item.name.clone().map(|x| x.into_owned()), description: item.description.clone().map(|x| x.into_owned()), volume: item.volume, + index: item.index, }), ListResult::End => { sender.take().unwrap().send(Ok(items.take().unwrap())); @@ -132,6 +134,7 @@ impl PA { name: item.name.clone().map(|x| x.into_owned()), description: item.description.clone().map(|x| x.into_owned()), volume: item.volume, + index: item.index, }); } ListResult::End => { @@ -163,6 +166,7 @@ impl PA { name: item.name.clone().map(|x| x.into_owned()), description: item.description.clone().map(|x| x.into_owned()), volume: item.volume, + index: item.index, }), ListResult::End => { sender.take().unwrap().send(Ok(items.take().unwrap())); @@ -191,6 +195,7 @@ impl PA { name: item.name.clone().map(|x| x.into_owned()), description: item.description.clone().map(|x| x.into_owned()), volume: item.volume, + index: item.index, }); } ListResult::End => { diff --git a/debian/rules b/debian/rules index aaa756a9..11deba6b 100755 --- a/debian/rules +++ b/debian/rules @@ -13,12 +13,13 @@ override_dh_shlibdeps: override_dh_auto_clean: if test "${CLEAN}" = "1"; then \ cargo clean; \ + cargo clean --manifest-path applets/cosmic-applet-audio/Cargo.toml; \ cargo clean --manifest-path applets/cosmic-applet-battery/Cargo.toml; \ fi if ! ischroot && test "${VENDOR}" = "1"; then \ mkdir -p .cargo; \ - cargo vendor --sync Cargo.toml --sync applets/cosmic-applet-battery/Cargo.toml | head -n -1 > .cargo/config; \ + cargo vendor --sync Cargo.toml --sync applets/cosmic-applet-audio/Cargo.toml applets/cosmic-applet-battery/Cargo.toml | head -n -1 > .cargo/config; \ echo 'directory = "vendor"' >> .cargo/config; \ tar pcf vendor.tar vendor; \ rm -rf vendor; \ diff --git a/justfile b/justfile index ee45c06b..3d147263 100644 --- a/justfile +++ b/justfile @@ -28,6 +28,7 @@ workspaces_button_id := 'com.system76.CosmicPanelWorkspacesButton' all: _extract_vendor cargo build {{cargo_args}} + cargo build --manifest-path applets/cosmic-applet-audio/Cargo.toml {{cargo_args}} cargo build --manifest-path applets/cosmic-applet-battery/Cargo.toml {{cargo_args}} # Installs files into the system @@ -42,7 +43,7 @@ install: # audio install -Dm0644 applets/cosmic-applet-audio/data/icons/{{audio_id}}.svg {{iconsdir}}/{{audio_id}}.svg install -Dm0644 applets/cosmic-applet-audio/data/{{audio_id}}.desktop {{sharedir}}/applications/{{audio_id}}.desktop - install -Dm0755 target/release/cosmic-applet-audio {{bindir}}/cosmic-applet-audio + install -Dm0755 applets/cosmic-applet-audio/target/release/cosmic-applet-audio {{bindir}}/cosmic-applet-audio # battery install -Dm0644 applets/cosmic-applet-battery/data/icons/{{battery_id}}.svg {{iconsdir}}/{{battery_id}}.svg