~tsdh/public-inbox

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
1

[PATCH swayrbar v2] Add pipewire (wpctl) module

Details
Message ID
<20241002154220.1009781-1-bitraid@protonmail.ch>
DKIM signature
pass
Download raw message
Patch: +231 -5
---
 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
Details
Message ID
<87v7yany9a.fsf@gnu.org>
In-Reply-To
<20241002154220.1009781-1-bitraid@protonmail.ch> (view parent)
DKIM signature
pass
Download raw message
Applied, pushed, and released as swayrbar-0.4.1!

Thanks a lot!
  Tassilo
Reply to thread Export thread (mbox)