From 621c1bd4d8e6dc9384208bc08221c19baccc8fff Mon Sep 17 00:00:00 2001 From: imgurbot12 Date: Fri, 11 Aug 2023 16:24:30 -0700 Subject: [PATCH] feat: better config defaults, added dmenu format, no-comments mode, placeholder opt --- Cargo.toml | 1 + Makefile | 2 + plugin-audio/Cargo.toml | 14 ++++++ plugin-audio/src/main.rs | 43 ++++++++++++++++ plugin-audio/src/pulse.rs | 100 ++++++++++++++++++++++++++++++++++++++ rmenu/public/config.yaml | 4 ++ 6 files changed, 164 insertions(+) create mode 100644 plugin-audio/Cargo.toml create mode 100644 plugin-audio/src/main.rs create mode 100644 plugin-audio/src/pulse.rs diff --git a/Cargo.toml b/Cargo.toml index 88c38ca..03923ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,5 @@ members = [ "rmenu-plugin", "plugin-run", "plugin-desktop", + "plugin-audio", ] diff --git a/Makefile b/Makefile index 476bd67..410e468 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ deploy: cp -vf ./target/release/rmenu ${INSTALL}/rmenu cp -vf ./target/release/desktop ${DEST}/drun cp -vf ./target/release/run ${DEST}/run + cp -vf ./target/release/audio ${DEST}/audio cp -vf ./rmenu/public/config.yaml ${DEST}/config.yaml build: build-rmenu build-plugins @@ -30,3 +31,4 @@ build-rmenu: build-plugins: ${CARGO} build -p run ${FLAGS} ${CARGO} build -p desktop ${FLAGS} + ${CARGO} build -p audio ${FLAGS} diff --git a/plugin-audio/Cargo.toml b/plugin-audio/Cargo.toml new file mode 100644 index 0000000..b07ebf3 --- /dev/null +++ b/plugin-audio/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "audio" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.3.21", features = ["derive"] } +env_logger = "0.10.0" +log = "0.4.19" +rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" } +serde_json = "1.0.104" +thiserror = "1.0.44" diff --git a/plugin-audio/src/main.rs b/plugin-audio/src/main.rs new file mode 100644 index 0000000..74fcd82 --- /dev/null +++ b/plugin-audio/src/main.rs @@ -0,0 +1,43 @@ +use clap::{Parser, Subcommand}; +use rmenu_plugin::Entry; + +mod pulse; + +#[derive(Debug, Subcommand)] +pub enum Commands { + ListSinks, + SetDefaultSink { sink: u32 }, +} + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct Cli { + #[clap(subcommand)] + command: Option, +} + +fn main() -> Result<(), pulse::PulseError> { + let cli = Cli::parse(); + let exe = std::env::current_exe()?.to_str().unwrap().to_string(); + + let command = cli.command.unwrap_or(Commands::ListSinks); + match command { + Commands::ListSinks => { + let sinks = pulse::get_sinks()?; + for sink in sinks { + let star = sink.default.then(|| "* ").unwrap_or(""); + let desc = format!("{star}{}", sink.description); + let exec = format!("{exe} set-default-sink {}", sink.index); + let entry = Entry::new(&desc, &exec, None); + println!("{}", serde_json::to_string(&entry).unwrap()); + } + } + Commands::SetDefaultSink { sink } => { + let sinkstr = format!("{sink}"); + pulse::set_default_sink(&sinkstr)?; + } + } + + Ok(()) +} diff --git a/plugin-audio/src/pulse.rs b/plugin-audio/src/pulse.rs new file mode 100644 index 0000000..eb1eed5 --- /dev/null +++ b/plugin-audio/src/pulse.rs @@ -0,0 +1,100 @@ +use std::io::{BufRead, BufReader}; +use std::process::{Command, ExitStatus, Stdio}; + +use thiserror::Error; + +static PACTL: &'static str = "pactl"; + +#[derive(Debug, Error)] +pub enum PulseError { + #[error("Invalid Status")] + InvalidStatus(ExitStatus), + #[error("Cannot Read Command Output")] + InvalidStdout, + #[error("Invalid Output Encoding")] + InvalidEncoding(#[from] std::string::FromUtf8Error), + #[error("Unexepcted Command Output")] + UnexepctedOutput(String), + #[error("Command Error")] + CommandError(#[from] std::io::Error), +} + +#[derive(Debug)] +pub struct Sink { + pub index: u32, + pub name: String, + pub description: String, + pub default: bool, +} + +/// Retrieve Default Sink +pub fn get_default_sink() -> Result { + let output = Command::new(PACTL).arg("get-default-sink").output()?; + if !output.status.success() { + return Err(PulseError::InvalidStatus(output.status)); + } + Ok(String::from_utf8(output.stdout)?.trim().to_string()) +} + +/// Set Default PACTL Sink +pub fn set_default_sink(name: &str) -> Result<(), PulseError> { + let status = Command::new(PACTL) + .args(["set-default-sink", name]) + .status()?; + if !status.success() { + return Err(PulseError::InvalidStatus(status)); + } + Ok(()) +} + +/// Retrieve PulseAudio Sinks From PACTL +pub fn get_sinks() -> Result, PulseError> { + // retrieve default-sink + let default = get_default_sink()?; + // spawn command and begin reading + let mut command = Command::new(PACTL) + .args(["list", "sinks"]) + .stdout(Stdio::piped()) + .spawn()?; + let stdout = command.stdout.as_mut().ok_or(PulseError::InvalidStdout)?; + // collect output into only important lines + let mut lines = vec![]; + for line in BufReader::new(stdout).lines().filter_map(|l| l.ok()) { + let line = line.trim_start(); + if line.starts_with("Sink ") + || line.starts_with("Name: ") + || line.starts_with("Description: ") + { + lines.push(line.to_owned()); + } + } + // ensure status after command completion + let status = command.wait()?; + if !status.success() { + return Err(PulseError::InvalidStatus(status)); + } + // ensure number of lines matches expected + if lines.len() == 0 || lines.len() % 3 != 0 { + return Err(PulseError::UnexepctedOutput(lines.join("\n"))); + } + // parse details into chunks and generate sinks + let mut sinks = vec![]; + for chunk in lines.chunks(3) { + let (_, idx) = chunk[0] + .split_once("#") + .ok_or_else(|| PulseError::UnexepctedOutput(chunk[0].to_owned()))?; + let (_, name) = chunk[1] + .split_once(" ") + .ok_or_else(|| PulseError::UnexepctedOutput(chunk[1].to_owned()))?; + let (_, desc) = chunk[2] + .split_once(" ") + .ok_or_else(|| PulseError::UnexepctedOutput(chunk[2].to_owned()))?; + sinks.push(Sink { + index: idx.parse().unwrap(), + name: name.to_owned(), + description: desc.to_owned(), + default: name == default, + }); + } + Ok(sinks) +} diff --git a/rmenu/public/config.yaml b/rmenu/public/config.yaml index a2d9cd0..3fadeb6 100644 --- a/rmenu/public/config.yaml +++ b/rmenu/public/config.yaml @@ -25,6 +25,10 @@ plugins: drun: exec: ["~/.config/rmenu/drun"] cache: onlogin + audio: + exec: ["~/.config/rmenu/audio"] + cache: false + placeholder: "Select an Audio Sink" # custom keybindings keybinds: