Compare commits

...

2 commits

Author SHA1 Message Date
d2a7e36f5e
Add cli options 2024-12-20 22:20:57 +01:00
99d764eeaf
Add common server interface 2024-12-20 21:39:46 +01:00
8 changed files with 267 additions and 144 deletions

61
Cargo.lock generated
View file

@ -49,9 +49,9 @@ dependencies = [
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.6" version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]] [[package]]
name = "anstyle-parse" name = "anstyle-parse"
@ -111,6 +111,46 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim 0.11.1",
]
[[package]]
name = "clap_derive"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote 1.0.35",
"syn 2.0.52",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.0" version = "1.0.0"
@ -192,7 +232,7 @@ dependencies = [
"ident_case", "ident_case",
"proc-macro2", "proc-macro2",
"quote 1.0.35", "quote 1.0.35",
"strsim", "strsim 0.10.0",
"syn 1.0.109", "syn 1.0.109",
] ]
@ -224,7 +264,7 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0ac8859845146979953797f03cc5b282fb4396891807cdb3d04929a88418197" checksum = "d0ac8859845146979953797f03cc5b282fb4396891807cdb3d04929a88418197"
dependencies = [ dependencies = [
"heck", "heck 0.3.3",
"quote 0.3.15", "quote 0.3.15",
"syn 0.11.11", "syn 0.11.11",
] ]
@ -349,6 +389,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "humantime" name = "humantime"
version = "2.1.0" version = "2.1.0"
@ -543,6 +589,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
name = "picokontroller" name = "picokontroller"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap",
"crossbeam-channel", "crossbeam-channel",
"directories", "directories",
"env_logger", "env_logger",
@ -684,6 +731,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "0.11.11" version = "0.11.11"

View file

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.5.23", features = ["derive"] }
crossbeam-channel = "0.5.8" crossbeam-channel = "0.5.8"
directories = "5.0.1" directories = "5.0.1"
env_logger = "0.11.3" env_logger = "0.11.3"

View file

@ -1,8 +1,9 @@
use crate::config::NanoKeys; use crate::config::NanoKeys;
use crate::config::{Config, KeyMapVariant, MprisAction, PulseAction}; use crate::config::{Config, KeyMapVariant, MprisAction};
use crate::interaction_server::InteractionServer;
use crate::midi_client::KeyEvent; use crate::midi_client::KeyEvent;
use crate::mpris_client; use crate::mpris_client;
use crate::pulse_client::{self, PulseMessage}; use crate::pulse_controller::{PulseController, PulseMessage};
use crossbeam_channel::{unbounded, Receiver, Sender}; use crossbeam_channel::{unbounded, Receiver, Sender};
use log::{error, info, warn}; use log::{error, info, warn};
use std::process::{Command, Output}; use std::process::{Command, Output};
@ -11,7 +12,8 @@ use std::thread;
pub fn run(in_channel: Receiver<KeyEvent>, config: Arc<Config>) { pub fn run(in_channel: Receiver<KeyEvent>, config: Arc<Config>) {
let (sender, receiver) = unbounded::<PulseMessage>(); let (sender, receiver) = unbounded::<PulseMessage>();
pulse_client::run(receiver); let pulse = PulseController::new(receiver);
pulse.start();
thread::spawn(move || { thread::spawn(move || {
exec(in_channel, sender, config); exec(in_channel, sender, config);
}); });

View file

@ -0,0 +1,5 @@
use std::thread::JoinHandle;
pub trait InteractionServer {
fn start(self) -> JoinHandle<()>;
}

View file

@ -1,26 +1,52 @@
mod config; mod config;
mod controller; mod controller;
mod interaction_server;
mod midi_client; mod midi_client;
mod mpris_client; mod mpris_client;
mod pulse_client; mod pulse_controller;
use clap::Parser;
use crossbeam_channel::unbounded; use crossbeam_channel::unbounded;
use directories::ProjectDirs; use directories::ProjectDirs;
use std::{path::PathBuf, sync::Arc}; use interaction_server::InteractionServer;
use midi_client::MidiClient;
use std::{path::PathBuf, process::exit, sync::Arc};
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[arg(long, help = "List all available MIDI devices.")]
list_midi: bool,
#[arg(
long,
help = "List all available MPRIS players."
)]
list_mpris: bool,
}
fn main() { fn main() {
env_logger::init(); env_logger::init();
let cli = Cli::parse();
// Check if the debug flag was provided
if cli.list_midi {
midi_client::list_ports();
exit(0);
} else if cli.list_mpris {
mpris_client::list_players();
exit(0);
}
let proj_dirs = ProjectDirs::from("com", "ghoscht", "picoKontroller"); let proj_dirs = ProjectDirs::from("com", "ghoscht", "picoKontroller");
let path: PathBuf = proj_dirs.unwrap().config_dir().join("config.toml"); let path: PathBuf = proj_dirs.unwrap().config_dir().join("config.toml");
// let path: &str = "./config.toml";
let cfg: config::Config = config::init(path); let cfg: config::Config = config::init(path);
let arc_cfg = Arc::new(cfg); let arc_cfg = Arc::new(cfg);
midi_client::list_ports();
println!("\nMPRIS Clients:");
mpris_client::list_clients();
let (sender, receiver) = unbounded::<midi_client::KeyEvent>(); let (sender, receiver) = unbounded::<midi_client::KeyEvent>();
let midi = MidiClient::new(arc_cfg.general.midi_device.clone(), sender);
controller::run(receiver, Arc::clone(&arc_cfg)); controller::run(receiver, Arc::clone(&arc_cfg));
midi_client::run(&arc_cfg.general.midi_device, sender).unwrap();
let server = midi.start();
let _ = server.join();
} }

