From 57b44a35d58796fd86be5d90bf9dd3b4171ad6e7 Mon Sep 17 00:00:00 2001 From: Lucy Date: Thu, 7 Apr 2022 12:36:01 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Continue=20rewrite=20of=20cosmic?= =?UTF-8?q?-applet-audio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- applets/cosmic-applet-audio/src/input.rs | 52 ++++++++++++++++++ applets/cosmic-applet-audio/src/main.rs | 66 +++++++++++++++++++++-- applets/cosmic-applet-audio/src/output.rs | 52 ++++++++++++++++++ applets/cosmic-applet-audio/src/volume.rs | 7 +++ 4 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 applets/cosmic-applet-audio/src/input.rs create mode 100644 applets/cosmic-applet-audio/src/output.rs create mode 100644 applets/cosmic-applet-audio/src/volume.rs diff --git a/applets/cosmic-applet-audio/src/input.rs b/applets/cosmic-applet-audio/src/input.rs new file mode 100644 index 00000000..4bef94e0 --- /dev/null +++ b/applets/cosmic-applet-audio/src/input.rs @@ -0,0 +1,52 @@ +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() + .expect("failed to list input devices") +} + +pub fn refresh_default_input(label: &Label) { + let default_input = SourceController::create() + .expect("failed to create input controller") + .get_default_device() + .expect("failed to get default input"); + label.set_text(match &default_input.description { + Some(name) => name.as_str(), + None => "Input Device", + }); +} + +pub fn refresh_input_widgets(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()); + let name = match &input.name { + Some(name) => name.to_owned(), + None => continue, // Why doesn't this have a name? Whatever, it's invalid. + }; + view! { + item = LabeledItem { + set_title: input.description + .as_ref() + .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"); + } + } + } + } + inputs.container_add(&item); + } +} diff --git a/applets/cosmic-applet-audio/src/main.rs b/applets/cosmic-applet-audio/src/main.rs index 3e72b17f..95e21f57 100644 --- a/applets/cosmic-applet-audio/src/main.rs +++ b/applets/cosmic-applet-audio/src/main.rs @@ -4,14 +4,22 @@ extern crate relm4_macros; mod icons; +mod input; +mod output; mod pa; mod task; +mod volume; use gtk4::{ - glib::{self, clone}, + gio::ApplicationFlags, + glib::{self, clone, MainContext, PRIORITY_DEFAULT}, prelude::*, - Align, Box as GtkBox, Button, Image, Label, ListBox, Orientation, PositionType, Revealer, - RevealerTransitionType, Scale, SelectionMode, Separator, Window, + Align, Application, ApplicationWindow, Box as GtkBox, Button, Image, Label, ListBox, + Orientation, PositionType, Revealer, RevealerTransitionType, Scale, SelectionMode, Separator, +}; +use libpulse_binding::{ + context::subscribe::{Facility, InterestMaskSet, Operation}, + volume::Volume, }; use once_cell::sync::Lazy; use pulsectl::Handler; @@ -20,13 +28,46 @@ use tokio::runtime::Runtime; static RT: Lazy = Lazy::new(|| Runtime::new().expect("failed to build tokio runtime")); fn main() { + let application = Application::new( + Some("com.system76.cosmic.applets.audio"), + ApplicationFlags::default(), + ); + application.connect_activate(app); + application.run(); +} + +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); + handler + .context + .borrow_mut() + .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"); + } + Some(Facility::Source) => { + refresh_input_tx.send(()).expect("failed to send output refresh message"); + } + _ => {} + } + })))); + handler + .context + .borrow_mut() + .subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE, |_| {}); view! { - window = Window { + window = ApplicationWindow { + set_application: Some(application), set_title: Some("COSMIC Network Applet"), set_default_width: 400, set_default_height: 300, @@ -116,4 +157,21 @@ fn main() { } } } + refresh_input_rx.attach( + None, + clone!(@weak inputs, @weak current_input => @default-return Continue(true), move |_| { + input::refresh_input_widgets(&inputs); + input::refresh_default_input(¤t_input); + Continue(true) + }), + ); + refresh_output_rx.attach( + None, + clone!(@weak outputs, @weak current_output => @default-return Continue(true), move |_| { + output::refresh_output_widgets(&outputs); + output::refresh_default_input(¤t_output); + Continue(true) + }), + ); + window.show(); } diff --git a/applets/cosmic-applet-audio/src/output.rs b/applets/cosmic-applet-audio/src/output.rs new file mode 100644 index 00000000..458e005b --- /dev/null +++ b/applets/cosmic-applet-audio/src/output.rs @@ -0,0 +1,52 @@ +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() + .expect("failed to list output devices") +} + +pub fn refresh_default_input(label: &Label) { + let default_output = SinkController::create() + .expect("failed to create output controller") + .get_default_device() + .expect("failed to get default output"); + label.set_text(match &default_output.description { + Some(name) => name.as_str(), + None => "Output Device", + }); +} + +pub fn refresh_output_widgets(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()); + let name = match &output.name { + Some(name) => name.to_owned(), + None => continue, // Why doesn't this have a name? Whatever, it's invalid. + }; + view! { + item = LabeledItem { + set_title: output.description + .as_ref() + .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"); + } + } + } + } + outputs.container_add(&item); + } +} diff --git a/applets/cosmic-applet-audio/src/volume.rs b/applets/cosmic-applet-audio/src/volume.rs new file mode 100644 index 00000000..6ee94127 --- /dev/null +++ b/applets/cosmic-applet-audio/src/volume.rs @@ -0,0 +1,7 @@ +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.); +}