Switch to client/controller architecture
All checks were successful
Continuous integration / Build and test (push) Successful in 46s

This commit is contained in:
GHOSCHT 2024-12-21 14:15:12 +01:00
parent bc488b7565
commit db797623ea
Signed by: ghoscht
GPG key ID: 2C2C1C62A5388E82
9 changed files with 362 additions and 249 deletions

View file

@ -13,7 +13,8 @@ pub struct MidiClient {
pub out_channel: Sender<KeyEvent>,
}
pub type KeyEvent = (NanoKeys, u8);
pub type KeyEvent = (NanoKeys, KeyState);
pub type KeyState = u8;
impl InteractionServer for MidiClient {
fn start(self) -> JoinHandle<()> {

View file

@ -93,7 +93,7 @@ pub fn write_default(path: &PathBuf) -> () {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum MprisAction {
Play,
Pause,

View file

@ -1,128 +0,0 @@
use crate::config::NanoKeys;
use crate::config::{Config, KeyMapVariant, MprisAction};
use crate::interaction_server::InteractionServer;
use crate::midi_client::KeyEvent;
use crate::mpris_client;
use crate::pulse_controller::{PulseController, 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<KeyEvent>, config: Arc<Config>) {
let (sender, receiver) = unbounded::<PulseMessage>();
let pulse = PulseController::new(receiver);
pulse.start();
thread::spawn(move || {
exec(in_channel, sender, config);
});
}
fn exec(in_channel: Receiver<KeyEvent>, pulse_channel: Sender<PulseMessage>, config: Arc<Config>) {
loop {
let event_in = in_channel.recv();
match event_in {
Ok((key, state)) => {
eval(key, state, &config, &pulse_channel);
}
Err(err) => {
error!("Failed receiving event in controller thread: {err}");
}
}
}
}
fn execute_binary_functionality<F>(player_ids: &Option<Vec<String>>, state: u8, exec_closure: F)
where
F: Fn(Option<&str>) -> (),
{
if is_logical_true(state) {
return;
}
match player_ids {
Some(ids) => ids.iter().for_each(|id| exec_closure(Option::Some(id))),
None => exec_closure(Option::None),
}
}
fn eval(key: NanoKeys, state: u8, config: &Arc<Config>, pulse_channel: &Sender<PulseMessage>) {
match config.keymap.get(&key) {
Some(actions) => {
info!(
"Registered actions for [{:?} | {}]: {:?}",
key, state, actions
);
for action in actions {
match action {
KeyMapVariant::Mpris { ids, action } => parse_mpris_action(state, ids, action),
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),
}
}
}
None => {
warn!("Midi input {:?} not mapped", key);
}
}
}
fn parse_mpris_action(state: u8, ids: &Option<Vec<String>>, action: &MprisAction) {
match action {
MprisAction::Play => execute_binary_functionality(ids, state, |player_id: Option<&str>| {
mpris_client::play(player_id)
}),
MprisAction::Pause => {
execute_binary_functionality(ids, state, |player_id: Option<&str>| {
mpris_client::pause(player_id)
})
}
MprisAction::PlayPause => {
execute_binary_functionality(ids, state, |player_id: Option<&str>| {
mpris_client::play_pause(player_id)
})
}
MprisAction::Stop => execute_binary_functionality(ids, state, |player_id: Option<&str>| {
mpris_client::stop(player_id)
}),
MprisAction::Next => execute_binary_functionality(ids, state, |player_id: Option<&str>| {
mpris_client::next(player_id)
}),
MprisAction::Previous => {
execute_binary_functionality(ids, state, |player_id: Option<&str>| {
mpris_client::previous(player_id)
})
}
MprisAction::Volume => {
let volume = state as f64 / 100.0;
match ids {
Some(some_ids) => some_ids
.iter()
.for_each(|id| mpris_client::set_volume(Option::Some(id), volume)),
None => mpris_client::set_volume(Option::None, volume),
}
}
}
}
fn parse_exec_action(command: &str, args: &Option<Vec<String>>) {
let output: Result<Output, std::io::Error> = match args {
Some(some_args) => Command::new(command).args(some_args).output(),
None => Command::new(command).output(),
};
match output {
Ok(out) => {
info!("{:?}", out);
}
Err(err) => {
error!("{:?}", err);
}
}
}
fn is_logical_true(state: u8) -> bool {
return state == 127;
}

57
src/controllers/exec.rs Normal file
View file

@ -0,0 +1,57 @@
use crate::interaction_server::InteractionServer;
use crossbeam_channel::Receiver;
use std::process::{Command, Output};
use log::{error, info};
use std::{
sync::Arc,
thread::{self, JoinHandle},
};
pub type ExecMessage = (Arc<String>, Arc<Option<Vec<String>>>);
pub struct ExecController {
pub in_channel: Receiver<ExecMessage>,
}
impl InteractionServer for ExecController {
fn start(self) -> JoinHandle<()> {
thread::spawn(move || {
self.exec();
})
}
}
impl ExecController {
pub fn new(in_channel: Receiver<ExecMessage>) -> ExecController {
ExecController { in_channel }
}
fn exec(self) {
loop {
let event_in = self.in_channel.recv();
match event_in {
Ok((command, options)) => {
parse_exec_action(&*command, &*options);
}
Err(err) => {
error!("Failed receiving event in controller thread: {err}");
}
}
}
}
}
fn parse_exec_action(command: &str, args: &Option<Vec<String>>) {
let output: Result<Output, std::io::Error> = match args {
Some(some_args) => Command::new(command).args(some_args).output(),
None => Command::new(command).output(),
};
match output {
Ok(out) => {
info!("{:?}", out);
}
Err(err) => {
error!("{:?}", err);
}
}
}

197
src/controllers/mpris.rs Normal file
View file

@ -0,0 +1,197 @@
use crate::config::MprisAction;
use crossbeam_channel::Receiver;
use itertools::Itertools;
use log::{error, warn};
use mpris::Player;
use mpris::PlayerFinder;
use regex::Regex;
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use crate::interaction_server::InteractionServer;
pub type MprisMessage = (Arc<Option<Vec<MprisId>>>, MprisAction, MprisState);
pub type MprisState = u8;
pub type MprisId = String;
pub struct MprisController {
pub in_channel: Receiver<MprisMessage>,
}
impl InteractionServer for MprisController {
fn start(self) -> JoinHandle<()> {
thread::spawn(move || {
self.exec();
})
}
}
impl MprisController {
pub fn new(in_channel: Receiver<MprisMessage>) -> MprisController {
MprisController { in_channel }
}
fn exec(self) {
loop {
let event_in = self.in_channel.recv();
match event_in {
Ok((ids, action, state)) => {
parse_mpris_action(state, ids, &action);
}
Err(err) => {
error!("Failed receiving event in controller thread: {err}");
}
}
}
}
}
fn parse_mpris_action(state: MprisState, ids: Arc<Option<Vec<MprisId>>>, action: &MprisAction) {
match action {
MprisAction::Play => {
execute_binary_functionality(ids, state, |player_id: Option<&str>| play(player_id))
}
MprisAction::Pause => {
execute_binary_functionality(ids, state, |player_id: Option<&str>| pause(player_id))
}
MprisAction::PlayPause => {
execute_binary_functionality(ids, state, |player_id: Option<&str>| {
play_pause(player_id)
})
}
MprisAction::Stop => {
execute_binary_functionality(ids, state, |player_id: Option<&str>| stop(player_id))
}
MprisAction::Next => {
execute_binary_functionality(ids, state, |player_id: Option<&str>| next(player_id))
}
MprisAction::Previous => {
execute_binary_functionality(ids, state, |player_id: Option<&str>| previous(player_id))
}
MprisAction::Volume => {
let volume = state as f64 / 100.0;
match &*ids {
Some(some_ids) => some_ids
.iter()
.for_each(|id| set_volume(Option::Some(id), volume)),
None => set_volume(Option::None, volume),
}
}
}
}
fn execute_binary_functionality<F>(player_ids: Arc<Option<Vec<String>>>, state: u8, exec_closure: F)
where
F: Fn(Option<&str>) -> (),
{
if is_logical_true(state) {
return;
}
match &*player_ids {
Some(ids) => ids.iter().for_each(|id| exec_closure(Option::Some(id))),
None => exec_closure(Option::None),
}
}
fn is_logical_true(state: u8) -> bool {
return state == 127;
}
pub fn list_players() -> () {
println!("All available MPRIS players:");
PlayerFinder::new()
.expect("Could not connect to D-Bus")
.find_all()
.unwrap_or_else(|_| Vec::new())
.iter()
.map(|p| p.identity().to_owned())
.dedup()
.for_each(|i| println!("{}", i));
}
fn for_all_players<F>(player_id: &str, function: F)
where
F: Fn(&Player) -> (),
{
let player_filter = Regex::new(player_id).expect("Creating RegEx failed");
PlayerFinder::new()
.expect("Could not connect to D-Bus")
.find_all()
.unwrap_or_else(|_| Vec::new())
.into_iter()
.filter(|player| player_filter.is_match(player.identity()))
.into_iter()
.for_each(|player| {
function(&player);
});
}
fn for_active_player<F>(function: F)
where
F: Fn(&Player) -> (),
{
let player = PlayerFinder::new()
.expect("Could not connect to D-Bus")
.find_active();
match player {
Ok(p) => {
function(&p);
}
Err(err) => {
warn!("Could not find an active player: {err}");
}
}
}
fn execute_functionality<F>(player_id: Option<&str>, exec_closure: F)
where
F: Fn(&Player) -> (),
{
match player_id {
Some(id) => for_all_players(id, exec_closure),
None => for_active_player(exec_closure),
}
}
pub fn play(player_id: Option<&str>) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.play();
});
}
pub fn pause(player_id: Option<&str>) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.pause();
});
}
pub fn play_pause(player_id: Option<&str>) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.play_pause();
});
}
pub fn stop(player_id: Option<&str>) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.stop();
});
}
pub fn next(player_id: Option<&str>) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.next();
});
}
pub fn previous(player_id: Option<&str>) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.previous();
});
}
pub fn set_volume(player_id: Option<&str>, volume: f64) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.set_volume(volume);
});
}

