diff --git a/Cargo.toml b/Cargo.toml index e7eb0b4..d5911b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ default = [ "focused", "http", "ipc", + "bindmode+all", "keyboard+all", "launcher", "label", @@ -38,6 +39,11 @@ ipc = ["dep:serde_json", "dep:clap"] http = ["dep:reqwest"] +bindmode = [] +"bindmode+all" = ["bindmode+sway", "bindmode+hyprland"] +"bindmode+sway" = ["bindmode", "sway"] +"bindmode+hyprland" = ["bindmode", "hyprland"] + "config+all" = [ "config+json", "config+yaml", diff --git a/docs/GTK4.md b/docs/GTK4.md index 0ac3f4f..0ba7fc7 100644 --- a/docs/GTK4.md +++ b/docs/GTK4.md @@ -29,6 +29,7 @@ A full list of feature flags can be found [here](Compiling#features). | Module | Status | Notes | |-----------------|--------|------------------------------------------------------------------------------------------------------------------------------------------| +| Bindmode | ❌ | | | Cairo | ✅ | | | Clipboard | ✅ | | | Clock | ✅ | | @@ -41,7 +42,6 @@ A full list of feature flags can be found [here](Compiling#features). | Network Manager | ❌ | | | Notifications | ✅ | | | Script | ✅ | | -| Sway Mode | ❌ | | | SysInfo | ✅ | | | Tray | ❌ | GTK4 removes widgets required to move the tray. No `libdbusmenu-gtk4` either. will need to manually re-create menus with custom widgets. | | UPower | ❌ | | diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index 260518c..c476d86 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -25,6 +25,7 @@ # Modules +- [Bindmode](bindmode) - [Cairo](cairo) - [Clipboard](clipboard) - [Clock](clock) @@ -37,7 +38,6 @@ - [Network Manager](network-manager) - [Notifications](notifications) - [Script](script) -- [Sway-mode](sway-mode) - [Sys_Info](sys-info) - [Tray](tray) - [Upower](upower) diff --git a/docs/modules/Sway-mode.md b/docs/modules/Bindmode.md similarity index 79% rename from docs/modules/Sway-mode.md rename to docs/modules/Bindmode.md index 9082f1a..8c802df 100644 --- a/docs/modules/Sway-mode.md +++ b/docs/modules/Bindmode.md @@ -1,12 +1,12 @@ -Displays the current sway mode in a label. If the current sway mode is -"default", nothing is displayed. +> [!IMPORTANT] +> This module is currently only available on Sway and Hyprland. -> [!NOTE] -> This module only works under the [Sway](https://swaywm.org/) compositor. +Displays Sway's current binding mode or [Hyprland's current submap](https://wiki.hyprland.org/Configuring/Binds/#submaps) +in a label. Nothing is displayed if no binding mode is active. ## Configuration -> Type: `sway-mode` +> Type: `bindmode` | Name | Type | Default | Description | | --------------------- | ------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -22,7 +22,7 @@ Displays the current sway mode in a label. If the current sway mode is { "end": [ { - "type": "sway-mode", + "type": "bindmode", "truncate": "start" } ] @@ -36,7 +36,7 @@ Displays the current sway mode in a label. If the current sway mode is ```toml [[end]] -type = "sway-mode" +type = "bindmode" truncate = "start" ``` @@ -47,7 +47,7 @@ truncate = "start" ```yaml end: - - type: "sway-mode" + - type: "bindmode" truncate: "start" ``` @@ -60,7 +60,7 @@ end: { end = [ { - type = "sway-mode" + type = "bindmode" truncate = "start" } ] @@ -71,8 +71,8 @@ end: ## Styling -| Selector | Description | -| ------------ | ---------------------- | -| `.sway_mode` | Sway mode label widget | +| Selector | Description | +| ----------- | ---------------------- | +| `.bindmode` | Bind mode label widget | For more information on styling, please see the [styling guide](styling-guide). diff --git a/src/clients/compositor/hyprland.rs b/src/clients/compositor/hyprland.rs index f205d6d..ede71b8 100644 --- a/src/clients/compositor/hyprland.rs +++ b/src/clients/compositor/hyprland.rs @@ -10,11 +10,22 @@ use hyprland::shared::{HyprDataVec, WorkspaceType}; use tokio::sync::broadcast::{Receiver, Sender, channel}; use tracing::{debug, error, info}; +#[cfg(feature = "bindmode+hyprland")] +use super::{BindModeClient, BindModeUpdate}; +#[cfg(feature = "keyboard+hyprland")] +use super::{KeyboardLayoutClient, KeyboardLayoutUpdate}; + #[derive(Debug)] struct TxRx { tx: Sender, _rx: Receiver, } +impl TxRx { + fn new() -> Self { + let (tx, rx) = channel(16); + Self { tx, _rx: rx } + } +} #[derive(Debug)] pub struct Client { @@ -23,27 +34,20 @@ pub struct Client { #[cfg(feature = "keyboard+hyprland")] keyboard_layout: TxRx, + + #[cfg(feature = "bindmode+hyprland")] + bindmode: TxRx, } impl Client { pub(crate) fn new() -> Self { - #[cfg(feature = "workspaces+hyprland")] - let (workspace_tx, workspace_rx) = channel(16); - - #[cfg(feature = "keyboard+hyprland")] - let (keyboard_layout_tx, keyboard_layout_rx) = channel(16); - let instance = Self { #[cfg(feature = "workspaces+hyprland")] - workspace: TxRx { - tx: workspace_tx, - _rx: workspace_rx, - }, + workspace: TxRx::new(), #[cfg(feature = "keyboard+hyprland")] - keyboard_layout: TxRx { - tx: keyboard_layout_tx, - _rx: keyboard_layout_rx, - }, + keyboard_layout: TxRx::new(), + #[cfg(feature = "bindmode+hyprland")] + bindmode: TxRx::new(), }; instance.listen_events(); @@ -54,11 +58,14 @@ impl Client { info!("Starting Hyprland event listener"); #[cfg(feature = "workspaces+hyprland")] - let tx = self.workspace.tx.clone(); + let workspace_tx = self.workspace.tx.clone(); #[cfg(feature = "keyboard+hyprland")] let keyboard_layout_tx = self.keyboard_layout.tx.clone(); + #[cfg(feature = "bindmode+hyprland")] + let bindmode_tx = self.bindmode.tx.clone(); + spawn_blocking(move || { let mut event_listener = EventListener::new(); @@ -67,10 +74,13 @@ impl Client { // cache the active workspace since Hyprland doesn't give us the prev active #[cfg(feature = "workspaces+hyprland")] - Self::listen_workspace_events(tx, &mut event_listener, &lock); + Self::listen_workspace_events(workspace_tx, &mut event_listener, &lock); #[cfg(feature = "keyboard+hyprland")] - Self::listen_keyboard_events(keyboard_layout_tx, &mut event_listener, lock); + Self::listen_keyboard_events(keyboard_layout_tx, &mut event_listener, &lock); + + #[cfg(feature = "bindmode+hyprland")] + Self::listen_bindmode_events(bindmode_tx, &mut event_listener, &lock); event_listener .start_listener() @@ -257,7 +267,7 @@ impl Client { fn listen_keyboard_events( keyboard_layout_tx: Sender, event_listener: &mut EventListener, - lock: std::sync::Arc>, + lock: &std::sync::Arc>, ) { let tx = keyboard_layout_tx.clone(); let lock = lock.clone(); @@ -307,6 +317,29 @@ impl Client { }); } + #[cfg(feature = "bindmode+hyprland")] + fn listen_bindmode_events( + bindmode_tx: Sender, + event_listener: &mut EventListener, + lock: &std::sync::Arc>, + ) { + let tx = bindmode_tx.clone(); + let lock = lock.clone(); + + event_listener.add_sub_map_change_handler(move |bind_mode| { + let _lock = lock!(lock); + debug!("Received bind mode: {bind_mode:?}"); + + send!( + tx, + BindModeUpdate { + name: bind_mode, + pango_markup: false, + } + ); + }); + } + /// Sends a `WorkspaceUpdate::Focus` event /// and updates the active workspace cache. #[cfg(feature = "workspaces+hyprland")] @@ -392,9 +425,6 @@ impl super::WorkspaceClient for Client { } } -#[cfg(feature = "keyboard+hyprland")] -use super::{KeyboardLayoutClient, KeyboardLayoutUpdate}; - #[cfg(feature = "keyboard+hyprland")] impl KeyboardLayoutClient for Client { fn set_next_active(&self) { @@ -436,6 +466,13 @@ impl KeyboardLayoutClient for Client { } } +#[cfg(feature = "bindmode+hyprland")] +impl BindModeClient for Client { + fn subscribe(&self) -> Result> { + Ok(self.bindmode.tx.subscribe()) + } +} + fn get_workspace_name(name: WorkspaceType) -> String { match name { WorkspaceType::Regular(name) => name, diff --git a/src/clients/compositor/mod.rs b/src/clients/compositor/mod.rs index f413b99..e9397a8 100644 --- a/src/clients/compositor/mod.rs +++ b/src/clients/compositor/mod.rs @@ -66,6 +66,30 @@ impl Compositor { } } + #[cfg(feature = "bindmode")] + pub fn create_bindmode_client( + clients: &mut super::Clients, + ) -> ClientResult { + let current = Self::get_current(); + debug!("Getting keyboard_layout client for: {current}"); + match current { + #[cfg(feature = "bindmode+sway")] + Self::Sway => clients + .sway() + .map(|client| client as Arc), + #[cfg(feature = "bindmode+hyprland")] + Self::Hyprland => Ok(clients.hyprland()), + #[cfg(feature = "niri")] + Self::Niri => Err(Report::msg("Unsupported compositor") + .note("Currently bindmode is only supported by Sway and Hyprland")), + Self::Unsupported => Err(Report::msg("Unsupported compositor") + .note("Currently bindmode is only supported by Sway and Hyprland")), + #[allow(unreachable_patterns)] + _ => Err(Report::msg("Unsupported compositor") + .note("Bindmode feature is disabled for this compositor")), + } + } + #[cfg(feature = "keyboard")] pub fn create_keyboard_layout_client( clients: &mut super::Clients, @@ -195,6 +219,14 @@ pub enum WorkspaceUpdate { Unknown, } +#[derive(Clone, Debug)] +pub struct BindModeUpdate { + /// The binding mode that became active. + pub name: String, + /// Whether the mode should be parsed as pango markup. + pub pango_markup: bool, +} + #[cfg(feature = "workspaces")] pub trait WorkspaceClient: Debug + Send + Sync { /// Requests the workspace with this id is focused. @@ -218,3 +250,12 @@ pub trait KeyboardLayoutClient: Debug + Send + Sync { #[cfg(feature = "keyboard")] register_fallible_client!(dyn KeyboardLayoutClient, keyboard_layout); + +#[cfg(feature = "bindmode")] +pub trait BindModeClient: Debug + Send + Sync { + /// Add a callback for bindmode updates. + fn subscribe(&self) -> Result>; +} + +#[cfg(feature = "bindmode")] +register_fallible_client!(dyn BindModeClient, bindmode); diff --git a/src/clients/compositor/sway.rs b/src/clients/compositor/sway.rs index 5e01575..1de3536 100644 --- a/src/clients/compositor/sway.rs +++ b/src/clients/compositor/sway.rs @@ -231,3 +231,36 @@ impl TryFrom for KeyboardLayoutUpdate { } } } + +#[cfg(feature = "bindmode+sway")] +use super::{BindModeClient, BindModeUpdate}; + +#[cfg(feature = "bindmode+sway")] +impl BindModeClient for Client { + fn subscribe(&self) -> Result, Report> { + let (tx, rx) = channel(16); + await_sync(async { + self.add_listener::(move |mode| { + tracing::trace!("mode: {:?}", mode); + + // when no bindind is active the bindmode is named "default", but we must display + // nothing in this case. + let name = if mode.change == "default" { + String::new() + } else { + mode.change.clone() + }; + + send!( + tx, + BindModeUpdate { + name, + pango_markup: mode.pango_markup, + } + ); + }) + .await + })?; + Ok(rx) + } +} diff --git a/src/clients/mod.rs b/src/clients/mod.rs index 5ba6392..c4fce0d 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -7,7 +7,12 @@ use std::sync::Arc; #[cfg(feature = "clipboard")] pub mod clipboard; -#[cfg(any(feature = "keyboard", feature = "workspaces", feature = "hyprland"))] +#[cfg(any( + feature = "bindmode", + feature = "hyprland", + feature = "keyboard", + feature = "workspaces", +))] pub mod compositor; #[cfg(feature = "keyboard")] pub mod libinput; @@ -42,6 +47,8 @@ pub struct Clients { sway: Option>, #[cfg(feature = "hyprland")] hyprland: Option>, + #[cfg(feature = "bindmode")] + bindmode: Option>, #[cfg(feature = "clipboard")] clipboard: Option>, #[cfg(feature = "keyboard")] @@ -114,6 +121,19 @@ impl Clients { Ok(client) } + #[cfg(feature = "bindmode")] + pub fn bindmode(&mut self) -> ClientResult { + let client = if let Some(client) = &self.bindmode { + client.clone() + } else { + let client = compositor::Compositor::create_bindmode_client(self)?; + self.bindmode.replace(client.clone()); + client + }; + + Ok(client) + } + #[cfg(feature = "sway")] pub fn sway(&mut self) -> ClientResult { let client = if let Some(client) = &self.sway { diff --git a/src/config/mod.rs b/src/config/mod.rs index 5930225..daf9ae9 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -3,6 +3,8 @@ mod r#impl; mod layout; mod truncate; +#[cfg(any(feature = "bindmode"))] +use crate::modules::bindmode::Bindmode; #[cfg(feature = "cairo")] use crate::modules::cairo::CairoModule; #[cfg(feature = "clipboard")] @@ -27,8 +29,6 @@ use crate::modules::networkmanager::NetworkManagerModule; use crate::modules::notifications::NotificationsModule; #[cfg(feature = "script")] use crate::modules::script::ScriptModule; -#[cfg(feature = "sway")] -use crate::modules::sway::mode::SwayModeModule; #[cfg(feature = "sys_info")] use crate::modules::sysinfo::SysInfoModule; #[cfg(feature = "tray")] @@ -57,6 +57,8 @@ pub use self::truncate::{EllipsizeMode, TruncateMode}; #[serde(tag = "type", rename_all = "snake_case")] #[cfg_attr(feature = "schema", derive(JsonSchema))] pub enum ModuleConfig { + #[cfg(feature = "bindmode")] + Bindmode(Box), #[cfg(feature = "cairo")] Cairo(Box), #[cfg(feature = "clipboard")] @@ -83,8 +85,6 @@ pub enum ModuleConfig { Script(Box), #[cfg(feature = "sys_info")] SysInfo(Box), - #[cfg(feature = "sway")] - SwayMode(Box), #[cfg(feature = "tray")] Tray(Box), #[cfg(feature = "upower")] @@ -109,6 +109,8 @@ impl ModuleConfig { } match self { + #[cfg(feature = "bindmode")] + Self::Bindmode(module) => create!(module), #[cfg(feature = "cairo")] Self::Cairo(module) => create!(module), #[cfg(feature = "clipboard")] @@ -135,8 +137,6 @@ impl ModuleConfig { Self::Script(module) => create!(module), #[cfg(feature = "sys_info")] Self::SysInfo(module) => create!(module), - #[cfg(feature = "sway")] - Self::SwayMode(module) => create!(module), #[cfg(feature = "tray")] Self::Tray(module) => create!(module), #[cfg(feature = "upower")] diff --git a/src/modules/sway/mode.rs b/src/modules/bindmode.rs similarity index 58% rename from src/modules/sway/mode.rs rename to src/modules/bindmode.rs index e0a3cc7..e1bc617 100644 --- a/src/modules/sway/mode.rs +++ b/src/modules/bindmode.rs @@ -1,18 +1,18 @@ +use crate::clients::compositor::BindModeUpdate; use crate::config::{CommonConfig, LayoutConfig, TruncateMode}; use crate::gtk_helpers::IronbarLabelExt; -use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; -use crate::{await_sync, glib_recv, module_impl, try_send}; -use color_eyre::{Report, Result}; +use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext}; +use crate::{glib_recv, module_impl, module_update, send_async, spawn}; +use color_eyre::Result; use gtk::Label; use gtk::prelude::*; use serde::Deserialize; -use swayipc_async::ModeEvent; use tokio::sync::mpsc; use tracing::{info, trace}; #[derive(Debug, Deserialize, Clone)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -pub struct SwayModeModule { +pub struct Bindmode { // -- Common -- /// See [truncate options](module-level-options#truncate-mode). /// @@ -28,11 +28,11 @@ pub struct SwayModeModule { pub common: Option, } -impl Module