View file

@ -1,47 +1,72 @@
use crate::config::NanoKeys; use crate::{config::NanoKeys, interaction_server::InteractionServer};
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use midir::{Ignore, MidiInput}; use midir::{Ignore, MidiInput};
use regex::Regex; use regex::Regex;
use std::error::Error; use std::thread::{self, JoinHandle};
use std::time; use std::time;
const CLIENT_NAME: &str = "PicoKontroller";
const PORT_NAME: &str = "PicoController Input";
pub struct MidiClient {
pub device_name: String,
pub out_channel: Sender<KeyEvent>,
}
pub type KeyEvent = (NanoKeys, u8);
impl InteractionServer for MidiClient {
fn start(self) -> JoinHandle<()> {
thread::spawn(move || {
self.exec();
})
}
}
impl MidiClient {
pub fn new(device_name: String, out_channel: Sender<KeyEvent>) -> MidiClient {
MidiClient {
device_name,
out_channel,
}
}
fn exec(self) {
let port_filter = Regex::new(&self.device_name).expect("Creating RegEx failed");
let mut midi_in = MidiInput::new(CLIENT_NAME).expect("Creating MIDI device failed");
midi_in.ignore(Ignore::None);
let in_ports = midi_in.ports();
let in_port = in_ports
.iter()
.find(|p| port_filter.is_match(&midi_in.port_name(p).unwrap()))
.expect("Couldn't find a port matching the supplied RegEx");
// _conn_in needs to be a named parameter, because it needs to be kept alive until the end of the scope
let _conn_in = midi_in.connect(
in_port,
PORT_NAME,
move |_, message, _| {
let key = NanoKeys::try_from(message[1]).unwrap();
let state = message[2];
let _ = self.out_channel.send((key, state));
},
(),
);
loop {
// sleep forever, callback functions happen on separate thread
// couldn't reuse this thread, since _conn_in needs to be alive in this scope
std::thread::sleep(time::Duration::from_secs(u64::MAX));
}
}
}
pub fn list_ports() -> () { pub fn list_ports() -> () {
let midi_in = MidiInput::new("PicoKontroller").expect("Creating Midi device failed"); let midi_in = MidiInput::new(CLIENT_NAME).expect("Creating MIDI device failed");
let in_ports = midi_in.ports(); let in_ports = midi_in.ports();
println!("All available midi devices:"); println!("All available MIDI devices:");
for (_, p) in in_ports.iter().enumerate() { for (_, p) in in_ports.iter().enumerate() {
println!("{}", midi_in.port_name(p).unwrap()); println!("{}", midi_in.port_name(p).unwrap());
} }
} }
pub fn run(port_name: &str, out_channel: Sender<KeyEvent>) -> Result<(), Box<dyn Error>> {
let port_filter = Regex::new(port_name).expect("Creating RegEx failed");
let mut midi_in = MidiInput::new("PicoKontroller").expect("Creating Midi device failed");
midi_in.ignore(Ignore::None);
let in_ports = midi_in.ports();
let in_port = in_ports
.iter()
.find(|p| port_filter.is_match(&midi_in.port_name(p).unwrap()))
.expect("Couldn't find a port matching the supplied RegEx");
// _conn_in needs to be a named parameter, because it needs to be kept alive until the end of the scope
let _conn_in = midi_in.connect(
in_port,
"PicoController Input",
move |_, message, _| {
let key = NanoKeys::try_from(message[1]).unwrap();
let state = message[2];
let _ = out_channel.send((key, state));
},
(),
)?;
loop {
// sleep forever, callback functions happen on separate thread
// couldn't reuse this thread, since _conn_in needs to be alive in this scope
std::thread::sleep(time::Duration::from_secs(u64::MAX));
}
}
pub type KeyEvent = (NanoKeys, u8);