View file

@ -17,7 +17,9 @@ use std::thread::{self, JoinHandle};
const APPLICATION_NAME: &str = "picoKontroller";
const CONTEXT_NAME: &str = "picoKontrollerContext";
pub type PulseMessage = (Arc<Vec<String>>, PulseAction, u8);
pub type PulseMessage = (Arc<Vec<PulseId>>, PulseAction, PulseState);
pub type PulseState = u8;
pub type PulseId = String;
pub struct PulseController {
pub in_channel: Receiver<PulseMessage>,
@ -121,8 +123,8 @@ impl PulseController {
}
fn parse_pulse_action(
state: u8,
ids: Arc<Vec<String>>,
state: PulseState,
ids: Arc<Vec<PulseId>>,
action: &PulseAction,
mainloop: Arc<RefCell<Mainloop>>,
context: Arc<RefCell<Context>>,

View file

@ -1,15 +1,22 @@
mod config;
mod controller;
mod router;
mod interaction_server;
mod midi_client;
mod mpris_client;
mod pulse_controller;
mod controllers {
pub mod mpris;
pub mod pulse;
pub mod exec;
}
mod clients {
pub mod midi;
}
use clap::Parser;
use crossbeam_channel::unbounded;
use directories::ProjectDirs;
use interaction_server::InteractionServer;
use midi_client::MidiClient;
use clients::midi::MidiClient;
use std::{path::PathBuf, process::exit, sync::Arc};
#[derive(Parser)]
@ -17,10 +24,7 @@ use std::{path::PathBuf, process::exit, sync::Arc};
struct Cli {
#[arg(long, help = "List all available MIDI devices.")]
list_midi: bool,
#[arg(
long,
help = "List all available MPRIS players."
)]
#[arg(long, help = "List all available MPRIS players.")]
list_mpris: bool,
}
@ -29,10 +33,10 @@ fn main() {
let cli = Cli::parse();
if cli.list_midi {
midi_client::list_ports();
clients::midi::list_ports();
exit(0);
} else if cli.list_mpris {
mpris_client::list_players();
controllers::mpris::list_players();
exit(0);
}
@ -41,10 +45,10 @@ fn main() {
let cfg: config::Config = config::init(path);
let arc_cfg = Arc::new(cfg);
let (sender, receiver) = unbounded::<midi_client::KeyEvent>();
let (sender, receiver) = unbounded::<clients::midi::KeyEvent>();
let midi = MidiClient::new(arc_cfg.general.midi_device.clone(), sender);
controller::run(receiver, Arc::clone(&arc_cfg));
router::run(receiver, Arc::clone(&arc_cfg));
let server = midi.start();
let _ = server.join();

View file

@ -1,103 +0,0 @@
use itertools::Itertools;
use log::warn;
use mpris::Player;
use mpris::PlayerFinder;
use regex::Regex;
pub fn list_players() -> () {
println!("All available MPRIS players:");
PlayerFinder::new()
.expect("Could not connect to D-Bus")
.find_all()
.unwrap_or_else(|_| Vec::new())
.iter()
.map(|p| p.identity().to_owned())
.dedup()
.for_each(|i| {println!("{}",i)});
}
fn for_all_players<F>(player_id: &str, function: F)
where
F: Fn(&Player) -> (),
{
let player_filter = Regex::new(player_id).expect("Creating RegEx failed");
PlayerFinder::new()
.expect("Could not connect to D-Bus")
.find_all()
.unwrap_or_else(|_| Vec::new())
.into_iter()
.filter(|player| player_filter.is_match(player.identity()))
.into_iter()
.for_each(|player| {
function(&player);
});
}
fn for_active_player<F>(function: F)
where
F: Fn(&Player) -> (),
{
let player = PlayerFinder::new()
.expect("Could not connect to D-Bus")
.find_active();
match player {
Ok(p) => {
function(&p);
}
Err(err) => {
warn!("Could not find an active player: {err}");
}
}
}
fn execute_functionality<F>(player_id: Option<&str>, exec_closure: F)
where
F: Fn(&Player) -> (),
{
match player_id {
Some(id) => for_all_players(id, exec_closure),
None => for_active_player(exec_closure),
}
}
pub fn play(player_id: Option<&str>) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.play();
});
}
pub fn pause(player_id: Option<&str>) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.pause();
});
}
pub fn play_pause(player_id: Option<&str>) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.play_pause();
});
}
pub fn stop(player_id: Option<&str>) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.stop();
});
}
pub fn next(player_id: Option<&str>) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.next();
});
}
pub fn previous(player_id: Option<&str>) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.previous();
});
}
pub fn set_volume(player_id: Option<&str>, volume: f64) -> () {
execute_functionality(player_id, |player: &Player| {
let _ = player.set_volume(volume);
});
}

