Add sinks command

This commit is contained in:
2024-08-14 11:11:02 +02:00
parent 1f6e33fa75
commit e4ffeb6747
7 changed files with 146 additions and 33 deletions

View File

@ -1,23 +1,11 @@
use eyre::{bail, Context}; use crate::util::CommandExt;
/// Update eww bar variable /// Update eww bar variable
pub fn update_var(key: &str, value: &str) -> eyre::Result<()> { pub fn update_var(key: &str, value: &str) -> eyre::Result<()> {
println!("eww update {key}={value}"); std::process::Command::new("eww")
let output = std::process::Command::new("eww")
.arg("update") .arg("update")
.arg(format!("{key}={value}")) .arg(format!("{key}={value}"))
.output() .just_exec()?;
.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.");
}
Ok(()) Ok(())
} }

View File

@ -1,5 +1,5 @@
use crate::{eww, output, Command}; use crate::{eww, output, pulse, util::CommandExt, Command};
use eyre::{bail, eyre, Context}; use eyre::{bail, Context};
use serde::{de::DeserializeOwned, Deserialize}; use serde::{de::DeserializeOwned, Deserialize};
use std::str; use std::str;
@ -22,34 +22,26 @@ pub fn handle(command: Command) -> eyre::Result<()> {
std::process::Command::new("hyprctl") std::process::Command::new("hyprctl")
.args(["dispatch", "workspace"]) .args(["dispatch", "workspace"])
.arg(format!("{to}")) .arg(format!("{to}"))
.status() .just_exec()?;
.map_err(|e| eyre!("hyprctl error: {e}"))?;
eww::update_var("workspaces", &get_workspaces()?)?; eww::update_var("workspaces", &get_workspaces()?)?;
} }
Command::KeyboardLayout { .. } => { Command::KeyboardLayout { .. } => {
bail!("not supported on Hyprland"); bail!("not supported on Hyprland");
} }
Command::Sinks => {
println!("{}", serde_json::to_string(&pulse::get_sinks()?)?);
}
} }
Ok(()) Ok(())
} }
fn hyprctl<T: DeserializeOwned>(args: &[&str]) -> eyre::Result<T> { fn hyprctl<T: DeserializeOwned>(args: &[&str]) -> eyre::Result<T> {
let workspaces_output = std::process::Command::new("hyprctl") std::process::Command::new("hyprctl")
.args(args) .args(args)
.arg("-j") // JSON output .arg("-j") // JSON output
.output() .just_exec_json()
.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")
} }
/// Get a JSON-string containing info about workspaces /// Get a JSON-string containing info about workspaces

View File

@ -7,6 +7,8 @@ mod eww;
mod hyprland; mod hyprland;
mod niri; mod niri;
mod output; mod output;
mod pulse;
mod util;
#[derive(Parser)] #[derive(Parser)]
struct Opt { struct Opt {
@ -28,6 +30,9 @@ enum Command {
SwitchWorkspace { SwitchWorkspace {
to: u8, to: u8,
}, },
/// Get the list out audio sinks
Sinks,
} }
enum WindowManager { enum WindowManager {

View File

@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::{eww, Command, Workspace}; use crate::{eww, pulse, Command, Workspace};
use eyre::{bail, eyre, Context}; use eyre::{bail, eyre, Context};
use niri_ipc::{Socket, WorkspaceReferenceArg}; use niri_ipc::{Socket, WorkspaceReferenceArg};
@ -21,7 +21,10 @@ pub fn handle(command: Command) -> eyre::Result<()> {
eww::update_var("workspaces", &get_workspaces()?)?; 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(()) Ok(())

View File

@ -8,3 +8,18 @@ pub struct Workspace {
pub monitor: u32, pub monitor: u32,
pub active: bool, pub active: bool,
} }
#[derive(Clone, Default, Debug, Serialize)]
pub struct SinkList {
pub default: Option<Sink>,
pub all: Vec<Sink>,
}
#[derive(Clone, Debug, Serialize)]
pub struct Sink {
pub name: String,
pub pretty_name: String,
pub muted: bool,
pub default: bool,
pub volume: u8,
}

62
src/pulse.rs Normal file
View File

@ -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<output::SinkList> {
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<Vec<Sink>> {
std::process::Command::new("pactl")
.args(["--format", "json"])
.args(["list", "sinks"])
.just_exec_json()
}
fn pactl_get_default_sink_name() -> eyre::Result<String> {
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)
}

48
src/util.rs Normal file
View File

@ -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<String>;
fn error(&self) -> String;
fn just_exec_json<T: DeserializeOwned>(&mut self) -> eyre::Result<T> {
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<String> {
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}")
}
}