diff --git a/src/config.rs b/src/config.rs index f1a2d32..2b1004e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -104,6 +104,19 @@ pub enum MprisAction { Volume, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum PulseAction { + SinkVolume, + SinkMute, + SinkInputVolume, + SinkInputMute, + SourceVolume, + SourceMute, + SourceOutputVolume, + SourceOutputMute, + DefaultSink, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type")] pub enum KeyMapVariant { @@ -113,6 +126,7 @@ pub enum KeyMapVariant { }, PulseAudio { ids: Vec, + action: PulseAction, }, Exec { command: String, diff --git a/src/controller.rs b/src/controller.rs index e2b9379..694359e 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -1,25 +1,28 @@ use crate::config::NanoKeys; -use crate::config::{Config, KeyMapVariant, MprisAction}; +use crate::config::{Config, KeyMapVariant, MprisAction, PulseAction}; use crate::midi_client::KeyEvent; use crate::mpris_client; -use crossbeam_channel::Receiver; +use crate::pulse_client::{self, PulseMessage}; +use crossbeam_channel::{unbounded, Receiver, Sender}; use log::{error, info, warn}; use std::process::{Command, Output}; use std::sync::Arc; use std::thread; pub fn run(in_channel: Receiver, config: Arc) { + let (sender, receiver) = unbounded::(); + pulse_client::run(receiver); thread::spawn(move || { - exec(in_channel, config); + exec(in_channel, sender, config); }); } -fn exec(in_channel: Receiver, config: Arc) { +fn exec(in_channel: Receiver, pulse_channel: Sender, config: Arc) { loop { let event_in = in_channel.recv(); match event_in { Ok((key, state)) => { - eval(key, state, &config); + eval(key, state, &config, &pulse_channel); } Err(err) => { error!("Failed receiving event in controller thread: {err}"); @@ -41,7 +44,7 @@ where } } -fn eval(key: NanoKeys, state: u8, config: &Arc) { +fn eval(key: NanoKeys, state: u8, config: &Arc, pulse_channel: &Sender) { match config.keymap.get(&key) { Some(actions) => { info!( @@ -51,7 +54,10 @@ fn eval(key: NanoKeys, state: u8, config: &Arc) { for action in actions { match action { KeyMapVariant::Mpris { ids, action } => parse_mpris_action(state, ids, action), - KeyMapVariant::PulseAudio { ids } => {} + KeyMapVariant::PulseAudio { ids, action } => { + //TODO: maybe clean up? + let _ = pulse_channel.send((Arc::new(ids.to_vec()), action.clone(), state)); + } KeyMapVariant::Exec { command, args } => parse_exec_action(command, args), } } diff --git a/src/pulse_client.rs b/src/pulse_client.rs index 42b1a3f..b08ea6f 100644 --- a/src/pulse_client.rs +++ b/src/pulse_client.rs @@ -1,5 +1,8 @@ extern crate libpulse_binding as pulse; +use crate::config::{Config, KeyMapVariant, PulseAction}; +use crossbeam_channel::Receiver; +use log::{error, info, warn}; use pulse::context::{Context, FlagSet as ContextFlagSet}; use pulse::mainloop::threaded::Mainloop; use pulse::proplist::Proplist; @@ -8,9 +11,16 @@ use regex::Regex; use std::cell::RefCell; use std::ops::Deref; use std::sync::Arc; -use std::time; +use std::thread; -pub fn run() { +pub type PulseMessage = (Arc>, PulseAction, u8); + +pub fn run(in_channel: Receiver) { + thread::spawn(move || { + exec(in_channel); + }); +} +pub fn exec(in_channel: Receiver) { let mut proplist = Proplist::new().unwrap(); proplist .set_str( @@ -82,16 +92,38 @@ pub fn run() { context.borrow_mut().set_state_callback(None); mainloop.borrow_mut().unlock(); - //--------------------- - let ml = Arc::clone(&mainloop); - let ctx = Arc::clone(&context); - - set_volume_sink_input(ml, ctx, "Firefox", 100); - //--------------------- - loop { - //TODO: react to incoming commands here instead of infinite sleep - std::thread::sleep(time::Duration::from_secs(u64::MAX)); + let ml = Arc::clone(&mainloop); + let ctx = Arc::clone(&context); + let event_in = in_channel.recv(); + match event_in { + Ok((ids, action, state)) => { + parse_pulse_action(state, ids, &action, ml, ctx); + } + Err(err) => { + error!("Failed receiving event in controller thread: {err}"); + } + } + } +} + +fn parse_pulse_action( + state: u8, + ids: Arc>, + action: &PulseAction, + mainloop: Arc>, + context: Arc>, +) { + match action { + PulseAction::SinkInputVolume => { + for id in ids.iter() { + set_volume_sink_input(mainloop.clone(), context.clone(), id, state); + } + } + PulseAction::DefaultSink => { + set_default_sink(mainloop.clone(), context.clone(), ids.get(0).unwrap()); + } + _ => {} } } @@ -140,7 +172,6 @@ fn for_all_sink_inputs( .unwrap(); let matches_regex = sink_filter.is_match(&application_name); - println!("{:?}", application_name); if matches_regex { function( callback_context.clone(), @@ -176,7 +207,7 @@ pub fn set_volume_sink_input( mainloop: Arc>, context: Arc>, name: &str, - volume: i16, + volume: u8, ) { for_all_sink_inputs( mainloop, @@ -197,12 +228,10 @@ pub fn set_volume_sink_input( } //taken from https://github.com/jantap/rsmixer -pub fn percent_to_volume(target_percent: i16) -> u32 { +pub fn percent_to_volume(target_percent: u8) -> u32 { let base_delta = (volume::Volume::NORMAL.0 as f32 - volume::Volume::MUTED.0 as f32) / 100.0; - if target_percent < 0 { - volume::Volume::MUTED.0 - } else if target_percent == 100 { + if target_percent == 100 { volume::Volume::NORMAL.0 } else if target_percent >= 150 { (volume::Volume::NORMAL.0 as f32 * 1.5) as u32