diff --git a/swayr/src/shared/fmt.rs b/swayr/src/shared/fmt.rs index cb521a7..7ebc477 100644 --- a/swayr/src/shared/fmt.rs +++ b/swayr/src/shared/fmt.rs @@ -23,6 +23,7 @@ use std::fmt; pub enum FmtArg { I64(i64), I32(i32), + U8(u8), F64(f64), F32(f32), String(String), @@ -40,6 +41,12 @@ impl From for FmtArg { } } +impl From for FmtArg { + fn from(x: u8) -> FmtArg { + FmtArg::U8(x) + } +} + impl From for FmtArg { fn from(x: f64) -> FmtArg { FmtArg::F64(x) @@ -70,6 +77,7 @@ impl ToString for FmtArg { FmtArg::String(x) => x.clone(), FmtArg::I64(x) => x.to_string(), FmtArg::I32(x) => x.to_string(), + FmtArg::U8(x) => x.to_string(), FmtArg::F64(x) => x.to_string(), FmtArg::F32(x) => x.to_string(), } @@ -86,6 +94,7 @@ impl FormatArgument for FmtArg { Self::String(val) => fmt::Display::fmt(&val, f), Self::I64(val) => fmt::Display::fmt(&val, f), Self::I32(val) => fmt::Display::fmt(&val, f), + Self::U8(val) => fmt::Display::fmt(&val, f), Self::F64(val) => fmt::Display::fmt(&val, f), Self::F32(val) => fmt::Display::fmt(&val, f), } diff --git a/swayrbar/src/bar.rs b/swayrbar/src/bar.rs index bf85559..f8f6b0e 100644 --- a/swayrbar/src/bar.rs +++ b/swayrbar/src/bar.rs @@ -50,6 +50,7 @@ fn create_modules(config: config::Config) -> Vec> { "sysinfo" => module::sysinfo::BarModuleSysInfo::create(mc), "battery" => module::battery::BarModuleBattery::create(mc), "date" => module::date::BarModuleDate::create(mc), + "pactl" => module::pactl::BarModulePactl::create(mc), unknown => { log::warn!("Unknown module name '{}'. Ignoring...", unknown); continue; diff --git a/swayrbar/src/config.rs b/swayrbar/src/config.rs index 52a7908..8697d8c 100644 --- a/swayrbar/src/config.rs +++ b/swayrbar/src/config.rs @@ -58,6 +58,9 @@ impl Default for Config { crate::module::battery::BarModuleBattery::default_config( "0".to_owned(), ), + crate::module::pactl::BarModulePactl::default_config( + "0".to_owned(), + ), crate::module::date::BarModuleDate::default_config( "0".to_owned(), ), diff --git a/swayrbar/src/module.rs b/swayrbar/src/module.rs index 4995a3d..26d6a01 100644 --- a/swayrbar/src/module.rs +++ b/swayrbar/src/module.rs @@ -20,6 +20,7 @@ use swaybar_types as s; pub mod battery; pub mod date; +pub mod pactl; pub mod sysinfo; pub mod window; diff --git a/swayrbar/src/module/pactl.rs b/swayrbar/src/module/pactl.rs new file mode 100644 index 0000000..fb7294c --- /dev/null +++ b/swayrbar/src/module/pactl.rs @@ -0,0 +1,175 @@ +// Copyright (C) 2022 Tassilo Horn +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +//! The pactl `swayrbar` module. + +use crate::config; +use crate::module::BarModuleFn; +use crate::shared::fmt::subst_placeholders; +use once_cell::sync::Lazy; +use regex::Regex; +use std::collections::HashMap; +use std::process::Command; +use std::sync::Mutex; +use swaybar_types as s; + +const NAME: &str = "pactl"; + +struct State { + volume: u8, + muted: bool, +} + +pub static VOLUME_RX: Lazy = + Lazy::new(|| Regex::new(r".?* (\d+)%.*").unwrap()); + +fn run_pactl(args: &[&str]) -> String { + match Command::new("pactl").args(args).output() { + Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(), + Err(err) => { + log::error!("Could not run pactl: {}", err); + String::new() + } + } +} + +fn get_volume() -> u8 { + let output = run_pactl(&["get-sink-volume", "@DEFAULT_SINK@"]); + VOLUME_RX + .captures(&output) + .map(|c| c.get(1).unwrap().as_str().parse::().unwrap()) + .unwrap_or(255_u8) +} + +fn get_mute_state() -> bool { + run_pactl(&["get-sink-mute", "@DEFAULT_SINK@"]).contains("yes") +} + +pub struct BarModulePactl { + config: config::ModuleConfig, + state: Mutex, +} + +fn refresh_state(state: &mut State) { + state.volume = get_volume(); + state.muted = get_mute_state(); +} + +fn get_text(fmt: &str, html_escape: bool, state: &State) -> String { + subst_placeholders!(fmt, html_escape, { + "volume" => { + state.volume + }, + "muted" =>{ + if state.muted { + " muted" + } else { + "" + } + }, + }) +} + +impl BarModuleFn for BarModulePactl { + fn create(config: config::ModuleConfig) -> Box + where + Self: Sized, + { + Box::new(BarModulePactl { + config, + state: Mutex::new(State { + volume: 255_u8, + muted: false, + }), + }) + } + + fn default_config(instance: String) -> config::ModuleConfig + where + Self: Sized, + { + config::ModuleConfig { + name: NAME.to_owned(), + instance, + format: "🔈 Vol: {volume:{:3}}%{muted}".to_owned(), + html_escape: Some(true), + on_click: Some(HashMap::from([ + ("Left".to_owned(), vec!["pavucontrol".to_owned()]), + ( + "Right".to_owned(), + vec![ + "pactl".to_owned(), + "set-sink-mute".to_owned(), + "@DEFAULT_SINK@".to_owned(), + "toggle".to_owned(), + ], + ), + ( + "WheelUp".to_owned(), + vec![ + "pactl".to_owned(), + "set-sink-volume".to_owned(), + "@DEFAULT_SINK@".to_owned(), + "+1%".to_owned(), + ], + ), + ( + "WheelDown".to_owned(), + vec![ + "pactl".to_owned(), + "set-sink-volume".to_owned(), + "@DEFAULT_SINK@".to_owned(), + "-1%".to_owned(), + ], + ), + ])), + } + } + + fn get_config(&self) -> &config::ModuleConfig { + &self.config + } + + fn build(&self) -> s::Block { + let mut state = self.state.lock().expect("Could not lock state."); + refresh_state(&mut state); + let text = + get_text(&self.config.format, self.config.is_html_escape(), &state); + s::Block { + name: Some(NAME.to_owned()), + instance: Some(self.config.instance.clone()), + full_text: text, + align: Some(s::Align::Left), + markup: Some(s::Markup::Pango), + short_text: None, + color: None, + background: None, + border: None, + border_top: None, + border_bottom: None, + border_left: None, + border_right: None, + min_width: None, + urgent: None, + separator: Some(true), + separator_block_width: None, + } + } + + fn subst_args<'a>(&'a self, cmd: &'a [String]) -> Option> { + let state = self.state.lock().expect("Could not lock state."); + Some(cmd.iter().map(|arg| get_text(arg, false, &state)).collect()) + } +}