---
README.md | 30 +++++-
swayrbar/src/bar.rs | 1 +
swayrbar/src/module.rs | 1 +
swayrbar/src/module/wpctl.rs | 204 +++++++++++++++++++++++++++++++++++
4 files changed, 231 insertions(+), 5 deletions(-)
create mode 100644 swayrbar/src/module/wpctl.rs
diff --git a/README.md b/README.md
index 1321bc2..1dc6e84 100644
--- a/README.md
+++ b/README.md
@@ -212,7 +212,7 @@ These commands change the layout of the current workspace.
between a tabbed and tiled layout, i.e., it calls `shuffle-tile-workspace` if
it is currently tabbed, and calls `shuffle-tile-workspace` if it is currently
tiled.
-
+
#### Scripting commands
* `get-windows-as-json` returns a JSON containing all windows, possibly with
@@ -265,7 +265,7 @@ These commands change the layout of the current workspace.
Swayr supports most of the criteria querys defined by Sway, see section
`CRITERIA` in `man sway(5)`. Right now, these are:
* `app_id=<regex | __focused__>`
-* `class=<regex | __focused__>`
+* `class=<regex | __focused__>`
* `instance=<regex | __focused__>`
* `title=<regex | __focused__>`
* `workspace=<regex | __focused__ | __visible__ >`
@@ -276,7 +276,7 @@ Swayr supports most of the criteria querys defined by Sway, see section
* `floating`
* `tiling`
* `app_name=<regex | __focused__>` (not in sway!)
-
+
The last criterion `app_name` is matched against the application's name which
can either be `app_id`, `window_properties.class`, or
`window_properties.instance` (whatever is filled).
@@ -717,9 +717,12 @@ Right now, there are the following modules:
4. The `date` module can show, you guess it, the current date and time!
5. The `pactl` module can show the current volume percentage and muted state.
Clicks can increase/decrease the volume or toggle the mute state.
-6. The `nmcli` module uses NetworkManager's `nmcli` command line tool to show
+6. The `wpctl` module can show the current volume percentage and muted state.
+ Clicks can increase/decrease the volume or toggle the mute state. It
+ requires PipeWire.
+7. The `nmcli` module uses NetworkManager's `nmcli` command line tool to show
the currently connected wifi and its signal strength.
-7. The `iwctl` module the `iwctl` command line tool to show the currently
+8. The `iwctl` module the `iwctl` command line tool to show the currently
connected wifi and its signal strength.
@@ -892,6 +895,23 @@ By default, it has the following click bindings:
* `WheelUp` and `WheelDown` increase/decrease the volume of the default sink.
+#### The `wpctl` module
+
+The `wpctl` module requires the pipewire command line tool of the same name
+to be installed. It supports the following placeholders:
+* `{volume}` is the current volume percentage of the default sink.
+* `{muted}` is the string `" muted"` if the default sink is currently muted,
+ otherwise it is the empty string.
+* `{volume_source}` is the current volume percentage of the default source.
+* `{muted_source}` is the string `" muted"` if the default source is currently
+ muted, otherwise it is the empty string.
+
+By default, it has the following click bindings:
+* `Left` executes `foot watch wpctl status` (monitor PipeWire objects).
+* `Right` toggles the default sink's mute state.
+* `WheelUp` and `WheelDown` increase/decrease the volume of the default sink.
+
+
#### The `nmcli` module
The `nmcli` module requires NetworkManager and the `nmcli` command line tool.
diff --git a/swayrbar/src/bar.rs b/swayrbar/src/bar.rs
index 8321c3b..b62053b 100644
--- a/swayrbar/src/bar.rs
+++ b/swayrbar/src/bar.rs
@@ -97,6 +97,7 @@ fn create_modules(config: config::Config) -> Vec<Box<dyn BarModuleFn>> {
"battery" => module::battery::create(mc),
"date" => module::date::create(mc),
"pactl" => module::pactl::create(mc),
+ "wpctl" => module::wpctl::create(mc),
"nmcli" => module::wifi::create(module::wifi::WifiTool::Nmcli, mc),
"iwctl" => module::wifi::create(module::wifi::WifiTool::Iwctl, mc),
"cmd" => module::cmd::create(mc),
diff --git a/swayrbar/src/module.rs b/swayrbar/src/module.rs
index fe2d39f..c3e3ba4 100644
--- a/swayrbar/src/module.rs
+++ b/swayrbar/src/module.rs
@@ -23,6 +23,7 @@ pub mod battery;
pub mod cmd;
pub mod date;
pub mod pactl;
+pub mod wpctl;
pub mod sysinfo;
pub mod wifi;
pub mod window;
diff --git a/swayrbar/src/module/wpctl.rs b/swayrbar/src/module/wpctl.rs
new file mode 100644
index 0000000..be9f4f4
--- /dev/null
+++ b/swayrbar/src/module/wpctl.rs
@@ -0,0 +1,204 @@
+// Copyright (C) 2024 Tassilo Horn <tsdh@gnu.org>
+// Copyright (C) 2024 bitraid <bitraid@protonmail.ch>
+//
+// 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 <https://www.gnu.org/licenses/>.
+
+//! The wpctl `swayrbar` module.
+
+use crate::config;
+use crate::module::{BarModuleFn, RefreshReason};
+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 = "wpctl";
+
+struct State {
+ volume: u8,
+ muted: bool,
+ volume_source: u8,
+ muted_source: bool,
+ cached_text: String,
+}
+
+pub static VOLUME_RX: Lazy<Regex> =
+ Lazy::new(|| Regex::new(r".* (?<num>\d+)\.(?<frac>\d{2}).*").unwrap());
+
+fn run_wpctl(args: &[&str]) -> String {
+ match Command::new("wpctl").args(args).output() {
+ Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(),
+ Err(err) => {
+ log::error!("Could not run wpctl: {err}");
+ String::new()
+ }
+ }
+}
+
+fn get_volume(device: &str) -> (u8, bool) {
+ let output = run_wpctl(&["get-volume", device]);
+ let mut volume = String::new();
+ VOLUME_RX
+ .captures(&output)
+ .unwrap()
+ .expand("$num$frac", &mut volume);
+ (volume.parse::<u8>().unwrap_or(255_u8), output.contains("[MUTED]"))
+}
+
+pub struct BarModuleWpctl {
+ config: config::ModuleConfig,
+ state: Mutex<State>,
+}
+
+fn refresh_state(state: &mut State, fmt_str: &str, html_escape: bool) {
+ (state.volume, state.muted) = get_volume("@DEFAULT_AUDIO_SINK@");
+ (state.volume_source, state.muted_source) = get_volume("@DEFAULT_AUDIO_SOURCE@");
+ state.cached_text = subst_placeholders(fmt_str, html_escape, state);
+}
+
+fn subst_placeholders(fmt: &str, html_escape: bool, state: &State) -> String {
+ subst_placeholders!(fmt, html_escape, {
+ "volume" => {
+ state.volume
+ },
+ "muted" =>{
+ if state.muted {
+ " muted"
+ } else {
+ ""
+ }
+ },
+ "volume_source" => {
+ state.volume_source
+ },
+ "muted_source" =>{
+ if state.muted_source {
+ " muted"
+ } else {
+ ""
+ }
+ },
+ })
+}
+
+pub fn create(config: config::ModuleConfig) -> Box<dyn BarModuleFn> {
+ Box::new(BarModuleWpctl {
+ config,
+ state: Mutex::new(State {
+ volume: 255_u8,
+ muted: false,
+ volume_source: 255_u8,
+ muted_source: false,
+ cached_text: String::new(),
+ }),
+ })
+}
+
+impl BarModuleFn for BarModuleWpctl {
+ 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!["foot".to_owned(), "watch".to_owned(),
+ "wpctl".to_owned(), "status".to_owned()]),
+ (
+ "Right".to_owned(),
+ vec![
+ "wpctl".to_owned(),
+ "set-mute".to_owned(),
+ "@DEFAULT_AUDIO_SINK@".to_owned(),
+ "toggle".to_owned(),
+ ],
+ ),
+ (
+ "WheelUp".to_owned(),
+ vec![
+ "wpctl".to_owned(),
+ "set-volume".to_owned(),
+ "@DEFAULT_AUDIO_SINK@".to_owned(),
+ "1%+".to_owned(),
+ ],
+ ),
+ (
+ "WheelDown".to_owned(),
+ vec![
+ "wpctl".to_owned(),
+ "set-volume".to_owned(),
+ "@DEFAULT_AUDIO_SINK@".to_owned(),
+ "1%-".to_owned(),
+ ],
+ ),
+ ])),
+ }
+ }
+
+ fn get_config(&self) -> &config::ModuleConfig {
+ &self.config
+ }
+
+ fn build(&self, reason: &RefreshReason) -> s::Block {
+ let mut state = self.state.lock().expect("Could not lock state.");
+
+ if match reason {
+ RefreshReason::TimerEvent => true,
+ RefreshReason::ClickEvent { name, instance } => {
+ name == &self.config.name && instance == &self.config.instance
+ }
+ _ => false,
+ } {
+ refresh_state(
+ &mut state,
+ &self.config.format,
+ self.config.is_html_escape(),
+ );
+ }
+
+ s::Block {
+ name: Some(NAME.to_owned()),
+ instance: Some(self.config.instance.clone()),
+ full_text: state.cached_text.to_owned(),
+ 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_cmd_args<'a>(&'a self, cmd: &'a [String]) -> Vec<String> {
+ let state = self.state.lock().expect("Could not lock state.");
+ cmd.iter()
+ .map(|arg| subst_placeholders(arg, false, &state))
+ .collect()
+ }
+}
--
2.46.1