cosmic-applets/applets/cosmic-applet-audio/src/app.rs

412 lines
15 KiB
Rust
Raw Normal View History

use futures_util::StreamExt;
2022-03-07 17:37:40 -05:00
use libcosmic_widgets::LabeledItem;
2022-03-17 14:23:06 -04:00
use libpulse_binding::{
context::subscribe::{Facility, InterestMaskSet, Operation},
volume::Volume,
};
use pulsectl::{
controllers::{
types::{ApplicationInfo, DeviceInfo},
AppControl, DeviceControl, SinkController, SourceController,
},
2022-03-17 14:23:06 -04:00
Handler,
};
use relm4::{
component,
gtk::{
self,
glib::{self, clone},
prelude::*,
Box as GtkBox, Button, Image, Label, ListBox, Orientation, PositionType, Revealer,
RevealerTransitionType, Scale, Separator, Window,
},
view, ComponentParts, RelmContainerExt, Sender, SimpleComponent,
};
2022-03-11 15:01:43 -05:00
use std::rc::Rc;
use tracker::track;
2022-03-07 17:37:40 -05:00
2022-03-18 12:11:05 -04:00
pub enum AppInput {
Inputs,
Outputs,
InputVolume,
OutputVolume,
NowPlaying,
2022-03-17 14:23:06 -04:00
}
#[track]
2022-03-07 17:37:40 -05:00
pub struct App {
#[no_eq]
2022-03-07 17:37:40 -05:00
default_input: Option<DeviceInfo>,
#[no_eq]
2022-03-07 17:37:40 -05:00
inputs: Vec<DeviceInfo>,
#[no_eq]
2022-03-07 17:37:40 -05:00
default_output: Option<DeviceInfo>,
#[no_eq]
2022-03-07 17:37:40 -05:00
outputs: Vec<DeviceInfo>,
#[no_eq]
now_playing: Vec<ApplicationInfo>,
#[do_not_track]
2022-03-17 14:23:06 -04:00
handler: Handler,
2022-03-07 17:37:40 -05:00
}
2022-03-11 11:06:41 -05:00
impl Default for App {
fn default() -> Self {
let mut input_controller =
SourceController::create().expect("failed to create input controller");
let default_input = input_controller.get_default_device().ok();
let inputs = input_controller.list_devices().unwrap_or_default();
let mut output_controller =
SinkController::create().expect("failed to create output controller");
let default_output = output_controller.get_default_device().ok();
let outputs = output_controller.list_devices().unwrap_or_default();
let now_playing = output_controller.list_applications().unwrap_or_default();
2022-03-17 14:23:06 -04:00
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;
}
}));
2022-03-11 11:06:41 -05:00
Self {
default_input,
inputs,
default_output,
outputs,
now_playing,
2022-03-17 14:23:06 -04:00
handler,
tracker: 0,
2022-03-11 11:06:41 -05:00
}
}
}
2022-03-10 12:40:41 -05:00
impl App {
2022-03-17 14:23:06 -04:00
fn get_default_input_name(&self) -> &str {
2022-03-10 12:40:41 -05:00
match &self.default_input {
2022-03-11 11:06:41 -05:00
Some(input) => match &input.description {
2022-03-10 12:40:41 -05:00
Some(name) => name.as_str(),
None => "Input Device",
},
None => "No Input Device",
}
}
2022-03-17 14:23:06 -04:00
fn get_default_output_name(&self) -> &str {
2022-03-10 12:40:41 -05:00
match &self.default_output {
2022-03-11 11:06:41 -05:00
Some(output) => match &output.description {
2022-03-10 12:40:41 -05:00
Some(name) => name.as_str(),
None => "Output Device",
},
None => "No Output Device",
}
}
fn refresh_default_input(&mut self) {
2022-03-17 14:23:06 -04:00
let mut input_controller =
SourceController::create().expect("failed to create input controller");
self.default_input = match self.default_input.as_ref() {
Some(input) => match &input.name {
Some(name) => input_controller.get_device_by_name(name.as_str()).ok(),
None => input_controller.get_device_by_index(input.index).ok(),
},
None => return,
};
}
2022-03-07 17:37:40 -05:00
fn refresh_default_output(&mut self) {
2022-03-17 14:23:06 -04:00
let mut output_controller =
SinkController::create().expect("failed to create output controller");
self.default_output = match self.default_output.as_ref() {
Some(output) => match &output.name {
Some(name) => output_controller.get_device_by_name(name.as_str()).ok(),
None => output_controller.get_device_by_index(output.index).ok(),
},
None => return,
};
}
2022-03-18 12:11:05 -04:00
fn subscribe_for_updates(&self, input: &Sender<AppInput>) {
2022-03-17 14:23:06 -04:00
let mut context = self.handler.context.borrow_mut();
let input = input.clone();
context.set_subscribe_callback(Some(Box::new(move |facility, operation, _idx| {
if !matches!(operation, Some(Operation::Changed)) {
return;
2022-03-17 14:23:06 -04:00
}
match facility {
2022-03-23 13:36:47 -04:00
Some(Facility::Sink) => {
2022-03-18 12:11:05 -04:00
send!(input, AppInput::OutputVolume);
send!(input, AppInput::NowPlaying);
2022-03-17 14:23:06 -04:00
}
2022-03-23 13:36:47 -04:00
Some(Facility::Source) => {
2022-03-18 12:11:05 -04:00
send!(input, AppInput::InputVolume);
2022-03-17 14:23:06 -04:00
}
_ => {}
}
})));
2022-03-23 13:36:47 -04:00
context.subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE, |_| {});
2022-03-17 14:23:06 -04:00
}
fn refresh_input_list(&mut self) {
2022-03-14 13:57:01 -04:00
let mut input_controller =
SourceController::create().expect("failed to create input controller");
self.set_inputs(input_controller.list_devices().unwrap_or_default());
}
fn refresh_input_widgets(&self, widgets: &AppWidgets) {
2022-03-16 11:20:14 -04:00
while let Some(row) = widgets.inputs.row_at_index(0) {
2022-03-14 13:57:01 -04:00
widgets.inputs.remove(&row);
}
for input in self.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.
};
2022-03-14 13:57:01 -04:00
view! {
item = LabeledItem {
set_title: input.description
.as_ref()
.unwrap_or(&name),
2022-03-14 13:57:01 -04:00
set_child: set_current_input_device = &Button {
set_label: "Switch",
2022-03-17 12:35:30 -04:00
connect_clicked: move |_| {
SourceController::create()
.expect("failed to create input controller")
.set_default_device(&name)
.expect("failed to set default device");
2022-03-17 12:35:30 -04:00
}
2022-03-14 13:57:01 -04:00
}
}
}
widgets.inputs.container_add(&item);
}
}
fn refresh_output_list(&mut self) {
2022-03-11 15:01:43 -05:00
let mut output_controller =
SinkController::create().expect("failed to create output controller");
self.set_outputs(output_controller.list_devices().unwrap_or_default());
}
fn refresh_output_widgets(&self, widgets: &AppWidgets) {
2022-03-16 11:20:14 -04:00
while let Some(row) = widgets.outputs.row_at_index(0) {
2022-03-11 15:01:43 -05:00
widgets.outputs.remove(&row);
}
for output in self.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.
};
2022-03-11 15:01:43 -05:00
view! {
item = LabeledItem {
set_title: output.description
.as_ref()
.unwrap_or(&name),
2022-03-11 15:01:43 -05:00
set_child: set_current_output_device = &Button {
set_label: "Switch",
2022-03-17 12:35:30 -04:00
connect_clicked: move |_| {
SinkController::create()
.expect("failed to create output controller")
.set_default_device(&name)
.expect("failed to set default device");
2022-03-17 12:35:30 -04:00
}
2022-03-11 15:01:43 -05:00
}
}
}
2022-03-14 13:57:01 -04:00
widgets.outputs.container_add(&item);
2022-03-11 15:01:43 -05:00
}
}
fn refresh_now_playing(&mut self) {
let mut output_controller =
SinkController::create().expect("failed to create output controller");
self.set_now_playing(output_controller.list_applications().unwrap_or_default());
}
fn refresh_now_playing_widgets(&self, widgets: &AppWidgets) {
while let Some(row) = widgets.playing_apps.row_at_index(0) {
widgets.playing_apps.remove(&row);
}
for app in self.get_now_playing() {
let icon_name = app
.proplist
.get_str("application.icon_name")
.unwrap_or_default();
let name = app.name.clone().unwrap_or_default();
view! {
item = GtkBox {
set_orientation: Orientation::Horizontal,
append: icon = &Image {
set_icon_name: Some(&icon_name),
set_pixel_size: 24,
},
append: title = &Label {
set_label: &name,
}
}
}
widgets.playing_apps.container_add(&item);
}
}
2022-03-11 15:01:43 -05:00
}
2022-03-14 11:52:29 -04:00
#[component(pub)]
impl SimpleComponent for App {
type Widgets = AppWidgets;
2022-03-07 17:37:40 -05:00
type InitParams = ();
2022-03-18 12:11:05 -04:00
type Input = AppInput;
2022-03-14 11:52:29 -04:00
type Output = ();
2022-03-07 17:37:40 -05:00
2022-03-14 11:52:29 -04:00
view! {
Window {
set_title: Some("COSMIC Network Applet"),
set_default_width: 400,
set_default_height: 300,
2022-03-07 17:37:40 -05:00
2022-03-14 11:52:29 -04:00
&GtkBox {
2022-03-07 17:37:40 -05:00
set_orientation: Orientation::Vertical,
set_spacing: 24,
2022-03-14 11:52:29 -04:00
&GtkBox {
2022-03-07 17:37:40 -05:00
set_orientation: Orientation::Horizontal,
set_spacing: 16,
2022-03-14 11:52:29 -04:00
&Image {
2022-03-07 17:37:40 -05:00
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.) },
2022-03-07 17:37:40 -05:00
set_value_pos: PositionType::Right,
2022-03-11 11:06:41 -05:00
set_hexpand: true
2022-03-07 17:37:40 -05:00
}
},
2022-03-14 11:52:29 -04:00
&GtkBox {
2022-03-07 17:37:40 -05:00
set_orientation: Orientation::Horizontal,
set_spacing: 16,
2022-03-14 11:52:29 -04:00
&Image {
2022-03-07 17:37:40 -05:00
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.)
},
2022-03-07 17:37:40 -05:00
set_value_pos: PositionType::Right,
2022-03-11 11:06:41 -05:00
set_hexpand: true
2022-03-07 17:37:40 -05:00
}
},
2022-03-14 11:52:29 -04:00
&Separator {
2022-03-07 17:37:40 -05:00
set_orientation: Orientation::Horizontal,
},
2022-03-16 12:52:30 -04:00
&GtkBox {
set_orientation: Orientation::Vertical,
&Button {
2022-03-11 15:01:43 -05:00
set_child: current_output = Some(&Label) {
set_text: watch! { model.get_default_output_name() }
},
2022-03-16 12:52:30 -04:00
connect_clicked(input, outputs_revealer) => move |_| {
2022-03-18 12:11:05 -04:00
send!(input, AppInput::Outputs);
2022-03-16 12:52:30 -04:00
outputs_revealer.set_reveal_child(!outputs_revealer.reveals_child());
}
},
append: outputs_revealer = &Revealer {
set_transition_type: RevealerTransitionType::SlideDown,
set_child: outputs = Some(&ListBox) {
set_selection_mode: gtk::SelectionMode::None,
2022-03-16 12:52:30 -04:00
set_activate_on_single_click: true
2022-03-16 11:20:14 -04:00
}
2022-03-10 12:40:41 -05:00
}
},
2022-03-14 11:52:29 -04:00
&Separator {
2022-03-10 12:40:41 -05:00
set_orientation: Orientation::Horizontal,
},
2022-03-16 12:52:30 -04:00
&GtkBox {
set_orientation: Orientation::Vertical,
&Button {
2022-03-11 15:01:43 -05:00
set_child: current_input = Some(&Label) {
set_text: watch! { model.get_default_input_name() }
2022-03-16 11:20:14 -04:00
},
2022-03-16 12:52:30 -04:00
connect_clicked(input, inputs_revealer) => move |_| {
2022-03-18 12:11:05 -04:00
send!(input, AppInput::Inputs);
2022-03-16 12:52:30 -04:00
inputs_revealer.set_reveal_child(!inputs_revealer.reveals_child());
}
},
append: inputs_revealer = &Revealer {
set_transition_type: RevealerTransitionType::SlideDown,
set_child: inputs = Some(&ListBox) {
set_selection_mode: gtk::SelectionMode::None,
2022-03-16 12:52:30 -04:00
set_activate_on_single_click: true
2022-03-11 15:01:43 -05:00
}
}
},
&Separator {
set_orientation: Orientation::Horizontal,
},
append: playing_apps = &ListBox {
set_selection_mode: gtk::SelectionMode::None,
2022-03-07 17:37:40 -05:00
}
}
}
2022-03-14 11:52:29 -04:00
}
fn init_parts(
_init_params: Self::InitParams,
root: &Self::Root,
2022-03-16 11:20:14 -04:00
input: &Sender<Self::Input>,
2022-03-14 11:52:29 -04:00
_output: &Sender<Self::Output>,
) -> ComponentParts<Self> {
let model = App::default();
let widgets = view_output!();
2022-03-17 14:23:06 -04:00
model.subscribe_for_updates(input);
2022-03-14 11:52:29 -04:00
ComponentParts { model, widgets }
2022-03-07 17:37:40 -05:00
}
2022-03-14 13:57:01 -04:00
fn update(
&mut self,
msg: Self::Input,
_input: &Sender<Self::Input>,
_output: &Sender<Self::Output>,
2022-03-14 13:57:01 -04:00
) {
self.reset();
2022-03-14 13:57:01 -04:00
match msg {
2022-03-18 12:11:05 -04:00
AppInput::Outputs => {
self.refresh_output_list();
2022-03-14 13:57:01 -04:00
}
2022-03-18 12:11:05 -04:00
AppInput::Inputs => {
self.refresh_input_list();
2022-03-14 13:57:01 -04:00
}
2022-03-18 12:11:05 -04:00
AppInput::InputVolume => {
self.refresh_default_input();
2022-03-17 14:23:06 -04:00
}
2022-03-18 12:11:05 -04:00
AppInput::OutputVolume => {
self.refresh_default_output();
2022-03-17 14:23:06 -04:00
}
AppInput::NowPlaying => {
self.refresh_now_playing();
}
2022-03-14 13:57:01 -04:00
}
}
fn pre_view() {
if model.changed(App::outputs()) {
model.refresh_output_widgets(widgets);
2022-03-14 13:57:01 -04:00
}
if model.changed(App::inputs()) {
model.refresh_input_widgets(widgets);
2022-03-14 13:57:01 -04:00
}
if model.changed(App::now_playing()) {
model.refresh_now_playing_widgets(widgets);
}
2022-03-14 13:57:01 -04:00
}
2022-03-07 17:37:40 -05:00
}