From e4ffeb67471718cc6036f29ffdd5888f57646a2b Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Wed, 14 Aug 2024 11:11:02 +0200 Subject: [PATCH] Add `sinks` command --- src/eww.rs | 18 +++----------- src/hyprland.rs | 24 +++++++------------ src/main.rs | 5 ++++ src/niri.rs | 7 ++++-- src/output.rs | 15 ++++++++++++ src/pulse.rs | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ src/util.rs | 48 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 146 insertions(+), 33 deletions(-) create mode 100644 src/pulse.rs create mode 100644 src/util.rs diff --git a/src/eww.rs b/src/eww.rs index b1dd34d..53e97e7 100644 --- a/src/eww.rs +++ b/src/eww.rs @@ -1,23 +1,11 @@ -use eyre::{bail, Context}; +use crate::util::CommandExt; /// Update eww bar variable pub fn update_var(key: &str, value: &str) -> eyre::Result<()> { - println!("eww update {key}={value}"); - let output = std::process::Command::new("eww") + std::process::Command::new("eww") .arg("update") .arg(format!("{key}={value}")) - .output() - .wrap_err("failed to execute 'eww update'")?; - - if !output.status.success() { - let stdout = std::str::from_utf8(&output.stdout).unwrap_or("Invalid UTF-8"); - let stderr = std::str::from_utf8(&output.stderr).unwrap_or("Invalid UTF-8"); - - eprintln!("'eww update' stdout: {stdout}"); - eprintln!("'eww update' stderr: {stderr}"); - - bail!("'eww update' failed. See logs."); - } + .just_exec()?; Ok(()) } diff --git a/src/hyprland.rs b/src/hyprland.rs index 628a816..5f31788 100644 --- a/src/hyprland.rs +++ b/src/hyprland.rs @@ -1,5 +1,5 @@ -use crate::{eww, output, Command}; -use eyre::{bail, eyre, Context}; +use crate::{eww, output, pulse, util::CommandExt, Command}; +use eyre::{bail, Context}; use serde::{de::DeserializeOwned, Deserialize}; use std::str; @@ -22,34 +22,26 @@ pub fn handle(command: Command) -> eyre::Result<()> { std::process::Command::new("hyprctl") .args(["dispatch", "workspace"]) .arg(format!("{to}")) - .status() - .map_err(|e| eyre!("hyprctl error: {e}"))?; + .just_exec()?; eww::update_var("workspaces", &get_workspaces()?)?; } Command::KeyboardLayout { .. } => { bail!("not supported on Hyprland"); } + Command::Sinks => { + println!("{}", serde_json::to_string(&pulse::get_sinks()?)?); + } } Ok(()) } fn hyprctl(args: &[&str]) -> eyre::Result { - let workspaces_output = std::process::Command::new("hyprctl") + std::process::Command::new("hyprctl") .args(args) .arg("-j") // JSON output - .output() - .map_err(|e| eyre!("hyprctl error: {e}"))?; - - let workspaces_stdout = str::from_utf8(&workspaces_output.stdout)?; - //let workspaces_stderr = str::from_utf8(&workspaces_output.stderr)?; - - if !workspaces_output.status.success() { - bail!("hyprctl error, non-zero exit code"); - } - - serde_json::from_str(workspaces_stdout).wrap_err("Failed to deserialize output from hyprctl") + .just_exec_json() } /// Get a JSON-string containing info about workspaces diff --git a/src/main.rs b/src/main.rs index 9b8cba9..f82743f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,8 @@ mod eww; mod hyprland; mod niri; mod output; +mod pulse; +mod util; #[derive(Parser)] struct Opt { @@ -28,6 +30,9 @@ enum Command { SwitchWorkspace { to: u8, }, + + /// Get the list out audio sinks + Sinks, } enum WindowManager { diff --git a/src/niri.rs b/src/niri.rs index 2d3a839..0f8d858 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::{eww, Command, Workspace}; +use crate::{eww, pulse, Command, Workspace}; use eyre::{bail, eyre, Context}; use niri_ipc::{Socket, WorkspaceReferenceArg}; @@ -21,7 +21,10 @@ pub fn handle(command: Command) -> eyre::Result<()> { eww::update_var("workspaces", &get_workspaces()?)?; } - Command::KeyboardLayout { next: _ } => todo!(), + Command::KeyboardLayout { next: _ } => bail!("Not implemented"), + Command::Sinks => { + println!("{}", serde_json::to_string(&pulse::get_sinks()?)?); + } } Ok(()) diff --git a/src/output.rs b/src/output.rs index 0b2cfe6..ee4fd36 100644 --- a/src/output.rs +++ b/src/output.rs @@ -8,3 +8,18 @@ pub struct Workspace { pub monitor: u32, pub active: bool, } + +#[derive(Clone, Default, Debug, Serialize)] +pub struct SinkList { + pub default: Option, + pub all: Vec, +} + +#[derive(Clone, Debug, Serialize)] +pub struct Sink { + pub name: String, + pub pretty_name: String, + pub muted: bool, + pub default: bool, + pub volume: u8, +} diff --git a/src/pulse.rs b/src/pulse.rs new file mode 100644 index 0000000..9c55725 --- /dev/null +++ b/src/pulse.rs @@ -0,0 +1,62 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + output::{self, SinkList}, + util::CommandExt, +}; + +#[derive(Serialize, Deserialize)] +pub struct Sink { + pub state: String, + pub name: String, + pub description: String, + pub mute: bool, +} + +pub fn get_sinks() -> eyre::Result { + let default_sink = pactl_get_default_sink_name()?; + let sinks = pactl_get_sinks()?; + + let mut output = SinkList::default(); + + for sink in sinks { + let default = dbg!(&sink.name) == dbg!(&default_sink); + + let sink = output::Sink { + name: sink.name, + pretty_name: sink.description, + muted: sink.mute, + default, + volume: 0, // TODO + }; + + if default { + output.default = Some(sink.clone()); + } + + output.all.push(sink); + } + + output.all.sort_by_key(|s| s.pretty_name.clone()); + + Ok(output) +} + +fn pactl_get_sinks() -> eyre::Result> { + std::process::Command::new("pactl") + .args(["--format", "json"]) + .args(["list", "sinks"]) + .just_exec_json() +} + +fn pactl_get_default_sink_name() -> eyre::Result { + let mut default_sink = std::process::Command::new("pactl") + .arg("get-default-sink") + .just_exec()?; + + while default_sink.ends_with(char::is_whitespace) { + default_sink.pop(); + } + + Ok(default_sink) +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..e98c5c2 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,48 @@ +use eyre::{eyre, Context}; +use serde::de::DeserializeOwned; +use std::{fmt::Write, str}; + +pub trait CommandExt: Sized { + fn just_exec(&mut self) -> eyre::Result; + + fn error(&self) -> String; + + fn just_exec_json(&mut self) -> eyre::Result { + let error = self.error(); + let output = self.just_exec()?; + serde_json::from_str(&output) + .wrap_err("Failed to deserialize output as JSON") + .wrap_err(error) + } +} + +impl CommandExt for std::process::Command { + fn just_exec(&mut self) -> eyre::Result { + let error = self.error(); + let output = self + .output() + .wrap_err("Failed to exec command") + .wrap_err_with(|| error.clone())?; + + let stdout = str::from_utf8(&output.stdout) + .wrap_err("Output wasn't valid UTF-8") + .wrap_err_with(|| error.clone())?; + + if !output.status.success() { + return Err(eyre!("Non-zero exit code ({})", output.status)) + .wrap_err_with(|| error.clone()); + } + + Ok(stdout.to_string()) + } + + fn error(&self) -> String { + let mut command_string = self.get_program().to_string_lossy().to_string(); + + for arg in self.get_args() { + let _ = write!(&mut command_string, " {arg:?}"); + } + + format!("Command failed: {command_string}") + } +}