diff --git a/Cargo.lock b/Cargo.lock index 36ae5357..c3e0d674 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -340,25 +340,6 @@ dependencies = [ "xdg", ] -[[package]] -name = "cosmic-applet-audio" -version = "0.1.0" -dependencies = [ - "async-io", - "freedesktop-desktop-entry", - "futures-util", - "gtk4", - "libcosmic-widgets", - "libpulse-binding", - "mpris2-zbus", - "once_cell", - "pulsectl-rs", - "relm4-macros 0.4.4", - "tokio", - "tracker", - "zbus", -] - [[package]] name = "cosmic-applet-graphics" version = "0.1.0" @@ -620,15 +601,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" @@ -850,19 +822,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 = "futures" version = "0.3.21" @@ -1084,26 +1043,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" @@ -1541,33 +1480,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-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" @@ -1662,18 +1574,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" @@ -1731,17 +1631,6 @@ dependencies = [ "minimal-lexical", ] -[[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" @@ -2042,15 +1931,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" @@ -2168,17 +2048,6 @@ dependencies = [ "syn", ] -[[package]] -name = "relm4-macros" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7136d9b9b97dc87198c619587de7bd61aca5ec4bec58a7167404c1edf750a490" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "relm4-macros" version = "0.5.0-beta.1" @@ -2555,12 +2424,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" @@ -3058,7 +2921,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 5b85e326..acdda3e1 100644 --- a/applets/cosmic-applet-audio/Cargo.toml +++ b/applets/cosmic-applet-audio/Cargo.toml @@ -5,18 +5,20 @@ 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" } +libcosmic-widgets = { git = "https://github.com/pop-os/libcosmic", branch = "relm4-next" } 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" } 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/input.rs b/applets/cosmic-applet-audio/src/input.rs index d7a8cebd..4975f700 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 gtk4::{glib::clone, 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::{DeviceInfo, 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) -> DeviceInfo { + // 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. @@ -39,12 +40,9 @@ pub fn refresh_input_widgets(inputs: &ListBox) { .unwrap_or(&name), set_child: set_current_input_device = &Button { set_label: "Switch", - connect_clicked: move |_| { - 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/main.rs b/applets/cosmic-applet-audio/src/main.rs index edd398cc..4c4f7fe0 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}, @@ -19,12 +21,14 @@ 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 pulsectl::Handler; use tokio::runtime::Runtime; static RT: Lazy = Lazy::new(|| Runtime::new().expect("failed to build tokio runtime")); @@ -39,35 +43,38 @@ 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() - .set_subscribe_callback(Some(Box::new(clone!(@strong refresh_output_tx, @strong refresh_input_tx => move |facility, operation, _idx| { + // XXX handle no pulseaudio daemon? + 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 + .set_subscribe_callback(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() - .subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE, |_| {}); + })); + 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), @@ -75,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 { @@ -112,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 } @@ -131,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 } @@ -153,29 +165,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).await; + let default_input = input::refresh_default_input(&pa, ¤t_input).await; + 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).await; + 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..f05bd94f 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 gtk4::{glib::clone, 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::{DeviceInfo, 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) -> DeviceInfo { + // 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. @@ -39,12 +40,9 @@ pub fn refresh_output_widgets(outputs: &ListBox) { .unwrap_or(&name), set_child: set_current_input_device = &Button { set_label: "Switch", - connect_clicked: move |_| { - 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 9a4b1d58..093ef39e 100644 --- a/applets/cosmic-applet-audio/src/pa.rs +++ b/applets/cosmic-applet-audio/src/pa.rs @@ -1,12 +1,210 @@ -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::{Introspector, SinkInfo}, + subscribe::{Facility, InterestMaskSet, Operation}, + Context, FlagSet, State, + }, + error::PAErr, + volume::ChannelVolumes, +}; +use libpulse_glib_binding::Mainloop; +use std::{ + cell::{Ref, RefCell}, + 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 DeviceInfo { + pub name: Option, + pub description: Option, + pub volume: ChannelVolumes, + pub index: u32, +} + +pub struct ServerInfo { + pub default_sink_name: Option, + pub default_source_name: Option, +} + +struct PAInner { + main_loop: Mainloop, + 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(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.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.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()), + 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())); + } + 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.introspect() + .get_sink_info_by_name(&name, move |result| match result { + ListResult::Item(item) => { + sink = Some(DeviceInfo { + 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(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(&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(); + let mut sender = Some(sender); + let mut items = Some(Vec::new()); + 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()), + 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())); + } + 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.introspect() + .get_source_info_by_name(&name, move |result| match result { + ListResult::Item(item) => { + source = Some(DeviceInfo { + 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(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..0cc163f1 100644 --- a/applets/cosmic-applet-audio/src/volume.rs +++ b/applets/cosmic-applet-audio/src/volume.rs @@ -1,6 +1,7 @@ use gtk4::{prelude::*, Scale}; use libpulse_binding::volume::Volume; -use pulsectl::controllers::types::DeviceInfo; + +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.); 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