View file

@ -1,17 +1,19 @@
use itertools::Itertools;
use log::warn; use log::warn;
use mpris::Player; use mpris::Player;
use mpris::PlayerFinder; use mpris::PlayerFinder;
use regex::Regex; use regex::Regex;
pub fn list_clients() -> () { pub fn list_players() -> () {
println!("All available MPRIS players:");
PlayerFinder::new() PlayerFinder::new()
.expect("Could not connect to D-Bus") .expect("Could not connect to D-Bus")
.find_all() .find_all()
.unwrap_or_else(|_| Vec::new()) .unwrap_or_else(|_| Vec::new())
.into_iter() .iter()
.for_each(|player| { .map(|p| p.identity().to_owned())
println!("{}", player.identity()); .dedup()
}); .for_each(|i| {println!("{}",i)});
} }
fn for_all_players<F>(player_id: &str, function: F) fn for_all_players<F>(player_id: &str, function: F)

View file

@ -1,8 +1,9 @@
extern crate libpulse_binding as pulse; extern crate libpulse_binding as pulse;
use crate::config::{Config, KeyMapVariant, PulseAction}; use crate::config::PulseAction;
use crate::interaction_server::InteractionServer;
use crossbeam_channel::Receiver; use crossbeam_channel::Receiver;
use log::{error, info, warn}; use log::error;
use pulse::context::{Context, FlagSet as ContextFlagSet}; use pulse::context::{Context, FlagSet as ContextFlagSet};
use pulse::mainloop::threaded::Mainloop; use pulse::mainloop::threaded::Mainloop;
use pulse::proplist::Proplist; use pulse::proplist::Proplist;
@ -11,97 +12,109 @@ use regex::Regex;
use std::cell::RefCell; use std::cell::RefCell;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
use std::thread; 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<String>>, PulseAction, u8);
pub fn run(in_channel: Receiver<PulseMessage>) { pub struct PulseController {
thread::spawn(move || { pub in_channel: Receiver<PulseMessage>,
exec(in_channel);
});
} }
pub fn exec(in_channel: Receiver<PulseMessage>) {
let mut proplist = Proplist::new().unwrap();
proplist
.set_str(
pulse::proplist::properties::APPLICATION_NAME,
"picoKontroller",
)
.unwrap();
let mainloop = Arc::new(RefCell::new( impl InteractionServer for PulseController {
Mainloop::new().expect("Failed to create mainloop"), fn start(self) -> JoinHandle<()> {
)); thread::spawn(move || {
self.exec();
})
}
}
let context = Arc::new(RefCell::new( impl PulseController {
Context::new_with_proplist( pub fn new(in_channel: Receiver<PulseMessage>) -> PulseController {
mainloop.borrow().deref(), PulseController { in_channel }
"picoKontrollerContext", }
&proplist,
) fn exec(&self) {
.expect("Failed to create new context"), let mut proplist = Proplist::new().unwrap();
)); proplist
.set_str(
pulse::proplist::properties::APPLICATION_NAME,
APPLICATION_NAME,
)
.unwrap();
let mainloop = Arc::new(RefCell::new(
Mainloop::new().expect("Failed to create mainloop"),
));
let context = Arc::new(RefCell::new(
Context::new_with_proplist(mainloop.borrow().deref(), CONTEXT_NAME, &proplist)
.expect("Failed to create new context"),
));
// Context state change callback
{
let ml_ref = Arc::clone(&mainloop);
let context_ref = Arc::clone(&context);
context
.borrow_mut()
.set_state_callback(Some(Box::new(move || {
let state = unsafe { (*context_ref.as_ptr()).get_state() };
match state {
pulse::context::State::Ready
| pulse::context::State::Failed
| pulse::context::State::Terminated => unsafe {
(*ml_ref.as_ptr()).signal(false);
},
_ => {}
}
})));
}
// Context state change callback
{
let ml_ref = Arc::clone(&mainloop);
let context_ref = Arc::clone(&context);
context context
.borrow_mut() .borrow_mut()
.set_state_callback(Some(Box::new(move || { .connect(None, ContextFlagSet::NOFLAGS, None)
let state = unsafe { (*context_ref.as_ptr()).get_state() }; .expect("Failed to connect context");
match state {
pulse::context::State::Ready mainloop.borrow_mut().lock();
| pulse::context::State::Failed mainloop
| pulse::context::State::Terminated => unsafe { .borrow_mut()
(*ml_ref.as_ptr()).signal(false); .start()
}, .expect("Failed to start mainloop");
_ => {}
// Wait for context to be ready
loop {
match context.borrow().get_state() {
pulse::context::State::Ready => {
break;
}
pulse::context::State::Failed | pulse::context::State::Terminated => {
eprintln!("Context state failed/terminated, quitting...");
mainloop.borrow_mut().unlock();
mainloop.borrow_mut().stop();
return;
}
_ => {
mainloop.borrow_mut().wait();
} }
})));
}
context
.borrow_mut()
.connect(None, ContextFlagSet::NOFLAGS, None)
.expect("Failed to connect context");
mainloop.borrow_mut().lock();
mainloop
.borrow_mut()
.start()
.expect("Failed to start mainloop");
// Wait for context to be ready
loop {
match context.borrow().get_state() {
pulse::context::State::Ready => {
break;
}
pulse::context::State::Failed | pulse::context::State::Terminated => {
eprintln!("Context state failed/terminated, quitting...");
mainloop.borrow_mut().unlock();
mainloop.borrow_mut().stop();
return;
}
_ => {
mainloop.borrow_mut().wait();
} }
} }
} context.borrow_mut().set_state_callback(None);
context.borrow_mut().set_state_callback(None); mainloop.borrow_mut().unlock();
mainloop.borrow_mut().unlock();
loop { loop {
let ml = Arc::clone(&mainloop); let ml = Arc::clone(&mainloop);
let ctx = Arc::clone(&context); let ctx = Arc::clone(&context);
let event_in = in_channel.recv(); let event_in = self.in_channel.recv();
match event_in { match event_in {
Ok((ids, action, state)) => { Ok((ids, action, state)) => {
parse_pulse_action(state, ids, &action, ml, ctx); parse_pulse_action(state, ids, &action, ml, ctx);
} }
Err(err) => { Err(err) => {
error!("Failed receiving event in controller thread: {err}"); error!("Failed receiving event in controller thread: {err}");
}
} }
} }
} }
@ -127,11 +140,7 @@ fn parse_pulse_action(
} }
} }
pub fn set_default_sink( fn set_default_sink(mainloop: Arc<RefCell<Mainloop>>, context: Arc<RefCell<Context>>, name: &str) {
mainloop: Arc<RefCell<Mainloop>>,
context: Arc<RefCell<Context>>,
name: &str,
) {
mainloop.borrow_mut().lock(); mainloop.borrow_mut().lock();
let sink_filter = Regex::new(name).expect("Creating RegEx failed"); let sink_filter = Regex::new(name).expect("Creating RegEx failed");
let callback_context = context.clone(); let callback_context = context.clone();
@ -185,7 +194,7 @@ fn for_all_sink_inputs(
mainloop.borrow_mut().unlock(); mainloop.borrow_mut().unlock();
} }
pub fn mute_sink_input( fn mute_sink_input(
mainloop: Arc<RefCell<Mainloop>>, mainloop: Arc<RefCell<Mainloop>>,
context: Arc<RefCell<Context>>, context: Arc<RefCell<Context>>,
name: &str, name: &str,
@ -203,7 +212,7 @@ pub fn mute_sink_input(
) )
} }
pub fn set_volume_sink_input( fn set_volume_sink_input(
mainloop: Arc<RefCell<Mainloop>>, mainloop: Arc<RefCell<Mainloop>>,
context: Arc<RefCell<Context>>, context: Arc<RefCell<Context>>,
name: &str, name: &str,
@ -228,7 +237,7 @@ pub fn set_volume_sink_input(
} }
//taken from https://github.com/jantap/rsmixer //taken from https://github.com/jantap/rsmixer
pub fn percent_to_volume(target_percent: u8) -> u32 { 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; let base_delta = (volume::Volume::NORMAL.0 as f32 - volume::Volume::MUTED.0 as f32) / 100.0;
if target_percent == 100 { if target_percent == 100 {