diff --git a/Cargo.toml b/Cargo.toml index 988dfc99..3577ae87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,9 +76,11 @@ volume = ["libpulse-binding"] workspaces = ["futures-lite"] "workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"] -"workspaces+sway" = ["workspaces", "swayipc-async"] +"workspaces+sway" = ["workspaces", "sway"] "workspaces+hyprland" = ["workspaces", "hyprland"] +sway = ["swayipc-async"] + schema = ["dep:schemars"] [dependencies] diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index ffad8351..1f6e95f3 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -35,6 +35,7 @@ - [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/Sway-mode.md new file mode 100644 index 00000000..9082f1a9 --- /dev/null +++ b/docs/modules/Sway-mode.md @@ -0,0 +1,78 @@ +Displays the current sway mode in a label. If the current sway mode is +"default", nothing is displayed. + +> [!NOTE] +> This module only works under the [Sway](https://swaywm.org/) compositor. + +## Configuration + +> Type: `sway-mode` + +| Name | Type | Default | Description | +| --------------------- | ------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. | +| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. | +| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. | +| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. | + +
+JSON + +```json +{ + "end": [ + { + "type": "sway-mode", + "truncate": "start" + } + ] +} +``` + +
+ +
+TOML + +```toml +[[end]] +type = "sway-mode" +truncate = "start" +``` + +
+ +
+YAML + +```yaml +end: + - type: "sway-mode" + truncate: "start" +``` + +
+ +
+Corn + +```corn +{ + end = [ + { + type = "sway-mode" + truncate = "start" + } + ] +} +``` + +
+ +## Styling + +| Selector | Description | +| ------------ | ---------------------- | +| `.sway_mode` | Sway mode label widget | + +For more information on styling, please see the [styling guide](styling-guide). diff --git a/src/clients/compositor/mod.rs b/src/clients/compositor/mod.rs index 051a7b82..e03dbe4c 100644 --- a/src/clients/compositor/mod.rs +++ b/src/clients/compositor/mod.rs @@ -1,4 +1,4 @@ -use crate::{await_sync, register_fallible_client}; +use crate::register_fallible_client; use cfg_if::cfg_if; use color_eyre::{Help, Report, Result}; use std::fmt::{Debug, Display, Formatter}; @@ -56,13 +56,16 @@ impl Compositor { /// Creates a new instance of /// the workspace client for the current compositor. - pub fn create_workspace_client() -> Result> { + pub fn create_workspace_client( + clients: &mut super::Clients, + ) -> Result> { let current = Self::get_current(); debug!("Getting workspace client for: {current}"); match current { #[cfg(feature = "workspaces+sway")] - Self::Sway => await_sync(async { sway::Client::new().await }) - .map(|client| Arc::new(client) as Arc), + Self::Sway => clients + .sway() + .map(|client| client as Arc), #[cfg(feature = "workspaces+hyprland")] Self::Hyprland => Ok(Arc::new(hyprland::Client::new())), Self::Unsupported => Err(Report::msg("Unsupported compositor") diff --git a/src/clients/compositor/sway.rs b/src/clients/compositor/sway.rs index b3f41973..397c2256 100644 --- a/src/clients/compositor/sway.rs +++ b/src/clients/compositor/sway.rs @@ -1,85 +1,43 @@ use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate}; -use crate::{await_sync, send, spawn}; -use color_eyre::{Report, Result}; -use futures_lite::StreamExt; -use std::sync::Arc; -use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent}; -use tokio::sync::broadcast::{channel, Receiver, Sender}; -use tokio::sync::Mutex; -use tracing::{info, trace}; - -#[derive(Debug)] -pub struct Client { - client: Arc>, - workspace_tx: Sender, - _workspace_rx: Receiver, -} - -impl Client { - pub(crate) async fn new() -> Result { - // Avoid using `arc_mut!` here because we need tokio Mutex. - let client = Arc::new(Mutex::new(Connection::new().await?)); - info!("Sway IPC subscription client connected"); - - let (workspace_tx, workspace_rx) = channel(16); - - { - // create 2nd client as subscription takes ownership - let client = Connection::new().await?; - let workspace_tx = workspace_tx.clone(); - - spawn(async move { - let event_types = [EventType::Workspace]; - let mut events = client.subscribe(event_types).await?; - - while let Some(event) = events.next().await { - trace!("event: {:?}", event); - if let Event::Workspace(event) = event? { - let event = WorkspaceUpdate::from(*event); - if !matches!(event, WorkspaceUpdate::Unknown) { - workspace_tx.send(event)?; - } - }; - } - - Ok::<(), Report>(()) - }); - } +use crate::{await_sync, send}; +use color_eyre::Result; +use swayipc_async::{Node, WorkspaceChange, WorkspaceEvent}; +use tokio::sync::broadcast::{channel, Receiver}; - Ok(Self { - client, - workspace_tx, - _workspace_rx: workspace_rx, - }) - } -} +use crate::clients::sway::Client; impl WorkspaceClient for Client { fn focus(&self, id: String) -> Result<()> { await_sync(async move { - let mut client = self.client.lock().await; + let mut client = self.connection().lock().await; client.run_command(format!("workspace {id}")).await })?; Ok(()) } fn subscribe_workspace_change(&self) -> Receiver { - let rx = self.workspace_tx.subscribe(); + let (tx, rx) = channel(16); - { - let tx = self.workspace_tx.clone(); - let client = self.client.clone(); + let client = self.connection().clone(); - await_sync(async { - let mut client = client.lock().await; - let workspaces = client.get_workspaces().await.expect("to get workspaces"); + await_sync(async { + let mut client = client.lock().await; + let workspaces = client.get_workspaces().await.expect("to get workspaces"); - let event = - WorkspaceUpdate::Init(workspaces.into_iter().map(Workspace::from).collect()); + let event = + WorkspaceUpdate::Init(workspaces.into_iter().map(Workspace::from).collect()); - send!(tx, event); - }); - } + send!(tx, event); + + drop(client); + + self.add_listener::(move |event| { + let update = WorkspaceUpdate::from(event.clone()); + send!(tx, update); + }) + .await + .expect("to add listener"); + }); rx } diff --git a/src/clients/mod.rs b/src/clients/mod.rs index ebe2bda0..81e44b6a 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -14,6 +14,8 @@ pub mod lua; pub mod music; #[cfg(feature = "network_manager")] pub mod networkmanager; +#[cfg(feature = "sway")] +pub mod sway; #[cfg(feature = "notifications")] pub mod swaync; #[cfg(feature = "tray")] @@ -31,6 +33,8 @@ pub struct Clients { wayland: Option>, #[cfg(feature = "workspaces")] workspaces: Option>, + #[cfg(feature = "sway")] + sway: Option>, #[cfg(feature = "clipboard")] clipboard: Option>, #[cfg(feature = "cairo")] @@ -76,7 +80,7 @@ impl Clients { let client = match &self.workspaces { Some(workspaces) => workspaces.clone(), None => { - let client = compositor::Compositor::create_workspace_client()?; + let client = compositor::Compositor::create_workspace_client(self)?; self.workspaces.replace(client.clone()); client } @@ -85,6 +89,21 @@ impl Clients { Ok(client) } + #[cfg(feature = "sway")] + pub fn sway(&mut self) -> ClientResult { + let client = match &self.sway { + Some(client) => client.clone(), + None => { + let client = await_sync(async { sway::Client::new().await })?; + let client = Arc::new(client); + self.sway.replace(client.clone()); + client + } + }; + + Ok(client) + } + #[cfg(feature = "cairo")] pub fn lua(&mut self, config_dir: &Path) -> Rc { self.lua diff --git a/src/clients/sway.rs b/src/clients/sway.rs new file mode 100644 index 00000000..5a28bfb4 --- /dev/null +++ b/src/clients/sway.rs @@ -0,0 +1,181 @@ +use crate::spawn; +use color_eyre::{Report, Result}; +use futures_lite::StreamExt; +use std::sync::Arc; +use swayipc_async::{Connection, Event, EventType}; +use tokio::sync::Mutex; +use tracing::{info, trace}; + +type SyncFn = dyn Fn(&T) + Sync + Send; + +struct TaskState { + join_handle: Option>>, + // could have been a `HashMap>>`, but we don't + // expect enough listeners to justify the constant overhead of a hashmap. + listeners: Arc>)>>, +} + +pub struct Client { + connection: Arc>, + task_state: Mutex, +} + +impl std::fmt::Debug for Client { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Client") + .field("client", &"Connection") + .field("task_state", &format_args!("<...>")) + .finish() + } +} + +impl Client { + pub(crate) async fn new() -> Result { + // Avoid using `arc_mut!` here because we need tokio Mutex. + let client = Arc::new(Mutex::new(Connection::new().await?)); + info!("Sway IPC subscription client connected"); + + Ok(Self { + connection: client, + task_state: Mutex::new(TaskState { + listeners: Arc::new(Vec::new()), + join_handle: None, + }), + }) + } + + pub fn connection(&self) -> &Arc> { + &self.connection + } + + pub async fn add_listener( + &self, + f: impl Fn(&T) + Sync + Send + 'static, + ) -> Result<()> { + self.add_listener_type( + T::EVENT_TYPE, + Box::new(move |event| { + let event = T::from_event(event).expect("event type mismatch"); + f(event) + }), + ) + .await + } + + pub async fn add_listener_type( + &self, + event_type: EventType, + f: Box>, + ) -> Result<()> { + // abort current running task + let TaskState { + join_handle, + listeners, + } = &mut *self.task_state.lock().await; + + if let Some(handle) = join_handle.take() { + handle.abort(); + let _ = handle.await; + } + + // Only the task and self have a reference to listeners, and we just abort the task. This + // is the only reference to listeners, so we can safely get a mutable reference. + let listeners_mut = Arc::get_mut(listeners) + .ok_or_else(|| Report::msg("Failed to get mutable reference to listeners"))?; + + listeners_mut.push((event_type, f)); + + // create new client as subscription takes ownership + let client = Connection::new().await?; + + let event_types = listeners.iter().map(|(t, _)| *t).collect::>(); + let listeners = listeners.clone(); + + let handle = spawn(async move { + let mut events = client.subscribe(&event_types).await?; + + while let Some(event) = events.next().await { + trace!("event: {:?}", event); + let event = event?; + let ty = sway_event_to_event_type(&event); + for (t, f) in listeners.iter() { + if *t == ty { + f(&event); + } + } + } + + Ok::<(), Report>(()) + }); + + *join_handle = Some(handle); + + Ok(()) + } +} + +fn sway_event_to_event_type(event: &Event) -> EventType { + match event { + Event::Workspace(_) => EventType::Workspace, + Event::Mode(_) => EventType::Mode, + Event::Window(_) => EventType::Window, + Event::BarConfigUpdate(_) => EventType::BarConfigUpdate, + Event::Binding(_) => EventType::Binding, + Event::Shutdown(_) => EventType::Shutdown, + Event::Tick(_) => EventType::Tick, + Event::BarStateUpdate(_) => EventType::BarStateUpdate, + Event::Input(_) => EventType::Input, + _ => todo!(), + } +} + +pub trait SwayIpcEvent { + const EVENT_TYPE: EventType; + fn from_event(e: &Event) -> Option<&Self>; +} +macro_rules! sway_ipc_event_impl { + (@ $($t:tt)*) => { $($t)* }; + ($t:ty, $v:expr, $($m:tt)*) => { + sway_ipc_event_impl! {@ + impl SwayIpcEvent for $t { + const EVENT_TYPE: EventType = $v; + fn from_event(e: &Event) -> Option<&Self> { + match e { + $($m)* (x) => Some(x), + _ => None, + } + } + } + } + }; +} + +sway_ipc_event_impl!( + swayipc_async::WorkspaceEvent, + EventType::Workspace, + Event::Workspace +); +sway_ipc_event_impl!(swayipc_async::ModeEvent, EventType::Mode, Event::Mode); +sway_ipc_event_impl!(swayipc_async::WindowEvent, EventType::Window, Event::Window); +sway_ipc_event_impl!( + swayipc_async::BarConfig, + EventType::BarConfigUpdate, + Event::BarConfigUpdate +); +sway_ipc_event_impl!( + swayipc_async::BindingEvent, + EventType::Binding, + Event::Binding +); +sway_ipc_event_impl!( + swayipc_async::ShutdownEvent, + EventType::Shutdown, + Event::Shutdown +); +sway_ipc_event_impl!(swayipc_async::TickEvent, EventType::Tick, Event::Tick); +sway_ipc_event_impl!( + swayipc_async::BarStateUpdateEvent, + EventType::BarStateUpdate, + Event::BarStateUpdate +); +sway_ipc_event_impl!(swayipc_async::InputEvent, EventType::Input, Event::Input); diff --git a/src/config/mod.rs b/src/config/mod.rs index 6f7bfab6..0c3ece63 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -21,6 +21,8 @@ use crate::modules::networkmanager::NetworkManagerModule; #[cfg(feature = "notifications")] use crate::modules::notifications::NotificationsModule; 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")] @@ -69,6 +71,8 @@ pub enum ModuleConfig { Script(Box), #[cfg(feature = "sys_info")] SysInfo(Box), + #[cfg(feature = "sway")] + SwayMode(Box), #[cfg(feature = "tray")] Tray(Box), #[cfg(feature = "upower")] @@ -114,6 +118,8 @@ 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/mod.rs b/src/modules/mod.rs index ef9ccce1..e4ae5091 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -41,6 +41,8 @@ pub mod networkmanager; #[cfg(feature = "notifications")] pub mod notifications; pub mod script; +#[cfg(feature = "sway")] +pub mod sway; #[cfg(feature = "sys_info")] pub mod sysinfo; #[cfg(feature = "tray")] diff --git a/src/modules/sway/mod.rs b/src/modules/sway/mod.rs new file mode 100644 index 00000000..7b7e0c76 --- /dev/null +++ b/src/modules/sway/mod.rs @@ -0,0 +1 @@ +pub mod mode; diff --git a/src/modules/sway/mode.rs b/src/modules/sway/mode.rs new file mode 100644 index 00000000..9836520d --- /dev/null +++ b/src/modules/sway/mode.rs @@ -0,0 +1,88 @@ +use crate::config::{CommonConfig, TruncateMode}; +use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; +use crate::{await_sync, glib_recv, module_impl, try_send}; +use color_eyre::{Report, Result}; +use gtk::prelude::*; +use gtk::Label; +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 { + // -- Common -- + /// See [truncate options](module-level-options#truncate-mode). + /// + /// **Default**: `null` + pub truncate: Option, + + /// See [common options](module-level-options#common-options). + #[serde(flatten)] + pub common: Option, +} + +impl Module