mirror of
https://github.com/imgurbot12/rmenu.git
synced 2025-01-27 05:18:33 +01:00
feat: better config defaults, added dmenu format, no-comments mode, placeholder opt
This commit is contained in:
parent
8bd0a91eb1
commit
621c1bd4d8
6 changed files with 164 additions and 0 deletions
|
@ -5,4 +5,5 @@ members = [
|
||||||
"rmenu-plugin",
|
"rmenu-plugin",
|
||||||
"plugin-run",
|
"plugin-run",
|
||||||
"plugin-desktop",
|
"plugin-desktop",
|
||||||
|
"plugin-audio",
|
||||||
]
|
]
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -20,6 +20,7 @@ deploy:
|
||||||
cp -vf ./target/release/rmenu ${INSTALL}/rmenu
|
cp -vf ./target/release/rmenu ${INSTALL}/rmenu
|
||||||
cp -vf ./target/release/desktop ${DEST}/drun
|
cp -vf ./target/release/desktop ${DEST}/drun
|
||||||
cp -vf ./target/release/run ${DEST}/run
|
cp -vf ./target/release/run ${DEST}/run
|
||||||
|
cp -vf ./target/release/audio ${DEST}/audio
|
||||||
cp -vf ./rmenu/public/config.yaml ${DEST}/config.yaml
|
cp -vf ./rmenu/public/config.yaml ${DEST}/config.yaml
|
||||||
|
|
||||||
build: build-rmenu build-plugins
|
build: build-rmenu build-plugins
|
||||||
|
@ -30,3 +31,4 @@ build-rmenu:
|
||||||
build-plugins:
|
build-plugins:
|
||||||
${CARGO} build -p run ${FLAGS}
|
${CARGO} build -p run ${FLAGS}
|
||||||
${CARGO} build -p desktop ${FLAGS}
|
${CARGO} build -p desktop ${FLAGS}
|
||||||
|
${CARGO} build -p audio ${FLAGS}
|
||||||
|
|
14
plugin-audio/Cargo.toml
Normal file
14
plugin-audio/Cargo.toml
Normal file
|
@ -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"
|
43
plugin-audio/src/main.rs
Normal file
43
plugin-audio/src/main.rs
Normal file
|
@ -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<Commands>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
100
plugin-audio/src/pulse.rs
Normal file
100
plugin-audio/src/pulse.rs
Normal file
|
@ -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<String, PulseError> {
|
||||||
|
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<Vec<Sink>, 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)
|
||||||
|
}
|
|
@ -25,6 +25,10 @@ plugins:
|
||||||
drun:
|
drun:
|
||||||
exec: ["~/.config/rmenu/drun"]
|
exec: ["~/.config/rmenu/drun"]
|
||||||
cache: onlogin
|
cache: onlogin
|
||||||
|
audio:
|
||||||
|
exec: ["~/.config/rmenu/audio"]
|
||||||
|
cache: false
|
||||||
|
placeholder: "Select an Audio Sink"
|
||||||
|
|
||||||
# custom keybindings
|
# custom keybindings
|
||||||
keybinds:
|
keybinds:
|
||||||
|
|
Loading…
Reference in a new issue