feat: working version

This commit is contained in:
2024-06-28 23:55:33 +02:00
parent c2f0cd0492
commit 1f48737b2b
7 changed files with 1123 additions and 2 deletions

521
Cargo.lock generated
View File

@@ -2,6 +2,527 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "anyhow"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "autocfg"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "backtrace"
version = "0.3.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bytes"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
[[package]]
name = "cc"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "gimli"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "libc"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libpulse-binding"
version = "2.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff"
dependencies = [
"bitflags 1.3.2",
"libc",
"libpulse-sys",
"num-derive",
"num-traits",
"winapi",
]
[[package]]
name = "libpulse-sys"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b"
dependencies = [
"libc",
"num-derive",
"num-traits",
"pkg-config",
"winapi",
]
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "miniz_oxide"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"wasi",
"windows-sys 0.48.0",
]
[[package]]
name = "mixrs"
version = "0.1.0"
dependencies = [
"anyhow",
"libpulse-binding",
"tokio",
]
[[package]]
name = "num-derive"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "object"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434"
dependencies = [
"memchr",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets 0.52.5",
]
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pkg-config"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
dependencies = [
"bitflags 2.6.0",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "socket2"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.48.0",
]
[[package]]
name = "tokio-macros"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.5",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
"windows_aarch64_gnullvm 0.52.5",
"windows_aarch64_msvc 0.52.5",
"windows_i686_gnu 0.52.5",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.5",
"windows_x86_64_gnu 0.52.5",
"windows_x86_64_gnullvm 0.52.5",
"windows_x86_64_msvc 0.52.5",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"

View File

@@ -4,3 +4,6 @@ version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.86"
pulse = { version = "2.0", package = "libpulse-binding" }
tokio = { version = "1.38.0", features = ["full"] }

25
src/instructions.rs Normal file
View File

@@ -0,0 +1,25 @@
#[repr(u8)]
pub enum MixerInstruction {
SelectNext,
SelectPrevious,
ToggleMuteCurrent,
IncreaseCurrent,
DecreaseCurrent,
GetCurrent,
PlayPauseCurrent,
}
impl MixerInstruction {
pub fn from_u8(byte: u8) -> Self {
match byte {
0 => MixerInstruction::SelectNext,
1 => MixerInstruction::SelectPrevious,
2 => MixerInstruction::ToggleMuteCurrent,
3 => MixerInstruction::IncreaseCurrent,
4 => MixerInstruction::DecreaseCurrent,
5 => MixerInstruction::GetCurrent,
6 => MixerInstruction::PlayPauseCurrent,
_ => panic!("Could not parse '{byte}' to MixerInstruction"),
}
}
}

View File

@@ -1,3 +1,50 @@
fn main() {
println!("Hello, world!");
mod instructions;
pub mod mixer;
pub mod pulseaudio;
pub mod utils;
use anyhow::Result;
use mixer::Mixer;
use pulseaudio::PulseInstruction;
use std::{process::Command, sync::mpsc::channel};
const NOTIFY_SEND_REPLACE_ID: u32 = 1448531;
#[tokio::main]
async fn main() {
let mainloop = pulse::mainloop::standard::Mainloop::new().expect("Error getting main loop");
let (pulse_ix_tx, pulse_ix_rx) = channel::<PulseInstruction>();
let mut mixer = Mixer::new(mainloop, pulse_ix_tx);
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(())
}

465
src/mixer.rs Normal file
View File

@@ -0,0 +1,465 @@
use anyhow::Result;
use std::{
borrow::{Borrow, BorrowMut},
collections::HashMap,
fs,
io::Read,
os::unix::net::UnixListener,
path::Path,
sync::{
mpsc::{channel, Receiver, Sender},
Arc, Mutex,
},
thread,
time::Duration,
usize,
};
use pulse::{
callbacks::ListResult,
context::{
subscribe::{Facility, InterestMaskSet, Operation},
FlagSet,
},
mainloop::standard::{IterateResult, Mainloop},
volume::ChannelVolumes,
};
use crate::{
instructions::MixerInstruction,
playerctl_toggle,
pulseaudio::{PulseInstruction, SinkInputMixerData},
send_notification,
utils::{get_sink_input_name, percentage_to_total_volume},
};
pub struct Mixer {
sink_inputs: HashMap<u32, SinkInputMixerData>,
selected_index: Arc<Mutex<Option<usize>>>,
mainloop: Mainloop,
context: pulse::context::Context,
}
impl Mixer {
pub fn new(mut mainloop: Mainloop, pulse_ix_tx: Sender<PulseInstruction>) -> Self {
let mut context =
pulse::context::Context::new(&mainloop, "Mixrs").expect("Error creating pulse context");
context
.borrow_mut()
.connect(None, FlagSet::NOFLAGS, None)
.expect("Error connecting pulse context");
loop {
match mainloop.borrow_mut().iterate(false) {
IterateResult::Quit(_) | IterateResult::Err(_) => {
panic!("Iterate state was not success, quitting...");
}
IterateResult::Success(_) => {}
}
match context.borrow().get_state() {
pulse::context::State::Ready => {
break;
}
pulse::context::State::Failed | pulse::context::State::Terminated => {
panic!("Context state failed/terminated, quitting...");
}
_ => {}
}
}
let sink_inputs: HashMap<u32, SinkInputMixerData> = HashMap::new();
let selected_index: Arc<Mutex<Option<usize>>> = Arc::new(Mutex::new(None));
context.subscribe(InterestMaskSet::SINK_INPUT, |_| {});
context.set_subscribe_callback(Some(Box::new(move |facility, operation, index| {
let Some(facility) = facility else {
return;
};
let Some(operation) = operation else {
return;
};
let Facility::SinkInput = facility else {
return;
};
pulse_ix_tx
.send(match operation {
Operation::New => PulseInstruction::AddSinkInput(index),
Operation::Changed => PulseInstruction::UpdateSinkInput(index),
Operation::Removed => PulseInstruction::RemoveSinkInput(index),
})
.unwrap();
})));
Self {
sink_inputs,
selected_index,
mainloop,
context,
}
}
pub fn create_socket_listener(&self) -> Result<UnixListener> {
let socket_path = Path::new("/tmp/mixrs");
if socket_path.exists() {
fs::remove_file(socket_path)?;
}
let listener = UnixListener::bind(socket_path)?;
Ok(listener)
}
pub fn run(&mut self, pulse_ix_rx: Receiver<PulseInstruction>) -> ! {
let listener = self
.create_socket_listener()
.expect("Error creating unix socket listener");
let (mixer_tx, mixer_rx) = channel::<MixerInstruction>();
thread::spawn(move || {
for client in listener.incoming() {
match client {
Ok(mut stream) => {
let mut buf: Vec<u8> = Vec::with_capacity(1);
stream.read_to_end(&mut buf).expect("Error reading stream");
mixer_tx.send(MixerInstruction::from_u8(buf[0])).unwrap();
}
Err(_) => println!("Stream error"),
}
}
});
let initial_sink_inputs: Arc<Mutex<HashMap<u32, SinkInputMixerData>>> =
Arc::new(Mutex::new(HashMap::new()));
let callback_initial_sink_inputs = initial_sink_inputs.clone();
let initial_sink_inputs_operation = self
.context
.borrow_mut()
.introspect()
.borrow_mut()
.get_sink_input_info_list(move |r| {
let ListResult::Item(sink_input) = r else {
return;
};
callback_initial_sink_inputs.lock().unwrap().insert(
sink_input.index,
SinkInputMixerData {
name: get_sink_input_name(&sink_input).unwrap(),
volume: sink_input.volume.avg().0,
channels: sink_input.volume.len(),
muted: sink_input.mute,
},
);
});
while initial_sink_inputs_operation.get_state() == pulse::operation::State::Running {
iterate_mainloop(&mut self.mainloop);
}
self.sink_inputs = initial_sink_inputs.lock().unwrap().clone();
*self.selected_index.lock().unwrap() = match self.sink_inputs.keys().nth(0) {
Some(_) => Some(0),
None => None,
};
loop {
match mixer_rx.try_recv() {
Ok(ix) => match ix {
MixerInstruction::SelectNext => self.select_next(),
MixerInstruction::SelectPrevious => self.select_previous(),
MixerInstruction::ToggleMuteCurrent => self.toggle_mute_current(),
MixerInstruction::IncreaseCurrent => self.increase_volume_current(),
MixerInstruction::DecreaseCurrent => self.decrease_volume_current(),
MixerInstruction::GetCurrent => self.get_current(),
MixerInstruction::PlayPauseCurrent => self.play_pause_current(),
},
Err(_) => (),
}
if let Some(ix) = pulse_ix_rx.try_recv().ok() {
match ix {
PulseInstruction::AddSinkInput(sink_index) => {
let result: Arc<Mutex<Option<SinkInputMixerData>>> =
Arc::new(Mutex::new(None));
let operation_result = result.clone();
let operation = self
.context
.borrow_mut()
.introspect()
.borrow_mut()
.get_sink_input_info(sink_index, move |r| {
if let ListResult::Item(sink_input) = r {
*operation_result.lock().unwrap() = Some(SinkInputMixerData {
name: get_sink_input_name(sink_input).unwrap(),
volume: sink_input.volume.avg().0,
channels: sink_input.volume.len(),
muted: sink_input.mute,
});
}
});
while operation.get_state() == pulse::operation::State::Running {
iterate_mainloop(&mut self.mainloop);
}
let sink_input = result.lock().unwrap().take();
if let Some(sink_input) = sink_input {
self.sink_inputs.insert(sink_index, sink_input);
}
}
PulseInstruction::RemoveSinkInput(sink_index) => {
let selected_index_lock = self.selected_index.lock().unwrap();
match *selected_index_lock {
Some(current_index) => {
drop(selected_index_lock);
let removed_sink_input_index = self
.sink_inputs
.keys()
.position(|k| *k == sink_index)
.unwrap();
let current_key =
*self.sink_inputs.keys().nth(current_index).unwrap();
if self.sink_inputs.remove(&sink_index).is_some() {
if sink_index == current_key
|| removed_sink_input_index > current_index
{
self.select_previous();
}
}
}
None => (),
}
}
PulseInstruction::UpdateSinkInput(sink_index) => {
match self.sink_inputs.get_mut(&sink_index) {
Some(sink_input_mixer_data) => {
let new_sink_input: Arc<Mutex<Option<SinkInputMixerData>>> =
Arc::new(Mutex::new(None));
let callback_new_sink_input = new_sink_input.clone();
let operation = self
.context
.borrow_mut()
.introspect()
.borrow_mut()
.get_sink_input_info(sink_index, move |r| {
let ListResult::Item(sink_input) = r else {
return;
};
*callback_new_sink_input.lock().unwrap() =
Some(SinkInputMixerData {
name: get_sink_input_name(&sink_input).unwrap(),
volume: sink_input.volume.avg().0,
channels: sink_input.volume.len(),
muted: sink_input.mute,
});
});
while operation.get_state() == pulse::operation::State::Running {
iterate_mainloop(&mut self.mainloop);
}
let mut sink_input_lock = new_sink_input.lock().unwrap();
if let Some(new_sink_input) = sink_input_lock.take() {
*sink_input_mixer_data = new_sink_input;
}
}
None => (),
}
}
}
}
iterate_mainloop(&mut self.mainloop);
}
}
pub fn select_next(&mut self) {
let mut index_lock = self.selected_index.lock().unwrap();
match *index_lock {
Some(current_index) => {
let new_index: usize =
(current_index.overflowing_add(1).0 % self.sink_inputs.len()).max(0);
if current_index != new_index {
*index_lock = Some(new_index);
}
drop(index_lock);
self.get_current();
}
None => {
*index_lock = if self.sink_inputs.len() > 0 {
Some(0)
} else {
None
};
}
}
}
pub fn select_previous(&mut self) {
let mut index_lock = self.selected_index.lock().unwrap();
match *index_lock {
Some(current_index) => {
let new_index: usize = match current_index.overflowing_sub(1) {
(_, true) => self.sink_inputs.len() - 1,
(new_value, false) => new_value,
};
if current_index != new_index {
*index_lock = Some(new_index);
}
drop(index_lock);
self.get_current();
}
None => {
*index_lock = if self.sink_inputs.len() > 0 {
Some(0)
} else {
None
};
}
}
}
pub fn toggle_mute_current(&mut self) {
let index_lock = self.selected_index.lock().unwrap();
let Some(index) = *index_lock else {
return;
};
drop(index_lock);
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
self.context
.borrow_mut()
.introspect()
.borrow_mut()
.set_sink_input_mute(
sink_index,
!self.sink_inputs.get(&sink_index).unwrap().muted,
None,
);
}
pub fn increase_volume_current(&mut self) {
let index_lock = self.selected_index.lock().unwrap();
let Some(index) = *index_lock else {
return;
};
drop(index_lock);
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
let sink_input = self.sink_inputs.get(&sink_index).unwrap();
let mut volume = ChannelVolumes::default();
volume.set(
sink_input.channels,
pulse::volume::Volume(sink_input.volume),
);
volume.increase(pulse::volume::Volume(percentage_to_total_volume(5)));
self.context
.borrow_mut()
.introspect()
.borrow_mut()
.set_sink_input_volume(sink_index, &volume, None);
}
pub fn decrease_volume_current(&mut self) {
let index_lock = self.selected_index.lock().unwrap();
let Some(index) = *index_lock else {
return;
};
drop(index_lock);
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
let sink_input = self.sink_inputs.get(&sink_index).unwrap();
let mut volume = ChannelVolumes::default();
volume.set(
sink_input.channels,
pulse::volume::Volume(sink_input.volume),
);
volume.decrease(pulse::volume::Volume(percentage_to_total_volume(5)));
self.context
.borrow_mut()
.introspect()
.borrow_mut()
.set_sink_input_volume(sink_index, &volume, None);
}
pub fn get_current(&self) {
let index_lock = self.selected_index.lock().unwrap();
let Some(index) = *index_lock else {
return;
};
drop(index_lock);
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
let current_name = &self.sink_inputs.get(&sink_index).unwrap().name;
let _ = send_notification(&format!("Selected: {current_name}"));
}
pub fn play_pause_current(&self) {
let index_lock = self.selected_index.lock().unwrap();
let Some(index) = *index_lock else {
return;
};
drop(index_lock);
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
let current_name = &self.sink_inputs.get(&sink_index).unwrap().name;
match playerctl_toggle(current_name) {
Ok(_) => {
let _ = send_notification(&format!("Toggled {current_name}"));
}
Err(_) => (),
};
}
}
pub fn iterate_mainloop(mainloop: &mut pulse::mainloop::standard::Mainloop) {
mainloop.borrow_mut().iterate(false);
thread::sleep(Duration::from_millis(5));
}

32
src/pulseaudio/mod.rs Normal file
View File

@@ -0,0 +1,32 @@
use std::u32;
use crate::utils::total_volume_to_percentage;
pub enum PulseInstruction {
AddSinkInput(u32),
RemoveSinkInput(u32),
UpdateSinkInput(u32),
}
pub enum PulseResponse {
Ok,
Error,
SinkInput(Option<SinkInputMixerData>),
SinkInputs(Vec<SinkInputMixerData>),
}
#[derive(Clone, Debug)]
pub struct SinkInputMixerData {
/// The input sink's `application.name`
pub name: String,
/// The input sink's volume
pub volume: u32,
pub muted: bool,
pub channels: u8,
}
impl SinkInputMixerData {
pub fn get_volume_percent(&self) -> u8 {
total_volume_to_percentage(self.volume)
}
}

28
src/utils.rs Normal file
View File

@@ -0,0 +1,28 @@
use anyhow::anyhow;
use pulse::{context::introspect::SinkInputInfo, volume};
const FULL_VOLUME: u32 = 1 << 16;
pub fn volume_to_percentage(volume: volume::ChannelVolumes) -> u8 {
let average = volume.avg().0;
total_volume_to_percentage(average)
}
pub fn total_volume_to_percentage(volume: u32) -> u8 {
((volume as f32 / FULL_VOLUME as f32) * 100.0).round() as u8
}
pub fn percentage_to_total_volume(percentage: u8) -> u32 {
((FULL_VOLUME as f32 / 100.0) * percentage as f32).round() as u32
}
pub fn get_sink_input_name(sink_input: &SinkInputInfo) -> anyhow::Result<String> {
let Some(name_bytes) = sink_input.proplist.get("application.name") else {
return Err(anyhow!("Invalid sink input name"));
};
Ok(String::from_utf8(
name_bytes[..name_bytes.len() - 1].to_vec(),
)?)
}