feat: previous / next instructions
This commit is contained in:
@@ -7,6 +7,8 @@ pub enum MixerInstruction {
|
|||||||
DecreaseCurrent,
|
DecreaseCurrent,
|
||||||
GetCurrent,
|
GetCurrent,
|
||||||
PlayPauseCurrent,
|
PlayPauseCurrent,
|
||||||
|
PlayNext,
|
||||||
|
PlayPrevious,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MixerInstruction {
|
impl MixerInstruction {
|
||||||
@@ -19,6 +21,8 @@ impl MixerInstruction {
|
|||||||
4 => Some(MixerInstruction::DecreaseCurrent),
|
4 => Some(MixerInstruction::DecreaseCurrent),
|
||||||
5 => Some(MixerInstruction::GetCurrent),
|
5 => Some(MixerInstruction::GetCurrent),
|
||||||
6 => Some(MixerInstruction::PlayPauseCurrent),
|
6 => Some(MixerInstruction::PlayPauseCurrent),
|
||||||
|
7 => Some(MixerInstruction::PlayNext),
|
||||||
|
8 => Some(MixerInstruction::PlayPrevious),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/main.rs
36
src/main.rs
@@ -2,16 +2,14 @@ mod instructions;
|
|||||||
pub mod mixer;
|
pub mod mixer;
|
||||||
pub mod pulseaudio;
|
pub mod pulseaudio;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
pub mod playerctl;
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use mixer::Mixer;
|
use mixer::Mixer;
|
||||||
use pulseaudio::PulseInstruction;
|
use pulseaudio::PulseInstruction;
|
||||||
use std::{process::Command, sync::mpsc::channel};
|
use std::sync::mpsc::channel;
|
||||||
|
|
||||||
const NOTIFY_SEND_REPLACE_ID: u32 = 1448531;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mainloop = pulse::mainloop::standard::Mainloop::new().expect("Error getting main loop");
|
let mainloop = pulse::mainloop::standard::Mainloop::new().expect("Error getting PulseAudio main loop");
|
||||||
|
|
||||||
let (pulse_ix_tx, pulse_ix_rx) = channel::<PulseInstruction>();
|
let (pulse_ix_tx, pulse_ix_rx) = channel::<PulseInstruction>();
|
||||||
|
|
||||||
@@ -19,31 +17,3 @@ fn main() {
|
|||||||
|
|
||||||
mixer.run(pulse_ix_rx);
|
mixer.run(pulse_ix_rx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_notification(message: &str) -> Result<()> {
|
|
||||||
Command::new("notify-send")
|
|
||||||
.args(vec![
|
|
||||||
"Mixrs",
|
|
||||||
message,
|
|
||||||
"-r",
|
|
||||||
&NOTIFY_SEND_REPLACE_ID.to_string(),
|
|
||||||
])
|
|
||||||
.env("DBUS_SESSION_BUS_ADDRESS", "unix:path=/run/user/1000/bus")
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn playerctl_toggle(target: &str) -> Result<()> {
|
|
||||||
let get_players = Command::new("playerctl").arg("-l").output()?;
|
|
||||||
let get_players_output = String::from_utf8(get_players.stdout)?;
|
|
||||||
let players: Vec<&str> = get_players_output.split("\n").collect();
|
|
||||||
|
|
||||||
match players.iter().find(|p| p.to_lowercase().contains(&target.to_lowercase())) {
|
|
||||||
Some(player) => {
|
|
||||||
Command::new("playerctl").args(vec!["-p", player, "play-pause"]).spawn()?;
|
|
||||||
},
|
|
||||||
None => {},
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
91
src/mixer.rs
91
src/mixer.rs
@@ -28,10 +28,9 @@ use pulse::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
instructions::MixerInstruction,
|
instructions::MixerInstruction,
|
||||||
playerctl_toggle,
|
playerctl::{playerctl_next, playerctl_play_pause, playerctl_previous},
|
||||||
pulseaudio::{PulseInstruction, SinkInputMixerData},
|
pulseaudio::{PulseInstruction, SinkInputMixerData},
|
||||||
send_notification,
|
utils::{get_sink_input_name, percentage_to_total_volume, send_notification, total_volume_to_percentage, volume_to_percentage},
|
||||||
utils::{get_sink_input_name, percentage_to_total_volume},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Mixer {
|
pub struct Mixer {
|
||||||
@@ -187,6 +186,8 @@ impl Mixer {
|
|||||||
MixerInstruction::DecreaseCurrent => self.decrease_volume_current(),
|
MixerInstruction::DecreaseCurrent => self.decrease_volume_current(),
|
||||||
MixerInstruction::GetCurrent => self.get_current(),
|
MixerInstruction::GetCurrent => self.get_current(),
|
||||||
MixerInstruction::PlayPauseCurrent => self.play_pause_current(),
|
MixerInstruction::PlayPauseCurrent => self.play_pause_current(),
|
||||||
|
MixerInstruction::PlayNext => self.play_next_current(),
|
||||||
|
MixerInstruction::PlayPrevious => self.play_previous_current(),
|
||||||
},
|
},
|
||||||
Err(_) => (),
|
Err(_) => (),
|
||||||
}
|
}
|
||||||
@@ -357,14 +358,16 @@ impl Mixer {
|
|||||||
|
|
||||||
drop(index_lock);
|
drop(index_lock);
|
||||||
|
|
||||||
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
|
let Some(sink_index) = self.sink_inputs.keys().nth(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
self.context
|
self.context
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.introspect()
|
.introspect()
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.set_sink_input_mute(
|
.set_sink_input_mute(
|
||||||
sink_index,
|
*sink_index,
|
||||||
!self.sink_inputs.get(&sink_index).unwrap().muted,
|
!self.sink_inputs.get(&sink_index).unwrap().muted,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
@@ -379,9 +382,12 @@ impl Mixer {
|
|||||||
|
|
||||||
drop(index_lock);
|
drop(index_lock);
|
||||||
|
|
||||||
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
|
let Some(sink_index) = self.sink_inputs.keys().nth(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let sink_input = self.sink_inputs.get(&sink_index).unwrap();
|
let sink_input = self.sink_inputs.get(&sink_index).unwrap();
|
||||||
|
let sink_name = sink_input.name.clone();
|
||||||
|
|
||||||
let mut volume = ChannelVolumes::default();
|
let mut volume = ChannelVolumes::default();
|
||||||
volume.set(
|
volume.set(
|
||||||
@@ -395,7 +401,11 @@ impl Mixer {
|
|||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.introspect()
|
.introspect()
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.set_sink_input_volume(sink_index, &volume, None);
|
.set_sink_input_volume(*sink_index, &volume, Some(Box::new(move |success| {
|
||||||
|
if success {
|
||||||
|
let _ = send_notification(&format!("{sink_name}: {}%", volume_to_percentage(volume)));
|
||||||
|
}
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decrease_volume_current(&mut self) {
|
pub fn decrease_volume_current(&mut self) {
|
||||||
@@ -407,9 +417,12 @@ impl Mixer {
|
|||||||
|
|
||||||
drop(index_lock);
|
drop(index_lock);
|
||||||
|
|
||||||
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
|
let Some(sink_index) = self.sink_inputs.keys().nth(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let sink_input = self.sink_inputs.get(&sink_index).unwrap();
|
let sink_input = self.sink_inputs.get(&sink_index).unwrap();
|
||||||
|
let sink_name = sink_input.name.clone();
|
||||||
|
|
||||||
let mut volume = ChannelVolumes::default();
|
let mut volume = ChannelVolumes::default();
|
||||||
volume.set(
|
volume.set(
|
||||||
@@ -423,7 +436,11 @@ impl Mixer {
|
|||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.introspect()
|
.introspect()
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.set_sink_input_volume(sink_index, &volume, None);
|
.set_sink_input_volume(*sink_index, &volume, Some(Box::new(move |success| {
|
||||||
|
if success {
|
||||||
|
let _ = send_notification(&format!("{sink_name}: {}%", volume_to_percentage(volume)));
|
||||||
|
}
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current(&self) {
|
pub fn get_current(&self) {
|
||||||
@@ -435,10 +452,12 @@ impl Mixer {
|
|||||||
|
|
||||||
drop(index_lock);
|
drop(index_lock);
|
||||||
|
|
||||||
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
|
let Some(sink_index) = self.sink_inputs.keys().nth(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let current_name = &self.sink_inputs.get(&sink_index).unwrap().name;
|
let current_name = &self.sink_inputs.get(&sink_index).unwrap().name;
|
||||||
let _ = send_notification(&format!("Selected: {current_name}"));
|
let _ = send_notification(&format!("{current_name}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play_pause_current(&self) {
|
pub fn play_pause_current(&self) {
|
||||||
@@ -450,13 +469,53 @@ impl Mixer {
|
|||||||
|
|
||||||
drop(index_lock);
|
drop(index_lock);
|
||||||
|
|
||||||
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
|
let Some(sink_index) = self.sink_inputs.keys().nth(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let current_name = &self.sink_inputs.get(&sink_index).unwrap().name;
|
let current_name = &self.sink_inputs.get(&sink_index).unwrap().name;
|
||||||
match playerctl_toggle(current_name) {
|
match playerctl_play_pause(current_name) {
|
||||||
Ok(_) => {
|
Ok(_) => (),
|
||||||
let _ = send_notification(&format!("Toggled {current_name}"));
|
Err(_) => (),
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play_next_current(&self) {
|
||||||
|
let index_lock = self.selected_index.lock().unwrap();
|
||||||
|
|
||||||
|
let Some(index) = *index_lock else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
drop(index_lock);
|
||||||
|
|
||||||
|
let Some(sink_index) = self.sink_inputs.keys().nth(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_name = &self.sink_inputs.get(&sink_index).unwrap().name;
|
||||||
|
match playerctl_next(current_name) {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(_) => (),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play_previous_current(&self) {
|
||||||
|
let index_lock = self.selected_index.lock().unwrap();
|
||||||
|
|
||||||
|
let Some(index) = *index_lock else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
drop(index_lock);
|
||||||
|
|
||||||
|
let Some(sink_index) = self.sink_inputs.keys().nth(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_name = &self.sink_inputs.get(&sink_index).unwrap().name;
|
||||||
|
match playerctl_previous(current_name) {
|
||||||
|
Ok(_) => (),
|
||||||
Err(_) => (),
|
Err(_) => (),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/playerctl.rs
Normal file
48
src/playerctl.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
|
fn get_playerctl_player(target: &str) -> Result<String> {
|
||||||
|
let get_players = Command::new("playerctl").arg("-l").output()?;
|
||||||
|
let get_players_output = String::from_utf8(get_players.stdout)?;
|
||||||
|
let players: Vec<&str> = get_players_output.split("\n").collect();
|
||||||
|
|
||||||
|
let Some(player) = players
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.to_lowercase().contains(&target.to_lowercase()))
|
||||||
|
else {
|
||||||
|
return Err(anyhow!("Error getting player '{target}'"));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(player.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn playerctl_play_pause(target: &str) -> Result<()> {
|
||||||
|
let player = get_playerctl_player(target)?;
|
||||||
|
|
||||||
|
Command::new("playerctl")
|
||||||
|
.args(vec!["-p", &player, "play-pause"])
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn playerctl_next(target: &str) -> Result<()> {
|
||||||
|
let player = get_playerctl_player(target)?;
|
||||||
|
|
||||||
|
Command::new("playerctl")
|
||||||
|
.args(vec!["-p", &player, "next"])
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn playerctl_previous(target: &str) -> Result<()> {
|
||||||
|
let player = get_playerctl_player(target)?;
|
||||||
|
|
||||||
|
Command::new("playerctl")
|
||||||
|
.args(vec!["-p", &player, "previous"])
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
47
src/utils.rs
47
src/utils.rs
@@ -1,6 +1,16 @@
|
|||||||
use anyhow::anyhow;
|
use std::process::Command;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
use pulse::{context::introspect::SinkInputInfo, volume};
|
use pulse::{context::introspect::SinkInputInfo, volume};
|
||||||
|
|
||||||
|
#[link(name = "c")]
|
||||||
|
extern "C" {
|
||||||
|
/// Gets the current user's ID
|
||||||
|
pub fn getuid() -> u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NOTIFY_SEND_REPLACE_ID: u32 = 1448531;
|
||||||
|
const NOTIFICATION_DURATION_MILLIS: u32 = 1000;
|
||||||
const FULL_VOLUME: u32 = 1 << 16;
|
const FULL_VOLUME: u32 = 1 << 16;
|
||||||
|
|
||||||
pub fn volume_to_percentage(volume: volume::ChannelVolumes) -> u8 {
|
pub fn volume_to_percentage(volume: volume::ChannelVolumes) -> u8 {
|
||||||
@@ -22,7 +32,38 @@ pub fn get_sink_input_name(sink_input: &SinkInputInfo) -> anyhow::Result<String>
|
|||||||
return Err(anyhow!("Invalid sink input name"));
|
return Err(anyhow!("Invalid sink input name"));
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(String::from_utf8(
|
Ok(capitalize_string(&String::from_utf8(
|
||||||
name_bytes[..name_bytes.len() - 1].to_vec(),
|
name_bytes[..name_bytes.len() - 1].to_vec(),
|
||||||
)?)
|
)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capitalize_string(s: &str) -> String {
|
||||||
|
let mut c = s.chars();
|
||||||
|
match c.next() {
|
||||||
|
None => String::new(),
|
||||||
|
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_notification(message: &str) -> Result<()> {
|
||||||
|
let user_id = unsafe { getuid() };
|
||||||
|
|
||||||
|
Command::new("notify-send")
|
||||||
|
.args(vec![
|
||||||
|
"Mixrs",
|
||||||
|
message,
|
||||||
|
"-t",
|
||||||
|
&NOTIFICATION_DURATION_MILLIS.to_string(),
|
||||||
|
"-r",
|
||||||
|
&NOTIFY_SEND_REPLACE_ID.to_string(),
|
||||||
|
"-i",
|
||||||
|
"/",
|
||||||
|
])
|
||||||
|
.env(
|
||||||
|
"DBUS_SESSION_BUS_ADDRESS",
|
||||||
|
format!("unix:path=/run/user/{user_id}/bus"),
|
||||||
|
)
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user