83
src/router.rs Normal file
View file

@ -0,0 +1,83 @@
use crate::config::NanoKeys;
use crate::config::{Config, KeyMapVariant};
use crate::controllers::exec::{ExecController, ExecMessage};
use crate::interaction_server::InteractionServer;
use crate::clients::midi::KeyEvent;
use crate::controllers::mpris::{MprisController, MprisMessage};
use crate::controllers::pulse::{PulseController, PulseMessage};
use crossbeam_channel::{unbounded, Receiver, Sender};
use log::{error, info, warn};
use std::sync::Arc;
use std::thread;
pub fn run(in_channel: Receiver<KeyEvent>, config: Arc<Config>) {
let (pulse_sender, pulse_receiver) = unbounded::<PulseMessage>();
let (mpris_sender, mpris_receiver) = unbounded::<MprisMessage>();
let (exec_sender, exec_receiver) = unbounded::<ExecMessage>();
let pulse_interactor = PulseController::new(pulse_receiver);
let mpris_interactor = MprisController::new(mpris_receiver);
let exec_interactor = ExecController::new(exec_receiver);
pulse_interactor.start();
mpris_interactor.start();
exec_interactor.start();
thread::spawn(move || {
exec(in_channel, pulse_sender, mpris_sender, exec_sender, config);
});
}
fn exec(
in_channel: Receiver<KeyEvent>,
pulse_channel: Sender<PulseMessage>,
mpris_channel: Sender<MprisMessage>,
exec_channel: Sender<ExecMessage>,
config: Arc<Config>,
) {
loop {
let event_in = in_channel.recv();
match event_in {
Ok((key, state)) => {
eval(key, state, &config, &pulse_channel, &mpris_channel, &exec_channel);
}
Err(err) => {
error!("Failed receiving event in controller thread: {err}");
}
}
}
}
fn eval(
key: NanoKeys,
state: u8,
config: &Arc<Config>,
pulse_channel: &Sender<PulseMessage>,
mpris_channel: &Sender<MprisMessage>,
exec_channel: &Sender<ExecMessage>,
) {
match config.keymap.get(&key) {
Some(actions) => {
info!(
"Registered actions for [{:?} | {}]: {:?}",
key, state, actions
);
for action in actions {
match action {
KeyMapVariant::Mpris { ids, action } => {
let _ = mpris_channel.send((Arc::new(ids.to_owned()), action.clone(), state));
}
KeyMapVariant::PulseAudio { ids, action } => {
let _ = pulse_channel.send((Arc::new(ids.to_owned()), action.clone(), state));
}
KeyMapVariant::Exec { command, args } => {
let _ = exec_channel.send((Arc::new(command.clone()), Arc::new(args.clone())));
}
}
}
}
None => {
warn!("Midi input {:?} not mapped", key);
}
}
}