From b037a55fb78d05cce0e03bad27a10cbdf743c573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Labeyrie?= Date: Mon, 12 Feb 2024 23:57:27 +0100 Subject: [PATCH 01/27] feat(tray): add `direction` option --- docs/modules/Tray.md | 17 +++++++++++++---- src/modules/tray/mod.rs | 31 ++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/docs/modules/Tray.md b/docs/modules/Tray.md index 7115b75..a80c48e 100644 --- a/docs/modules/Tray.md +++ b/docs/modules/Tray.md @@ -6,7 +6,10 @@ Displays a fully interactive icon tray using the KDE `libappindicator` protocol. > Type: `tray` -***This module provides no configuration options.*** + +| Name | Type | Default | Description | +|-------------|----------|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| +| `direction` | `string` | `left_to_right` if bar is horizontal, `top_to_bottom` otherwise | Direction to display the tray items. Possible values: `top_to_bottom`, `bottom_to_top`, `left_to_right`, `right_to_left` |
JSON @@ -15,7 +18,8 @@ Displays a fully interactive icon tray using the KDE `libappindicator` protocol. { "end": [ { - "type": "tray" + "type": "tray", + "direction": "top_to_bottom" } ] } @@ -29,6 +33,7 @@ Displays a fully interactive icon tray using the KDE `libappindicator` protocol. ```toml [[end]] type = "tray" +direction = "top_to_bottom" ```
@@ -39,6 +44,7 @@ type = "tray" ```yaml end: - type: "tray" + direction: "top_to_bottom" ``` @@ -49,7 +55,10 @@ end: ```corn { end = [ - { type = "tray" } + { + type = "tray" + direction = "top_to_bottom" + } ] } ``` @@ -63,4 +72,4 @@ end: | `.tray` | Tray widget box | | `.tray .item` | Tray icon button | -For more information on styling, please see the [styling guide](styling-guide). \ No newline at end of file +For more information on styling, please see the [styling guide](styling-guide). diff --git a/src/modules/tray/mod.rs b/src/modules/tray/mod.rs index b2830a8..5231e09 100644 --- a/src/modules/tray/mod.rs +++ b/src/modules/tray/mod.rs @@ -8,7 +8,7 @@ use crate::modules::tray::diff::get_diffs; use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::{glib_recv, spawn}; use color_eyre::Result; -use gtk::prelude::*; +use gtk::{prelude::*, PackDirection}; use gtk::{IconTheme, MenuBar}; use interface::TrayMenu; use serde::Deserialize; @@ -18,10 +18,28 @@ use tokio::sync::mpsc; #[derive(Debug, Deserialize, Clone)] pub struct TrayModule { + #[serde(default, deserialize_with = "deserialize_orientation")] + pub direction: Option, #[serde(flatten)] pub common: Option, } +fn deserialize_orientation<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Option::::deserialize(deserializer)?; + value + .map(|v| match v.as_str() { + "left_to_right" => Ok(PackDirection::Ltr), + "right_to_left" => Ok(PackDirection::Rtl), + "top_to_bottom" => Ok(PackDirection::Ttb), + "bottom_to_top" => Ok(PackDirection::Btt), + _ => Err(serde::de::Error::custom("invalid value for orientation")), + }) + .transpose() +} + impl Module for TrayModule { type SendMessage = NotifierItemMessage; type ReceiveMessage = NotifierItemCommand; @@ -70,6 +88,17 @@ impl Module for TrayModule { ) -> Result> { let container = MenuBar::new(); + let direction = self.direction.unwrap_or( + if info.bar_position.get_orientation() == gtk::Orientation::Vertical { + PackDirection::Ttb + } else { + PackDirection::Ltr + }, + ); + + container.set_pack_direction(direction); + container.set_child_pack_direction(direction); + { let container = container.clone(); let mut menus = HashMap::new(); From 6fe9c541347b7bdd69e3d735f07a17a5d4b124ca Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Sun, 18 Feb 2024 00:41:16 +0000 Subject: [PATCH 02/27] fix(clipboard): unable to paste large images into xwayland Thanks to a nightly clippy warning which found I wasn't using `file.write` correctly. Fixes #86. --- src/clients/wayland/wlr_data_control/mod.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/clients/wayland/wlr_data_control/mod.rs b/src/clients/wayland/wlr_data_control/mod.rs index 8c40a84..1cf76a5 100644 --- a/src/clients/wayland/wlr_data_control/mod.rs +++ b/src/clients/wayland/wlr_data_control/mod.rs @@ -179,6 +179,9 @@ impl Environment { MimeTypeCategory::Image => { let mut bytes = vec![]; file.read_to_end(&mut bytes)?; + + debug!("Read bytes: {}", bytes.len()); + let bytes = Bytes::from(&bytes); ClipboardValue::Image(bytes) @@ -234,6 +237,8 @@ impl DataControlDeviceHandler for Environment { return; }; + debug!("Receiving mime type: {}", mime_type.value); + if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) { let offer_clone = cur_offer.offer.clone(); @@ -331,9 +336,9 @@ impl DataControlSourceHandler for Environment { let pipe_size = set_pipe_size(fd.as_raw_fd(), bytes.len()) .expect("Failed to increase pipe size"); - let mut file = File::from(fd.try_clone().expect("Failed to clone fd")); + let mut file = File::from(fd.try_clone().expect("to be able to clone")); - trace!("Num bytes: {}", bytes.len()); + debug!("Writing {} bytes", bytes.len()); let mut events = (0..16).map(|_| EpollEvent::empty()).collect::>(); let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0); @@ -347,20 +352,23 @@ impl DataControlSourceHandler for Environment { while !bytes.is_empty() { let chunk = &bytes[..min(pipe_size as usize, bytes.len())]; - trace!("Writing {} bytes ({} remain)", chunk.len(), bytes.len()); - epoll_fd .wait(&mut events, 100) .expect("Failed to wait to epoll"); match file.write(chunk) { - Ok(_) => bytes = &bytes[chunk.len()..], + Ok(written) => { + trace!("Wrote {} bytes ({} remain)", written, bytes.len()); + bytes = &bytes[written..]; + } Err(err) => { error!("{err:?}"); break; } } } + + debug!("Done writing"); } else { error!("Failed to find source"); } @@ -388,7 +396,7 @@ impl DataControlSourceHandler for Environment { /// If the requested size is larger than the kernel max (normally 1MB), /// it will be clamped at this. /// -/// Returns the new size if succeeded +/// Returns the new size if succeeded. fn set_pipe_size(fd: RawFd, size: usize) -> io::Result { // clamp size at kernel max let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size") From a55ba8c523ff19fa607a31bac589a55b48db39ad Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Sun, 18 Feb 2024 14:54:17 +0000 Subject: [PATCH 03/27] refactor: rename `get_orientation` method to `orientation` --- src/bar.rs | 6 +++--- src/config/impl.rs | 2 +- src/modules/custom/mod.rs | 4 ++-- src/modules/focused.rs | 2 +- src/modules/launcher/item.rs | 4 +--- src/modules/launcher/mod.rs | 2 +- src/modules/sysinfo.rs | 2 +- src/modules/tray/mod.rs | 2 +- src/modules/workspaces.rs | 2 +- 9 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/bar.rs b/src/bar.rs index d15bd73..27af61a 100644 --- a/src/bar.rs +++ b/src/bar.rs @@ -60,7 +60,7 @@ impl Bar { window.set_widget_name(&name); let position = config.position; - let orientation = position.get_orientation(); + let orientation = position.orientation(); let content = gtk::Box::builder() .orientation(orientation) @@ -187,7 +187,7 @@ impl Bar { win.set_layer_shell_margin(gtk_layer_shell::Edge::Left, margin.left); win.set_layer_shell_margin(gtk_layer_shell::Edge::Right, margin.right); - let bar_orientation = position.get_orientation(); + let bar_orientation = position.orientation(); win.set_anchor( gtk_layer_shell::Edge::Top, @@ -351,7 +351,7 @@ fn add_modules( ironbar: &Rc, popup: &Rc>, ) -> Result<()> { - let orientation = info.bar_position.get_orientation(); + let orientation = info.bar_position.orientation(); macro_rules! add_module { ($module:expr, $id:expr) => {{ diff --git a/src/config/impl.rs b/src/config/impl.rs index c60e292..7b9e65d 100644 --- a/src/config/impl.rs +++ b/src/config/impl.rs @@ -38,7 +38,7 @@ impl<'de> Deserialize<'de> for MonitorConfig { impl BarPosition { /// Gets the orientation the bar and widgets should use /// based on this position. - pub fn get_orientation(self) -> Orientation { + pub fn orientation(self) -> Orientation { if self == Self::Top || self == Self::Bottom { Orientation::Horizontal } else { diff --git a/src/modules/custom/mod.rs b/src/modules/custom/mod.rs index 6ec7027..b6e97b9 100644 --- a/src/modules/custom/mod.rs +++ b/src/modules/custom/mod.rs @@ -194,7 +194,7 @@ impl Module for CustomModule { context: WidgetContext, info: &ModuleInfo, ) -> Result> { - let orientation = info.bar_position.get_orientation(); + let orientation = info.bar_position.orientation(); let container = gtk::Box::builder().orientation(orientation).build(); let popup_buttons = Rc::new(RefCell::new(Vec::new())); @@ -236,7 +236,7 @@ impl Module for CustomModule { if let Some(popup) = self.popup { let custom_context = CustomWidgetContext { tx: &tx, - bar_orientation: info.bar_position.get_orientation(), + bar_orientation: info.bar_position.orientation(), icon_theme: info.icon_theme, popup_buttons: Rc::new(RefCell::new(vec![])), }; diff --git a/src/modules/focused.rs b/src/modules/focused.rs index ce288e1..8c32c2b 100644 --- a/src/modules/focused.rs +++ b/src/modules/focused.rs @@ -113,7 +113,7 @@ impl Module for FocusedModule { ) -> Result> { let icon_theme = info.icon_theme; - let container = gtk::Box::new(info.bar_position.get_orientation(), 5); + let container = gtk::Box::new(info.bar_position.orientation(), 5); let icon = gtk::Image::new(); if self.show_icon { diff --git a/src/modules/launcher/item.rs b/src/modules/launcher/item.rs index d312cd8..ba4407d 100644 --- a/src/modules/launcher/item.rs +++ b/src/modules/launcher/item.rs @@ -225,9 +225,7 @@ impl ItemButton { try_send!( tx, - ModuleUpdateEvent::OpenPopupAt( - button.geometry(bar_position.get_orientation()) - ) + ModuleUpdateEvent::OpenPopupAt(button.geometry(bar_position.orientation())) ); } else { try_send!(tx, ModuleUpdateEvent::ClosePopup); diff --git a/src/modules/launcher/mod.rs b/src/modules/launcher/mod.rs index c231cc8..41a7931 100644 --- a/src/modules/launcher/mod.rs +++ b/src/modules/launcher/mod.rs @@ -301,7 +301,7 @@ impl Module for LauncherModule { ) -> crate::Result> { let icon_theme = info.icon_theme; - let container = gtk::Box::new(info.bar_position.get_orientation(), 0); + let container = gtk::Box::new(info.bar_position.orientation(), 0); { let container = container.clone(); diff --git a/src/modules/sysinfo.rs b/src/modules/sysinfo.rs index 420d923..c815110 100644 --- a/src/modules/sysinfo.rs +++ b/src/modules/sysinfo.rs @@ -188,7 +188,7 @@ impl Module for SysInfoModule { ) -> Result> { let re = Regex::new(r"\{([^}]+)}")?; - let container = gtk::Box::new(info.bar_position.get_orientation(), 10); + let container = gtk::Box::new(info.bar_position.orientation(), 10); let mut labels = Vec::new(); diff --git a/src/modules/tray/mod.rs b/src/modules/tray/mod.rs index 5231e09..cf4a2ff 100644 --- a/src/modules/tray/mod.rs +++ b/src/modules/tray/mod.rs @@ -89,7 +89,7 @@ impl Module for TrayModule { let container = MenuBar::new(); let direction = self.direction.unwrap_or( - if info.bar_position.get_orientation() == gtk::Orientation::Vertical { + if info.bar_position.orientation() == gtk::Orientation::Vertical { PackDirection::Ttb } else { PackDirection::Ltr diff --git a/src/modules/workspaces.rs b/src/modules/workspaces.rs index 2e9c4ee..40c30c5 100644 --- a/src/modules/workspaces.rs +++ b/src/modules/workspaces.rs @@ -189,7 +189,7 @@ impl Module for WorkspacesModule { context: WidgetContext, info: &ModuleInfo, ) -> Result> { - let container = gtk::Box::new(info.bar_position.get_orientation(), 0); + let container = gtk::Box::new(info.bar_position.orientation(), 0); let name_map = self.name_map.clone().unwrap_or_default(); let favs = self.favorites.clone(); From a10466e7e9dafd29e80994eccccdd398e9434b95 Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Sun, 18 Feb 2024 14:55:29 +0000 Subject: [PATCH 04/27] fix(popup): re-position on resize due to content change --- src/bar.rs | 11 ++--- src/ipc/server.rs | 15 +++--- src/modules/mod.rs | 23 ++------- src/popup.rs | 120 ++++++++++++++++++++++++++++++++------------- 4 files changed, 102 insertions(+), 67 deletions(-) diff --git a/src/bar.rs b/src/bar.rs index 27af61a..49f820a 100644 --- a/src/bar.rs +++ b/src/bar.rs @@ -10,7 +10,6 @@ use gtk::gdk::Monitor; use gtk::prelude::*; use gtk::{Application, ApplicationWindow, IconTheme, Orientation, Window, WindowType}; use gtk_layer_shell::LayerShell; -use std::cell::RefCell; use std::rc::Rc; use std::time::Duration; use tracing::{debug, info}; @@ -18,7 +17,7 @@ use tracing::{debug, info}; #[derive(Debug, Clone)] enum Inner { New { config: Option }, - Loaded { popup: Rc> }, + Loaded { popup: Rc }, } #[derive(Debug, Clone)] @@ -269,7 +268,7 @@ impl Bar { // popup ignores module location so can bodge this for now let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap); - let popup = Rc::new(RefCell::new(popup)); + let popup = Rc::new(popup); if let Some(modules) = config.start { let info = info!(ModuleLocation::Left); @@ -315,7 +314,7 @@ impl Bar { &self.monitor_name } - pub fn popup(&self) -> Rc> { + pub fn popup(&self) -> Rc { match &self.inner { Inner::New { .. } => { panic!("Attempted to get popup of uninitialized bar. This is a serious bug!") @@ -339,7 +338,7 @@ fn create_container(name: &str, orientation: Orientation) -> gtk::Box { #[derive(Debug)] struct BarLoadResult { - popup: Rc>, + popup: Rc, } /// Adds modules into a provided GTK box, @@ -349,7 +348,7 @@ fn add_modules( modules: Vec, info: &ModuleInfo, ironbar: &Rc, - popup: &Rc>, + popup: &Rc, ) -> Result<()> { let orientation = info.bar_position.orientation(); diff --git a/src/ipc/server.rs b/src/ipc/server.rs index 4d30ee4..931a4fa 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -167,13 +167,13 @@ impl Ipc { match bar { Some(bar) => { let popup = bar.popup(); - let current_widget = popup.borrow().current_widget(); + let current_widget = popup.current_widget(); - popup.borrow_mut().hide(); + popup.hide(); let data = popup - .borrow() .cache + .borrow() .iter() .find(|(_, value)| value.name == name) .map(|(id, value)| (*id, value.content.buttons.first().cloned())); @@ -181,7 +181,6 @@ impl Ipc { match data { Some((id, Some(button))) if current_widget != Some(id) => { let button_id = button.popup_id(); - let mut popup = popup.borrow_mut(); if popup.is_visible() { popup.hide(); @@ -207,11 +206,11 @@ impl Ipc { let popup = bar.popup(); // only one popup per bar, so hide if open for another widget - popup.borrow_mut().hide(); + popup.hide(); let data = popup - .borrow() .cache + .borrow() .iter() .find(|(_, value)| value.name == name) .map(|(id, value)| (*id, value.content.buttons.first().cloned())); @@ -219,7 +218,7 @@ impl Ipc { match data { Some((id, Some(button))) => { let button_id = button.popup_id(); - popup.borrow_mut().show(id, button_id); + popup.show(id, button_id); Response::Ok } @@ -236,7 +235,7 @@ impl Ipc { match bar { Some(bar) => { let popup = bar.popup(); - popup.borrow_mut().hide(); + popup.hide(); Response::Ok } diff --git a/src/modules/mod.rs b/src/modules/mod.rs index a293d0f..102b4bc 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -1,4 +1,3 @@ -use std::cell::RefCell; use std::fmt::Debug; use std::rc::Rc; use std::sync::Arc; @@ -215,7 +214,7 @@ pub fn create_module( ironbar: Rc, name: Option, info: &ModuleInfo, - popup: &Rc>, + popup: &Rc, ) -> Result> where TModule: Module, @@ -251,7 +250,7 @@ where .style_context() .add_class(&format!("popup-{module_name}")); - register_popup_content(popup, id, instance_name, popup_content); + popup.register_content(id, instance_name, popup_content); } setup_receiver(tx, ui_rx, popup.clone(), module_name, id); @@ -259,16 +258,6 @@ where Ok(module_parts) } -/// Registers the popup content with the popup. -fn register_popup_content( - popup: &Rc>, - id: usize, - name: String, - popup_content: ModulePopupParts, -) { - popup.borrow_mut().register_content(id, name, popup_content); -} - /// Sets up the bridge channel receiver /// to pick up events from the controller, widget or popup. /// @@ -277,7 +266,7 @@ fn register_popup_content( fn setup_receiver( tx: broadcast::Sender, rx: mpsc::Receiver>, - popup: Rc>, + popup: Rc, name: &'static str, id: usize, ) where @@ -294,7 +283,6 @@ fn setup_receiver( } ModuleUpdateEvent::TogglePopup(button_id) => { debug!("Toggling popup for {} [#{}]", name, id); - let mut popup = popup.borrow_mut(); if popup.is_visible() { popup.hide(); } else { @@ -309,8 +297,6 @@ fn setup_receiver( } ModuleUpdateEvent::OpenPopup(button_id) => { debug!("Opening popup for {} [#{}]", name, id); - - let mut popup = popup.borrow_mut(); popup.hide(); popup.show(id, button_id); @@ -324,7 +310,6 @@ fn setup_receiver( ModuleUpdateEvent::OpenPopupAt(geometry) => { debug!("Opening popup for {} [#{}]", name, id); - let mut popup = popup.borrow_mut(); popup.hide(); popup.show_at(id, geometry); @@ -336,8 +321,6 @@ fn setup_receiver( } ModuleUpdateEvent::ClosePopup => { debug!("Closing popup for {} [#{}]", name, id); - - let mut popup = popup.borrow_mut(); popup.hide(); } } diff --git a/src/popup.rs b/src/popup.rs index c7756f9..cb1a536 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -1,11 +1,13 @@ use glib::Propagation; +use std::cell::RefCell; use std::collections::HashMap; +use std::rc::Rc; use gtk::gdk::Monitor; use gtk::prelude::*; -use gtk::{ApplicationWindow, Orientation}; +use gtk::{ApplicationWindow, Button, Orientation}; use gtk_layer_shell::LayerShell; -use tracing::debug; +use tracing::{debug, trace}; use crate::config::BarPosition; use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry}; @@ -21,10 +23,10 @@ pub struct PopupCacheValue { #[derive(Debug, Clone)] pub struct Popup { pub window: ApplicationWindow, - pub cache: HashMap, + pub cache: Rc>>, monitor: Monitor, pos: BarPosition, - current_widget: Option, + current_widget: Rc>>, } impl Popup { @@ -33,7 +35,7 @@ impl Popup { /// and an empty `gtk::Box` container. pub fn new(module_info: &ModuleInfo, gap: i32) -> Self { let pos = module_info.bar_position; - let orientation = pos.get_orientation(); + let orientation = pos.orientation(); let win = ApplicationWindow::builder() .application(module_info.app) @@ -104,14 +106,14 @@ impl Popup { Self { window: win, - cache: HashMap::new(), + cache: Rc::new(RefCell::new(HashMap::new())), monitor: module_info.monitor.clone(), pos, - current_widget: None, + current_widget: Rc::new(RefCell::new(None)), } } - pub fn register_content(&mut self, key: usize, name: String, content: ModulePopupParts) { + pub fn register_content(&self, key: usize, name: String, content: ModulePopupParts) { debug!("Registered popup content for #{}", key); for button in &content.buttons { @@ -119,45 +121,94 @@ impl Popup { button.set_tag("popup-id", id); } - self.cache.insert(key, PopupCacheValue { name, content }); + let orientation = self.pos.orientation(); + let monitor = self.monitor.clone(); + let window = self.window.clone(); + + let current_widget = self.current_widget.clone(); + let cache = self.cache.clone(); + + content + .container + .connect_size_allocate(move |container, rect| { + if container.is_visible() { + trace!("Resized: {}x{}", rect.width(), rect.height()); + + if let Some((widget_id, button_id)) = *current_widget.borrow() { + if let Some(PopupCacheValue { content, .. }) = + cache.borrow().get(&widget_id) + { + Self::set_position( + &content.buttons, + button_id, + orientation, + &monitor, + &window, + ); + } + } + } + }); + + self.cache + .borrow_mut() + .insert(key, PopupCacheValue { name, content }); } - pub fn show(&mut self, widget_id: usize, button_id: usize) { + pub fn show(&self, widget_id: usize, button_id: usize) { self.clear_window(); - if let Some(PopupCacheValue { content, .. }) = self.cache.get(&widget_id) { - self.current_widget = Some(widget_id); + if let Some(PopupCacheValue { content, .. }) = self.cache.borrow().get(&widget_id) { + *self.current_widget.borrow_mut() = Some((widget_id, button_id)); content.container.style_context().add_class("popup"); self.window.add(&content.container); self.window.show(); - let button = content - .buttons - .iter() - .find(|b| b.popup_id() == button_id) - .expect("to find valid button"); - - let orientation = self.pos.get_orientation(); - let geometry = button.geometry(orientation); - - self.set_pos(geometry); + Self::set_position( + &content.buttons, + button_id, + self.pos.orientation(), + &self.monitor, + &self.window, + ); } } pub fn show_at(&self, widget_id: usize, geometry: WidgetGeometry) { self.clear_window(); - if let Some(PopupCacheValue { content, .. }) = self.cache.get(&widget_id) { + if let Some(PopupCacheValue { content, .. }) = self.cache.borrow().get(&widget_id) { content.container.style_context().add_class("popup"); self.window.add(&content.container); self.window.show(); - self.set_pos(geometry); + Self::set_pos( + geometry, + self.pos.orientation(), + &self.monitor, + &self.window, + ); } } + fn set_position( + buttons: &[Button], + button_id: usize, + orientation: Orientation, + monitor: &Monitor, + window: &ApplicationWindow, + ) { + let button = buttons + .iter() + .find(|b| b.popup_id() == button_id) + .expect("to find valid button"); + + let geometry = button.geometry(orientation); + Self::set_pos(geometry, orientation, monitor, window); + } + fn clear_window(&self) { let children = self.window.children(); for child in children { @@ -166,8 +217,8 @@ impl Popup { } /// Hides the popover - pub fn hide(&mut self) { - self.current_widget = None; + pub fn hide(&self) { + *self.current_widget.borrow_mut() = None; self.window.hide(); } @@ -177,22 +228,25 @@ impl Popup { } pub fn current_widget(&self) -> Option { - self.current_widget + self.current_widget.borrow().map(|w| w.0) } /// Sets the popup's X/Y position relative to the left or border of the screen /// (depending on orientation). - fn set_pos(&self, geometry: WidgetGeometry) { - let orientation = self.pos.get_orientation(); - - let mon_workarea = self.monitor.workarea(); + fn set_pos( + geometry: WidgetGeometry, + orientation: Orientation, + monitor: &Monitor, + window: &ApplicationWindow, + ) { + let mon_workarea = monitor.workarea(); let screen_size = if orientation == Orientation::Horizontal { mon_workarea.width() } else { mon_workarea.height() }; - let (popup_width, popup_height) = self.window.size(); + let (popup_width, popup_height) = window.size(); let popup_size = if orientation == Orientation::Horizontal { popup_width } else { @@ -217,6 +271,6 @@ impl Popup { gtk_layer_shell::Edge::Top }; - self.window.set_layer_shell_margin(edge, offset as i32); + window.set_layer_shell_margin(edge, offset as i32); } } From 91c9556bb2fad06bc7dfe26f89357db36ed49e92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 14:07:29 +0000 Subject: [PATCH 05/27] build(deps): bump clap from 4.5.0 to 4.5.1 Bumps [clap](https://github.com/clap-rs/clap) from 4.5.0 to 4.5.1. - [Release notes](https://github.com/clap-rs/clap/releases) - [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md) - [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.0...clap_complete-v4.5.1) --- updated-dependencies: - dependency-name: clap dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3628363..b9a0fc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,9 +444,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.0" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" +checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" dependencies = [ "clap_builder", "clap_derive", @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.0" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" +checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" dependencies = [ "anstream", "anstyle", diff --git a/Cargo.toml b/Cargo.toml index 2a49691..71f9b61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ ctrlc = "3.4.2" cfg-if = "1.0.0" # cli -clap = { version = "4.5.0", optional = true, features = ["derive"] } +clap = { version = "4.5.1", optional = true, features = ["derive"] } # ipc serde_json = { version = "1.0.113", optional = true } From 02fd889044b4f283171f00764ff20ea3b9494b0c Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Sun, 25 Feb 2024 21:19:35 +0000 Subject: [PATCH 06/27] ci: rewrite build workflow --- .github/workflows/build.yml | 95 ++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 810c075..d89e8e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,18 +9,24 @@ on: env: CARGO_TERM_COLOR: always + RUSTFLAGS: '-Dwarnings' jobs: - build: + rustfmt: runs-on: ubuntu-latest - + name: 'Formatting' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true + - name: Check formatting + run: cargo fmt --check + + + clippy-base: + runs-on: ubuntu-latest + name: 'Clippy (Base features)' + steps: + - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 name: Cache dependencies @@ -30,20 +36,45 @@ jobs: sudo apt-get update sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev - - name: Check formatting - run: cargo fmt --check + - name: Clippy + run: cargo clippy --no-default-features --features config+json + env: + # Allow some warnings through as we'll never get it perfect in a zero-feature situation + RUSTFLAGS: '-A unused-imports -A unused-variables -A unused-mut -A dead-code' - - name: Clippy (base features) - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --no-default-features --features config+json - - name: Clippy (all features) - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features + clippy-all: + runs-on: ubuntu-latest + name: 'Clippy (All features)' + steps: + - uses: actions/checkout@v4 + + - uses: Swatinem/rust-cache@v2 + name: Cache dependencies + + - name: Install build deps + run: | + sudo apt-get update + sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev + + - name: Clippy + run: cargo clippy --all-targets --all-features + + + build: + name: 'Build & Test' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: Swatinem/rust-cache@v2 + name: Cache dependencies + + - name: Install build deps + run: | + sudo apt-get update + sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev - name: Build run: cargo build --verbose @@ -51,28 +82,4 @@ jobs: - name: Run tests uses: actions-rs/cargo@v1 with: - command: test - - - - build-nix: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - uses: cachix/install-nix-action@v20 - with: - install_url: https://nixos.org/nix/install - extra_nix_config: | - auto-optimise-store = true - experimental-features = nix-command flakes - - - uses: cachix/cachix-action@v12 - with: - name: jakestanger - signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' - - - uses: DeterminateSystems/magic-nix-cache-action@main - - - run: nix build --print-build-logs \ No newline at end of file + command: test \ No newline at end of file From 163a70e690e2a9950c23ef8164dafd762a6a1020 Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Sun, 25 Feb 2024 21:19:49 +0000 Subject: [PATCH 07/27] docs(readme): update nix caching info --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 08a34d8..5e9afb3 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,8 @@ A flake is included with the repo which can be used with Home Manager. -There is a Cachix cache available at `https://app.cachix.org/cache/jakestanger`. +CI builds are automatically cached by Garnix. +You can use their binary cache by following the steps [here](https://garnix.io/docs/caching). ### Source From 5134534346c446a5d705d2645bcd62400a1ba7a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:36:50 +0000 Subject: [PATCH 08/27] build(deps): bump zbus from 3.15.0 to 3.15.2 Bumps [zbus](https://github.com/dbus2/zbus) from 3.15.0 to 3.15.2. - [Release notes](https://github.com/dbus2/zbus/releases) - [Commits](https://github.com/dbus2/zbus/compare/zbus-3.15.0...zbus-3.15.2) --- updated-dependencies: - dependency-name: zbus dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9a0fc9..7eeccc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3999,9 +3999,9 @@ checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" [[package]] name = "zbus" -version = "3.15.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c45d06ae3b0f9ba1fb2671268b975557d8f5a84bb5ec6e43964f87e763d8bca8" +checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" dependencies = [ "async-broadcast", "async-executor", @@ -4041,9 +4041,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.15.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a1ba45ed0ad344b85a2bb5a1fe9830aed23d67812ea39a586e7d0136439c7d" +checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 71f9b61..b6460f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,7 +130,7 @@ system-tray = { version = "0.1.5", optional = true } # upower upower_dbus = { version = "0.3.2", optional = true } futures-lite = { version = "2.2.0", optional = true } -zbus = { version = "3.15.0", optional = true } +zbus = { version = "3.15.2", optional = true } # workspaces swayipc-async = { version = "2.0.1", optional = true } From 43d1ae528da4a5ea81af0ecb428fe382237c6947 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:37:00 +0000 Subject: [PATCH 09/27] build(deps): bump serde_json from 1.0.113 to 1.0.114 Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.113 to 1.0.114. - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.113...v1.0.114) --- updated-dependencies: - dependency-name: serde_json dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9a0fc9..fe46dc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2688,9 +2688,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.113" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", diff --git a/Cargo.toml b/Cargo.toml index 71f9b61..5827acb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,7 @@ cfg-if = "1.0.0" clap = { version = "4.5.1", optional = true, features = ["derive"] } # ipc -serde_json = { version = "1.0.113", optional = true } +serde_json = { version = "1.0.114", optional = true } # http reqwest = { version = "0.11.24", optional = true } From 232c56e411e259bd22ddaf70160fdd6c1aa1a2d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:37:18 +0000 Subject: [PATCH 10/27] build(deps): bump serde from 1.0.196 to 1.0.197 Bumps [serde](https://github.com/serde-rs/serde) from 1.0.196 to 1.0.197. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.196...v1.0.197) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9a0fc9..73b3778 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2668,18 +2668,18 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote 1.0.35", diff --git a/Cargo.toml b/Cargo.toml index 71f9b61..77b1a99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,7 +87,7 @@ tracing-error = "0.2.0" tracing-appender = "0.2.3" strip-ansi-escapes = "0.2.0" color-eyre = "0.6.2" -serde = { version = "1.0.196", features = ["derive"] } +serde = { version = "1.0.197", features = ["derive"] } indexmap = "2.2.3" dirs = "5.0.1" walkdir = "2.4.0" From f2638497fac4f0e350d069857e6e7437cb756669 Mon Sep 17 00:00:00 2001 From: slowsage <84777606+slowsage@users.noreply.github.com> Date: Mon, 26 Feb 2024 13:43:21 -0500 Subject: [PATCH 11/27] fix(launcher): not resolving icon for some apps When using spotify in wayland with hyprland, the spotify icon is not pulled up correctly by the launcher module. The class/app_id is "" while the name/title is "Spotify Premium". This uses item.name as a fallback which ensure spotify icon shows up correctly under wayland. Refs: #228, #146 --- src/modules/launcher/item.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/modules/launcher/item.rs b/src/modules/launcher/item.rs index ba4407d..924b74a 100644 --- a/src/modules/launcher/item.rs +++ b/src/modules/launcher/item.rs @@ -166,8 +166,12 @@ impl ItemButton { if appearance.show_icons { let gtk_image = gtk::Image::new(); - let image = - ImageProvider::parse(&item.app_id.clone(), icon_theme, true, appearance.icon_size); + let input = if item.app_id.is_empty() { + item.name.clone() + } else { + item.app_id.clone() + }; + let image = ImageProvider::parse(&input, icon_theme, true, appearance.icon_size); if let Some(image) = image { button.set_image(Some(>k_image)); button.set_always_show_image(true); From 9558938152856bb2cd8d1ea69335bef24d900a0d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Mar 2024 00:54:00 +0000 Subject: [PATCH 12/27] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'crane': 'github:ipetkov/crane/c798790eabec3e3da48190ae3698ac227aab770c' (2024-01-28) → 'github:ipetkov/crane/2c94ff9a6fbeb9f3ea0107f28688edbe9c81deaa' (2024-02-24) • Updated input 'naersk/nixpkgs': 'github:NixOS/nixpkgs/5ad9903c16126a7d949101687af0aa589b1d7d3d' (2024-01-31) → 'github:NixOS/nixpkgs/ebe6e807793e7c9cc59cf81225fdee1a03413811' (2024-02-29) • Updated input 'nixpkgs': 'github:nixos/nixpkgs/97b17f32362e475016f942bbdfda4a4a72a8a652' (2024-01-29) → 'github:nixos/nixpkgs/9099616b93301d5cf84274b184a3a5ec69e94e08' (2024-02-28) • Updated input 'rust-overlay': 'github:oxalica/rust-overlay/2c993daf3136c6955fd13bfe215d0d4faf6090f1' (2024-01-31) → 'github:oxalica/rust-overlay/72fa0217f76020ad3aeb2dd9dd72490905b23b6f' (2024-02-29) --- flake.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/flake.lock b/flake.lock index 8bcf0de..47e5ffa 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1706473964, - "narHash": "sha256-Fq6xleee/TsX6NbtoRuI96bBuDHMU57PrcK9z1QEKbk=", + "lastModified": 1708794349, + "narHash": "sha256-jX+B1VGHT0ruHHL5RwS8L21R6miBn4B6s9iVyUJsJJY=", "owner": "ipetkov", "repo": "crane", - "rev": "c798790eabec3e3da48190ae3698ac227aab770c", + "rev": "2c94ff9a6fbeb9f3ea0107f28688edbe9c81deaa", "type": "github" }, "original": { @@ -58,11 +58,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1706683685, - "narHash": "sha256-FtPPshEpxH/ewBOsdKBNhlsL2MLEFv1hEnQ19f/bFsQ=", + "lastModified": 1709200309, + "narHash": "sha256-lKdtMbhnBNU1lr978T+wEYet3sfIXXgyiDZNEgx8CV8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5ad9903c16126a7d949101687af0aa589b1d7d3d", + "rev": "ebe6e807793e7c9cc59cf81225fdee1a03413811", "type": "github" }, "original": { @@ -72,11 +72,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1706550542, - "narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=", + "lastModified": 1709150264, + "narHash": "sha256-HofykKuisObPUfj0E9CJVfaMhawXkYx3G8UIFR/XQ38=", "owner": "nixos", "repo": "nixpkgs", - "rev": "97b17f32362e475016f942bbdfda4a4a72a8a652", + "rev": "9099616b93301d5cf84274b184a3a5ec69e94e08", "type": "github" }, "original": { @@ -102,11 +102,11 @@ ] }, "locked": { - "lastModified": 1706667075, - "narHash": "sha256-KBI5jcOWh9nsOFWj2SRq7vj+fPDf8Do8ceL582kFA70=", + "lastModified": 1709172595, + "narHash": "sha256-0oYeE5VkhnPA7YBl+0Utq2cYoHcfsEhSGwraCa27Vs8=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "2c993daf3136c6955fd13bfe215d0d4faf6090f1", + "rev": "72fa0217f76020ad3aeb2dd9dd72490905b23b6f", "type": "github" }, "original": { From 0726393ab365c6e982736e30f5d20541d7c94ce7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 23:28:51 +0000 Subject: [PATCH 13/27] build(deps): bump the cargo group group with 4 updates Bumps the cargo group group with 4 updates: [h2](https://github.com/hyperium/h2), [openssl](https://github.com/sfackler/rust-openssl), [rustix](https://github.com/bytecodealliance/rustix) and [unsafe-libyaml](https://github.com/dtolnay/unsafe-libyaml). Updates `h2` from 0.3.17 to 0.3.24 - [Release notes](https://github.com/hyperium/h2/releases) - [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md) - [Commits](https://github.com/hyperium/h2/compare/v0.3.17...v0.3.24) Updates `openssl` from 0.10.50 to 0.10.64 - [Release notes](https://github.com/sfackler/rust-openssl/releases) - [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.50...openssl-v0.10.64) Updates `rustix` from 0.37.11 to 0.37.27 - [Release notes](https://github.com/bytecodealliance/rustix/releases) - [Commits](https://github.com/bytecodealliance/rustix/compare/v0.37.11...v0.37.27) Updates `unsafe-libyaml` from 0.2.8 to 0.2.10 - [Release notes](https://github.com/dtolnay/unsafe-libyaml/releases) - [Commits](https://github.com/dtolnay/unsafe-libyaml/compare/0.2.8...0.2.10) --- updated-dependencies: - dependency-name: h2 dependency-type: indirect dependency-group: cargo-security-group - dependency-name: openssl dependency-type: indirect dependency-group: cargo-security-group - dependency-name: rustix dependency-type: indirect dependency-group: cargo-security-group - dependency-name: unsafe-libyaml dependency-type: indirect dependency-group: cargo-security-group ... Signed-off-by: dependabot[bot] --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c113143..04f5c38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,7 +168,7 @@ dependencies = [ "log", "parking", "polling 2.7.0", - "rustix 0.37.11", + "rustix 0.37.27", "slab", "socket2 0.4.9", "waker-fn", @@ -206,7 +206,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite 1.13.0", - "rustix 0.37.11", + "rustix 0.37.27", "signal-hook", "windows-sys 0.48.0", ] @@ -1352,9 +1352,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.17" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", @@ -1362,7 +1362,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.2.3", "slab", "tokio", "tokio-util", @@ -1767,9 +1767,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.3.1" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" @@ -2045,11 +2045,11 @@ checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "openssl" -version = "0.10.50" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e30d8bc91859781f0a943411186324d580f2bbeb71b452fe91ae344806af3f1" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "cfg-if", "foreign-types", "libc", @@ -2077,9 +2077,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.85" +version = "0.9.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d3d193fb1488ad46ffe3aaabc912cc931d02ee8518fe2959aea8ef52718b0c0" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" dependencies = [ "cc", "libc", @@ -2555,15 +2555,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.11" +version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys 0.3.1", + "linux-raw-sys 0.3.8", "windows-sys 0.48.0", ] @@ -3050,7 +3050,7 @@ dependencies = [ "cfg-if", "fastrand 1.9.0", "redox_syscall 0.3.5", - "rustix 0.37.11", + "rustix 0.37.27", "windows-sys 0.45.0", ] @@ -3421,9 +3421,9 @@ dependencies = [ [[package]] name = "unsafe-libyaml" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" +checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" [[package]] name = "upower_dbus" From 07b2b6a4bbaa9072977e9f1af59389f10735eb97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 14:57:45 +0000 Subject: [PATCH 14/27] build(deps): bump walkdir from 2.4.0 to 2.5.0 Bumps [walkdir](https://github.com/BurntSushi/walkdir) from 2.4.0 to 2.5.0. - [Commits](https://github.com/BurntSushi/walkdir/compare/2.4.0...2.5.0) --- updated-dependencies: - dependency-name: walkdir dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04f5c38..9d8f00c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3505,9 +3505,9 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", diff --git a/Cargo.toml b/Cargo.toml index 3fa4340..9554a58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,7 +90,7 @@ color-eyre = "0.6.2" serde = { version = "1.0.197", features = ["derive"] } indexmap = "2.2.3" dirs = "5.0.1" -walkdir = "2.4.0" +walkdir = "2.5.0" notify = { version = "6.1.1", default-features = false } wayland-client = "0.31.1" wayland-protocols = { version = "0.31.0", features = ["unstable", "client"] } From 4215478608c4c665af1d1857847978f6f883d029 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 14:58:09 +0000 Subject: [PATCH 15/27] build(deps): bump indexmap from 2.2.3 to 2.2.5 Bumps [indexmap](https://github.com/indexmap-rs/indexmap) from 2.2.3 to 2.2.5. - [Changelog](https://github.com/indexmap-rs/indexmap/blob/master/RELEASES.md) - [Commits](https://github.com/indexmap-rs/indexmap/compare/2.2.3...2.2.5) --- updated-dependencies: - dependency-name: indexmap dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04f5c38..ee8bb95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1362,7 +1362,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.3", + "indexmap 2.2.5", "slab", "tokio", "tokio-util", @@ -1579,9 +1579,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.3" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown 0.14.1", @@ -1649,7 +1649,7 @@ dependencies = [ "gtk", "gtk-layer-shell", "hyprland", - "indexmap 2.2.3", + "indexmap 2.2.5", "mpd-utils", "mpris", "nix 0.27.1", @@ -3228,7 +3228,7 @@ version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ - "indexmap 2.2.3", + "indexmap 2.2.5", "serde", "serde_spanned", "toml_datetime", @@ -3241,7 +3241,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.2.3", + "indexmap 2.2.5", "toml_datetime", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 3fa4340..0ab0637 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,7 +88,7 @@ tracing-appender = "0.2.3" strip-ansi-escapes = "0.2.0" color-eyre = "0.6.2" serde = { version = "1.0.197", features = ["derive"] } -indexmap = "2.2.3" +indexmap = "2.2.5" dirs = "5.0.1" walkdir = "2.4.0" notify = { version = "6.1.1", default-features = false } From a70956bb3b17f559fda1fdca444e271ae9d3c4cd Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Sat, 1 Apr 2023 13:07:47 +0100 Subject: [PATCH 16/27] feat: add new volume module --- Cargo.lock | 39 +++ Cargo.toml | 9 +- docs/Compiling.md | 7 + docs/_Sidebar.md | 1 + docs/modules/Volume.md | 128 +++++++++ examples/config.corn | 12 +- examples/style.css | 5 + src/bar.rs | 2 + src/clients/mod.rs | 11 + src/clients/volume/mod.rs | 312 ++++++++++++++++++++++ src/clients/volume/sink.rs | 175 +++++++++++++ src/clients/volume/sink_input.rs | 148 +++++++++++ src/config/mod.rs | 4 + src/macros.rs | 7 + src/modules/mod.rs | 2 + src/modules/volume.rs | 427 +++++++++++++++++++++++++++++++ 16 files changed, 1287 insertions(+), 2 deletions(-) create mode 100644 docs/modules/Volume.md create mode 100644 src/clients/volume/mod.rs create mode 100644 src/clients/volume/sink.rs create mode 100644 src/clients/volume/sink_input.rs create mode 100644 src/modules/volume.rs diff --git a/Cargo.lock b/Cargo.lock index 872dd8c..ff9fd24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1650,6 +1650,7 @@ dependencies = [ "gtk-layer-shell", "hyprland", "indexmap 2.2.5", + "libpulse-binding", "mpd-utils", "mpris", "nix 0.27.1", @@ -1756,6 +1757,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "libpulse-binding" +version = "2.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff" +dependencies = [ + "bitflags 1.3.2", + "libc", + "libpulse-sys", + "num-derive", + "num-traits", + "winapi", +] + +[[package]] +name = "libpulse-sys" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b" +dependencies = [ + "libc", + "num-derive", + "num-traits", + "pkg-config", + "winapi", +] + [[package]] name = "link-cplusplus" version = "1.0.8" @@ -2009,6 +2037,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote 1.0.35", + "syn 1.0.109", +] + [[package]] name = "num-traits" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index fbd403d..ad93883 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ default = [ "sys_info", "tray", "upower", + "volume", "workspaces+all" ] @@ -62,6 +63,8 @@ tray = ["system-tray"] upower = ["upower_dbus", "zbus", "futures-lite"] +volume = ["libpulse-binding"] + workspaces = ["futures-util"] "workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"] "workspaces+sway" = ["workspaces", "swayipc-async"] @@ -132,6 +135,10 @@ upower_dbus = { version = "0.3.2", optional = true } futures-lite = { version = "2.2.0", optional = true } zbus = { version = "3.15.2", optional = true } +# volume +libpulse-binding = { version = "2.28.1", optional = true } +# libpulse-glib-binding = { version = "2.27.1", optional = true } + # workspaces swayipc-async = { version = "2.0.1", optional = true } hyprland = { version = "0.3.13", features = ["silent"], optional = true } @@ -140,4 +147,4 @@ futures-util = { version = "0.3.30", optional = true } # shared regex = { version = "1.10.3", default-features = false, features = [ "std", -], optional = true } # music, sys_info \ No newline at end of file +], optional = true } # music, sys_info diff --git a/docs/Compiling.md b/docs/Compiling.md index 236b295..30c69df 100644 --- a/docs/Compiling.md +++ b/docs/Compiling.md @@ -20,6 +20,8 @@ You also need rust; only the latest stable version is supported. pacman -S gtk3 gtk-layer-shell # for http support pacman -S openssl +# for volume support +pacman -S libpulse ``` ### Ubuntu/Debian @@ -28,6 +30,8 @@ pacman -S openssl apt install build-essential libgtk-3-dev libgtk-layer-shell-dev # for http support apt install libssl-dev +# for volume support +apt install libpulse-dev ``` ### Fedora @@ -36,6 +40,8 @@ apt install libssl-dev dnf install gtk3-devel gtk-layer-shell-devel # for http support dnf install openssl-devel +# for volume support +dnf install libpulseaudio-devel ``` ## Features @@ -81,6 +87,7 @@ cargo build --release --no-default-features \ | sys_info | Enables the `sys_info` module. | | tray | Enables the `tray` module. | | upower | Enables the `upower` module. | +| volume | Enables the `volume` module. | | workspaces+all | Enables the `workspaces` module with support for all compositors. | | workspaces+sway | Enables the `workspaces` module with support for Sway. | | workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. | diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index 2553fe8..cbe5e0a 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -34,4 +34,5 @@ - [Sys_Info](sys-info) - [Tray](tray) - [Upower](upower) +- [Volume](volume) - [Workspaces](workspaces) diff --git a/docs/modules/Volume.md b/docs/modules/Volume.md new file mode 100644 index 0000000..4385646 --- /dev/null +++ b/docs/modules/Volume.md @@ -0,0 +1,128 @@ +Displays the current volume level. +Clicking on the widget opens a volume mixer, which allows you to change the device output level, +the default playback device, and control application volume levels individually. + +This requires PulseAudio to function (`pipewire-pulse` is supported). + +TODO: Screenshot + +## Configuration + +> Type: `volume` + +| Name | Type | Default | Description | +|-----------------------|----------|------------------------|----------------------------------------------------------------------------------------------------------------| +| `format` | `string` | `{icon} {percentage}%` | Format string to use for the widget button label. | +| `max_volume` | `float` | `100` | Maximum value to allow volume sliders to reach. Pulse supports values > 100 but this may result in distortion. | +| `icons.volume_high` | `string` | `󰕾` | Icon to show for high volume levels. | +| `icons.volume_medium` | `string` | `󰖀` | Icon to show for medium volume levels. | +| `icons.volume_low` | `string` | `󰕿` | Icon to show for low volume levels. | +| `icons.muted` | `string` | `󰝟` | Icon to show for muted outputs. | + +
+JSON + +```json +{ + "end": [ + { + "type": "volume", + "format": "{icon} {percentage}%", + "max_volume": 100, + "icons": { + "volume_high": "󰕾", + "volume_medium": "󰖀", + "volume_low": "󰕿", + "muted": "󰝟" + } + } + ] +} + +``` + +
+ +
+TOML + +```toml +[[end]] +type = "volume" +format = "{icon} {percentage}%" +max_volume = 100 + +[[end.icons]] +volume_high = "󰕾" +volume_medium = "󰖀" +volume_low = "󰕿" +muted = "󰝟" +``` + +
+ +
+YAML + +```yaml +end: + - type: "volume" + format: "{icon} {percentage}%" + max_volume: 100 + icons: + volume_high: "󰕾" + volume_medium: "󰖀" + volume_low: "󰕿" + muted: "󰝟" +``` + +
+ +
+Corn + +```corn +{ + end = [ + { + type = "volume" + format = "{icon} {percentage}%" + max_volume = 100 + icons.volume_high = "󰕾" + icons.volume_medium = "󰖀" + icons.volume_low = "󰕿" + icons.muted = "󰝟" + } + ] +} +``` + +
+ +### Formatting Tokens + +The following tokens can be used in the `format` config option: + +| Token | Description | +|----------------|-------------------------------------------| +| `{percentage}` | The active device volume percentage. | +| `{icon}` | The icon representing the current volume. | +| `{name}` | The active device name. | + +## Styling + +| Selector | Description | +|----------------------------------------------|----------------------------------------------------| +| `.volume` | Volume widget button. | +| `.popup-volume` | Volume popup box. | +| `.popup-volume .device-box` | Box for the device volume controls. | +| `.popup-volume .device-box .device-selector` | Default device dropdown selector. | +| `.popup-volume .device-box .slider` | Device volume slider. | +| `.popup-volume .device-box .btn-mute` | Device volume mute toggle button. | +| `.popup-volume .apps-box` | Parent box for the application volume controls. | +| `.popup-volume .apps-box .app-box` | Box for an individual application volume controls. | +| `.popup-volume .apps-box .app-box .title` | Name of the application playback stream. | +| `.popup-volume .apps-box .app-box .slider` | Application volume slider. | +| `.popup-volume .apps-box .app-box .btn-mute` | Application volume mute toggle button. | + +For more information on styling, please see the [styling guide](styling-guide). \ No newline at end of file diff --git a/examples/config.corn b/examples/config.corn index 4cc47da..718e830 100644 --- a/examples/config.corn +++ b/examples/config.corn @@ -67,6 +67,16 @@ let { $clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 } + $volume = { + type = "volume" + format = "{icon} {volume}%" + max_volume = 100 + icons.volume_high = "󰕾" + icons.volume_medium = "󰖀" + icons.volume_low = "󰕿" + icons.muted = "󰝟" + } + $label = { type = "label" label = "random num: {{500:echo FIXME}}" } // -- begin custom -- @@ -100,7 +110,7 @@ let { // -- end custom -- $left = [ $workspaces $launcher $label ] - $right = [ $mpd_local $mpd_server $phone_battery $sys_info $clipboard $power_menu $clock ] + $right = [ $mpd_local $mpd_server $phone_battery $sys_info $volume $clipboard $power_menu $clock ] } in { anchor_to_edges = true diff --git a/examples/style.css b/examples/style.css index c766da8..8887538 100644 --- a/examples/style.css +++ b/examples/style.css @@ -174,6 +174,11 @@ scale trough { margin-left: 10px; } +/* -- volume -- */ + +.popup-volume .device-box { + border-right: 1px solid @color_border; +} /* -- workspaces -- */ diff --git a/src/bar.rs b/src/bar.rs index 49f820a..0263d39 100644 --- a/src/bar.rs +++ b/src/bar.rs @@ -392,6 +392,8 @@ fn add_modules( ModuleConfig::Tray(mut module) => add_module!(module, id), #[cfg(feature = "upower")] ModuleConfig::Upower(mut module) => add_module!(module, id), + #[cfg(feature = "volume")] + ModuleConfig::Volume(mut module) => add_module!(module, id), #[cfg(feature = "workspaces")] ModuleConfig::Workspaces(mut module) => add_module!(module, id), } diff --git a/src/clients/mod.rs b/src/clients/mod.rs index f1433bc..8edec61 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -10,6 +10,8 @@ pub mod music; pub mod system_tray; #[cfg(feature = "upower")] pub mod upower; +#[cfg(feature = "volume")] +pub mod volume; pub mod wayland; /// Singleton wrapper consisting of @@ -27,6 +29,8 @@ pub struct Clients { tray: Option>, #[cfg(feature = "upower")] upower: Option>>, + #[cfg(feature = "volume")] + volume: Option>, } impl Clients { @@ -86,6 +90,13 @@ impl Clients { }) .clone() } + + #[cfg(feature = "volume")] + pub fn volume(&mut self) -> Arc { + self.volume + .get_or_insert_with(volume::create_client) + .clone() + } } /// Types implementing this trait diff --git a/src/clients/volume/mod.rs b/src/clients/volume/mod.rs new file mode 100644 index 0000000..7abf28d --- /dev/null +++ b/src/clients/volume/mod.rs @@ -0,0 +1,312 @@ +mod sink; +mod sink_input; + +use crate::{arc_mut, lock, register_client, send, spawn_blocking}; +use libpulse_binding::callbacks::ListResult; +use libpulse_binding::context::introspect::{Introspector, ServerInfo}; +use libpulse_binding::context::subscribe::{Facility, InterestMaskSet, Operation}; +use libpulse_binding::context::{Context, FlagSet, State}; +use libpulse_binding::mainloop::standard::{IterateResult, Mainloop}; +use libpulse_binding::proplist::Proplist; +use libpulse_binding::volume::{ChannelVolumes, Volume}; +use std::fmt::{Debug, Formatter}; +use std::sync::{Arc, Mutex}; +use tokio::sync::broadcast; +use tracing::{debug, error, info, warn}; + +pub use sink::Sink; +pub use sink_input::SinkInput; + +type ArcMutVec = Arc>>; + +#[derive(Debug, Clone)] +pub enum Event { + AddSink(Sink), + UpdateSink(Sink), + RemoveSink(String), + + AddInput(SinkInput), + UpdateInput(SinkInput), + RemoveInput(u32), +} + +#[derive(Debug)] +pub struct Client { + connection: Arc>, + + data: Data, + + tx: broadcast::Sender, + _rx: broadcast::Receiver, +} + +#[derive(Debug, Default, Clone)] +struct Data { + sinks: ArcMutVec, + sink_inputs: ArcMutVec, + + default_sink_name: Arc>>, +} + +pub enum ConnectionState { + Disconnected, + Connected { + context: Arc>, + introspector: Introspector, + }, +} + +impl Debug for ConnectionState { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Disconnected => "Disconnected", + Self::Connected { .. } => "Connected", + } + ) + } +} + +impl Client { + pub fn new() -> Self { + let (tx, rx) = broadcast::channel(32); + + Self { + connection: arc_mut!(ConnectionState::Disconnected), + data: Data::default(), + tx, + _rx: rx, + } + } + + /// Starts the client. + fn run(&self) { + let Some(mut proplist) = Proplist::new() else { + error!("Failed to create PA proplist"); + return; + }; + + if proplist + .set_str("APPLICATION_NAME", "dev.jstanger.ironbar") + .is_err() + { + error!("Failed to update PA proplist"); + } + + let Some(mut mainloop) = Mainloop::new() else { + error!("Failed to create PA mainloop"); + return; + }; + + let Some(context) = Context::new_with_proplist(&mainloop, "Ironbar Context", &proplist) + else { + error!("Failed to create PA context"); + return; + }; + + let context = arc_mut!(context); + + let state_callback = Box::new({ + let context = context.clone(); + let data = self.data.clone(); + let tx = self.tx.clone(); + + move || on_state_change(&context, &data, &tx) + }); + + lock!(context).set_state_callback(Some(state_callback)); + + if let Err(err) = lock!(context).connect(None, FlagSet::NOAUTOSPAWN, None) { + error!("{err:?}"); + } + + let introspector = lock!(context).introspect(); + + { + let mut inner = lock!(self.connection); + *inner = ConnectionState::Connected { + context, + introspector, + }; + } + + loop { + match mainloop.iterate(true) { + IterateResult::Success(_) => {} + IterateResult::Err(err) => error!("{err:?}"), + IterateResult::Quit(_) => break, + } + } + } + + /// Gets an event receiver. + pub fn subscribe(&self) -> broadcast::Receiver { + self.tx.subscribe() + } +} + +/// Creates a new Pulse volume client. +pub fn create_client() -> Arc { + let client = Arc::new(Client::new()); + + { + let client = client.clone(); + spawn_blocking(move || { + client.run(); + }); + } + + client +} + +fn on_state_change(context: &Arc>, data: &Data, tx: &broadcast::Sender) { + let Ok(state) = context.try_lock().map(|lock| lock.get_state()) else { + return; + }; + + match state { + State::Ready => { + info!("connected to server"); + + let introspect = lock!(context).introspect(); + let introspect2 = lock!(context).introspect(); + + introspect.get_sink_info_list({ + let sinks = data.sinks.clone(); + let default_sink = data.default_sink_name.clone(); + + let tx = tx.clone(); + + move |info| match info { + ListResult::Item(_) => sink::add(info, &sinks, &tx), + ListResult::End => { + introspect2.get_server_info({ + let sinks = sinks.clone(); + let default_sink = default_sink.clone(); + let tx = tx.clone(); + + move |info| set_default_sink(info, &sinks, &default_sink, &tx) + }); + } + ListResult::Error => error!("Error while receiving sinks"), + } + }); + + introspect.get_sink_input_info_list({ + let inputs = data.sink_inputs.clone(); + let tx = tx.clone(); + + move |info| sink_input::add(info, &inputs, &tx) + }); + + let subscribe_callback = Box::new({ + let context = context.clone(); + let data = data.clone(); + let tx = tx.clone(); + + move |facility, op, i| on_event(&context, &data, &tx, facility, op, i) + }); + + lock!(context).set_subscribe_callback(Some(subscribe_callback)); + lock!(context).subscribe( + InterestMaskSet::SERVER | InterestMaskSet::SINK_INPUT | InterestMaskSet::SINK, + |_| (), + ); + } + State::Failed => error!("Failed to connect to audio server"), + State::Terminated => error!("Connection to audio server terminated"), + _ => {} + } +} + +fn on_event( + context: &Arc>, + data: &Data, + tx: &broadcast::Sender, + facility: Option, + op: Option, + i: u32, +) { + let (Some(facility), Some(op)) = (facility, op) else { + return; + }; + + match facility { + Facility::Server => on_server_event(context, &data.sinks, &data.default_sink_name, tx), + Facility::Sink => sink::on_event(context, &data.sinks, &data.default_sink_name, tx, op, i), + Facility::SinkInput => sink_input::on_event(context, &data.sink_inputs, tx, op, i), + _ => error!("Received unhandled facility: {facility:?}"), + } +} + +fn on_server_event( + context: &Arc>, + sinks: &ArcMutVec, + default_sink: &Arc>>, + tx: &broadcast::Sender, +) { + lock!(context).introspect().get_server_info({ + let sinks = sinks.clone(); + let default_sink = default_sink.clone(); + let tx = tx.clone(); + + move |info| set_default_sink(info, &sinks, &default_sink, &tx) + }); +} + +fn set_default_sink( + info: &ServerInfo, + sinks: &ArcMutVec, + default_sink: &Arc>>, + tx: &broadcast::Sender, +) { + let default_sink_name = info.default_sink_name.as_ref().map(ToString::to_string); + + if default_sink_name != *lock!(default_sink) { + if let Some(ref default_sink_name) = default_sink_name { + if let Some(sink) = lock!(sinks) + .iter_mut() + .find(|s| s.name.as_str() == default_sink_name.as_str()) + { + sink.active = true; + debug!("Set sink active: {}", sink.name); + send!(tx, Event::UpdateSink(sink.clone())); + } else { + warn!("Couldn't find sink: {}", default_sink_name); + } + } + } + + *lock!(default_sink) = default_sink_name; +} + +/// Converts a Pulse `ChannelVolumes` struct into a single percentage value, +/// representing the average value across all channels. +fn volume_to_percent(volume: ChannelVolumes) -> f64 { + let avg = volume.avg().0; + let base_delta = (Volume::NORMAL.0 - Volume::MUTED.0) as f64 / 100.0; + + ((avg - Volume::MUTED.0) as f64 / base_delta).round() +} + +/// Converts a percentage volume into a Pulse volume value, +/// which can be used for setting channel volumes. +pub fn percent_to_volume(target_percent: f64) -> u32 { + let base_delta = (Volume::NORMAL.0 as f32 - Volume::MUTED.0 as f32) / 100.0; + + if target_percent < 0.0 { + Volume::MUTED.0 + } else if target_percent == 100.0 { + Volume::NORMAL.0 + } else if target_percent >= 150.0 { + (Volume::NORMAL.0 as f32 * 1.5) as u32 + } else if target_percent < 100.0 { + Volume::MUTED.0 + target_percent as u32 * base_delta as u32 + } else { + Volume::NORMAL.0 + (target_percent - 100.0) as u32 * base_delta as u32 + } +} + +register_client!(Client, volume); diff --git a/src/clients/volume/sink.rs b/src/clients/volume/sink.rs new file mode 100644 index 0000000..d08b99e --- /dev/null +++ b/src/clients/volume/sink.rs @@ -0,0 +1,175 @@ +use super::{percent_to_volume, volume_to_percent, ArcMutVec, Client, ConnectionState, Event}; +use crate::{lock, send}; +use libpulse_binding::callbacks::ListResult; +use libpulse_binding::context::introspect::SinkInfo; +use libpulse_binding::context::subscribe::Operation; +use libpulse_binding::context::Context; +use libpulse_binding::def::SinkState; +use std::sync::{mpsc, Arc, Mutex}; +use tokio::sync::broadcast; +use tracing::{debug, error}; + +#[derive(Debug, Clone)] +pub struct Sink { + index: u32, + pub name: String, + pub description: String, + pub volume: f64, + pub muted: bool, + pub active: bool, +} + +impl From<&SinkInfo<'_>> for Sink { + fn from(value: &SinkInfo) -> Self { + Self { + index: value.index, + name: value + .name + .as_ref() + .map(ToString::to_string) + .unwrap_or_default(), + description: value + .description + .as_ref() + .map(ToString::to_string) + .unwrap_or_default(), + muted: value.mute, + volume: volume_to_percent(value.volume), + active: value.state == SinkState::Running, + } + } +} + +impl Client { + pub fn sinks(&self) -> Arc>> { + self.data.sinks.clone() + } + + pub fn set_default_sink(&self, name: &str) { + if let ConnectionState::Connected { context, .. } = &*lock!(self.connection) { + lock!(context).set_default_sink(name, |_| {}); + } + } + + pub fn set_sink_volume(&self, name: &str, volume_percent: f64) { + if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) { + let (tx, rx) = mpsc::channel(); + + introspector.get_sink_info_by_name(name, move |info| { + let ListResult::Item(info) = info else { + return; + }; + send!(tx, info.volume); + }); + + let new_volume = percent_to_volume(volume_percent); + + let mut volume = rx.recv().expect("to receive info"); + for v in volume.get_mut() { + v.0 = new_volume; + } + + introspector.set_sink_volume_by_name(name, &volume, None); + } + } + + pub fn set_sink_muted(&self, name: &str, muted: bool) { + if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) { + introspector.set_sink_mute_by_name(name, muted, None); + } + } +} + +pub fn on_event( + context: &Arc>, + sinks: &ArcMutVec, + default_sink: &Arc>>, + tx: &broadcast::Sender, + op: Operation, + i: u32, +) { + let introspect = lock!(context).introspect(); + + match op { + Operation::New => { + debug!("new sink"); + introspect.get_sink_info_by_index(i, { + let sinks = sinks.clone(); + let tx = tx.clone(); + + move |info| add(info, &sinks, &tx) + }); + } + Operation::Changed => { + debug!("sink changed"); + introspect.get_sink_info_by_index(i, { + let sinks = sinks.clone(); + let default_sink = default_sink.clone(); + let tx = tx.clone(); + + move |info| update(info, &sinks, &default_sink, &tx) + }); + } + Operation::Removed => { + debug!("sink removed"); + remove(i, sinks, tx); + } + } +} + +pub fn add(info: ListResult<&SinkInfo>, sinks: &ArcMutVec, tx: &broadcast::Sender) { + let ListResult::Item(info) = info else { + return; + }; + + lock!(sinks).push(info.into()); + send!(tx, Event::AddSink(info.into())); +} + +fn update( + info: ListResult<&SinkInfo>, + sinks: &ArcMutVec, + default_sink: &Arc>>, + tx: &broadcast::Sender, +) { + let ListResult::Item(info) = info else { + return; + }; + + { + let mut sinks = lock!(sinks); + let Some(pos) = sinks.iter().position(|sink| sink.index == info.index) else { + error!("received update to untracked sink input"); + return; + }; + + sinks[pos] = info.into(); + + // update in local copy + if !sinks[pos].active { + if let Some(default_sink) = &*lock!(default_sink) { + sinks[pos].active = &sinks[pos].name == default_sink; + } + } + } + + let mut sink: Sink = info.into(); + + // update in broadcast copy + if !sink.active { + if let Some(default_sink) = &*lock!(default_sink) { + sink.active = &sink.name == default_sink; + } + } + + send!(tx, Event::UpdateSink(sink)); +} + +fn remove(index: u32, sinks: &ArcMutVec, tx: &broadcast::Sender) { + let mut sinks = lock!(sinks); + + if let Some(pos) = sinks.iter().position(|s| s.index == index) { + let info = sinks.remove(pos); + send!(tx, Event::RemoveSink(info.name)); + } +} diff --git a/src/clients/volume/sink_input.rs b/src/clients/volume/sink_input.rs new file mode 100644 index 0000000..102aed2 --- /dev/null +++ b/src/clients/volume/sink_input.rs @@ -0,0 +1,148 @@ +use super::{percent_to_volume, volume_to_percent, ArcMutVec, Client, ConnectionState, Event}; +use crate::{lock, send}; +use libpulse_binding::callbacks::ListResult; +use libpulse_binding::context::introspect::SinkInputInfo; +use libpulse_binding::context::subscribe::Operation; +use libpulse_binding::context::Context; +use std::sync::{mpsc, Arc, Mutex}; +use tokio::sync::broadcast; +use tracing::{debug, error}; + +#[derive(Debug, Clone)] +pub struct SinkInput { + pub index: u32, + pub name: String, + pub volume: f64, + pub muted: bool, + + pub can_set_volume: bool, +} + +impl From<&SinkInputInfo<'_>> for SinkInput { + fn from(value: &SinkInputInfo) -> Self { + Self { + index: value.index, + name: value + .name + .as_ref() + .map(ToString::to_string) + .unwrap_or_default(), + muted: value.mute, + volume: volume_to_percent(value.volume), + can_set_volume: value.has_volume && value.volume_writable, + } + } +} + +impl Client { + pub fn sink_inputs(&self) -> Arc>> { + self.data.sink_inputs.clone() + } + + pub fn set_input_volume(&self, index: u32, volume_percent: f64) { + if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) { + let (tx, rx) = mpsc::channel(); + + introspector.get_sink_input_info(index, move |info| { + let ListResult::Item(info) = info else { + return; + }; + send!(tx, info.volume); + }); + + let new_volume = percent_to_volume(volume_percent); + + let mut volume = rx.recv().expect("to receive info"); + for v in volume.get_mut() { + v.0 = new_volume; + } + + introspector.set_sink_input_volume(index, &volume, None); + } + } + + pub fn set_input_muted(&self, index: u32, muted: bool) { + if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) { + introspector.set_sink_input_mute(index, muted, None); + } + } +} + +pub fn on_event( + context: &Arc>, + inputs: &ArcMutVec, + tx: &broadcast::Sender, + op: Operation, + i: u32, +) { + let introspect = lock!(context).introspect(); + + match op { + Operation::New => { + debug!("new sink input"); + introspect.get_sink_input_info(i, { + let inputs = inputs.clone(); + let tx = tx.clone(); + + move |info| add(info, &inputs, &tx) + }); + } + Operation::Changed => { + debug!("sink input changed"); + introspect.get_sink_input_info(i, { + let inputs = inputs.clone(); + let tx = tx.clone(); + + move |info| update(info, &inputs, &tx) + }); + } + Operation::Removed => { + debug!("sink input removed"); + remove(i, inputs, tx); + } + } +} + +pub fn add( + info: ListResult<&SinkInputInfo>, + inputs: &ArcMutVec, + tx: &broadcast::Sender, +) { + let ListResult::Item(info) = info else { + return; + }; + + lock!(inputs).push(info.into()); + send!(tx, Event::AddInput(info.into())); +} + +fn update( + info: ListResult<&SinkInputInfo>, + inputs: &ArcMutVec, + tx: &broadcast::Sender, +) { + let ListResult::Item(info) = info else { + return; + }; + + { + let mut inputs = lock!(inputs); + let Some(pos) = inputs.iter().position(|input| input.index == info.index) else { + error!("received update to untracked sink input"); + return; + }; + + inputs[pos] = info.into(); + } + + send!(tx, Event::UpdateInput(info.into())); +} + +fn remove(index: u32, inputs: &ArcMutVec, tx: &broadcast::Sender) { + let mut inputs = lock!(inputs); + + if let Some(pos) = inputs.iter().position(|s| s.index == index) { + let info = inputs.remove(pos); + send!(tx, Event::RemoveInput(info.index)); + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index bb0d61f..bfd301b 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -21,6 +21,8 @@ use crate::modules::sysinfo::SysInfoModule; use crate::modules::tray::TrayModule; #[cfg(feature = "upower")] use crate::modules::upower::UpowerModule; +#[cfg(feature = "volume")] +use crate::modules::volume::VolumeModule; #[cfg(feature = "workspaces")] use crate::modules::workspaces::WorkspacesModule; use cfg_if::cfg_if; @@ -52,6 +54,8 @@ pub enum ModuleConfig { Tray(Box), #[cfg(feature = "upower")] Upower(Box), + #[cfg(feature = "volume")] + Volume(Box), #[cfg(feature = "workspaces")] Workspaces(Box), } diff --git a/src/macros.rs b/src/macros.rs index 028fd19..4cc986c 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -180,3 +180,10 @@ macro_rules! arc_rw { std::sync::Arc::new(std::sync::RwLock::new($val)) }; } + +#[macro_export] +macro_rules! rc_mut { + ($val:expr) => { + std::rc::Rc::new(std::cell::RefCell::new($val)) + }; +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 102b4bc..d8d671b 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -41,6 +41,8 @@ pub mod sysinfo; pub mod tray; #[cfg(feature = "upower")] pub mod upower; +#[cfg(feature = "volume")] +pub mod volume; #[cfg(feature = "workspaces")] pub mod workspaces; diff --git a/src/modules/volume.rs b/src/modules/volume.rs new file mode 100644 index 0000000..a7dda8d --- /dev/null +++ b/src/modules/volume.rs @@ -0,0 +1,427 @@ +use crate::clients::volume; +use crate::clients::volume::Event; +use crate::config::CommonConfig; +use crate::gtk_helpers::IronbarGtkExt; +use crate::modules::{ + Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext, +}; +use crate::{glib_recv, lock, send_async, spawn, try_send}; +use glib::Propagation; +use gtk::pango::EllipsizeMode; +use gtk::prelude::*; +use gtk::{Button, CellRendererText, ComboBoxText, Label, Orientation, Scale, ToggleButton}; +use serde::Deserialize; +use std::collections::HashMap; +use tokio::sync::mpsc; + +#[derive(Debug, Clone, Deserialize)] +pub struct VolumeModule { + #[serde(default = "default_format")] + format: String, + + #[serde(default = "default_max_volume")] + max_volume: f64, + + #[serde(default)] + icons: Icons, + + #[serde(flatten)] + pub common: Option, +} + +fn default_format() -> String { + String::from("{icon} {percentage}%") +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Icons { + #[serde(default = "default_icon_volume_high")] + volume_high: String, + #[serde(default = "default_icon_volume_medium")] + volume_medium: String, + #[serde(default = "default_icon_volume_low")] + volume_low: String, + #[serde(default = "default_icon_muted")] + muted: String, +} + +impl Icons { + fn volume_icon(&self, volume_percent: f64) -> &str { + match volume_percent as u32 { + 0..=33 => &self.volume_low, + 34..=66 => &self.volume_medium, + 67.. => &self.volume_high, + } + } +} + +impl Default for Icons { + fn default() -> Self { + Self { + volume_high: default_icon_volume_high(), + volume_medium: default_icon_volume_medium(), + volume_low: default_icon_volume_low(), + muted: default_icon_muted(), + } + } +} + +const fn default_max_volume() -> f64 { + 100.0 +} + +fn default_icon_volume_high() -> String { + String::from("󰕾") +} + +fn default_icon_volume_medium() -> String { + String::from("󰖀") +} + +fn default_icon_volume_low() -> String { + String::from("󰕿") +} + +fn default_icon_muted() -> String { + String::from("󰝟") +} + +#[derive(Debug, Clone)] +pub enum Update { + SinkChange(String), + SinkVolume(String, f64), + SinkMute(String, bool), + + InputVolume(u32, f64), + InputMute(u32, bool), +} + +impl Module