mirror of
				https://github.com/Zedfrigg/ironbar.git
				synced 2025-10-31 05:31:54 +01:00 
			
		
		
		
	feat(workspaces): hyprland support
Resolves #18. The bar will now automatically detect whether running under Sway or Hyprland and use the correct IPC client depending.
This commit is contained in:
		
					parent
					
						
							
								a79900d842
							
						
					
				
			
			
				commit
				
					
						6e5d0c1e8c
					
				
			
		
					 8 changed files with 585 additions and 177 deletions
				
			
		
							
								
								
									
										33
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										33
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							|  | @ -585,6 +585,12 @@ dependencies = [ | |||
|  "libloading", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "doc-comment" | ||||
| version = "0.3.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "downcast-rs" | ||||
| version = "1.2.0" | ||||
|  | @ -1084,6 +1090,25 @@ version = "0.4.3" | |||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "hyprland" | ||||
| version = "0.3.0-alpha.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a04a666b11a405dd7d74dbfb915e7d6f09bc0a52b0715204bf2e54d5572ab935" | ||||
| dependencies = [ | ||||
|  "async-trait", | ||||
|  "doc-comment", | ||||
|  "hex", | ||||
|  "lazy_static", | ||||
|  "num-traits", | ||||
|  "paste", | ||||
|  "regex", | ||||
|  "serde", | ||||
|  "serde_json", | ||||
|  "serde_repr", | ||||
|  "tokio", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "iana-time-zone" | ||||
| version = "0.1.53" | ||||
|  | @ -1171,6 +1196,7 @@ dependencies = [ | |||
|  "glib", | ||||
|  "gtk", | ||||
|  "gtk-layer-shell", | ||||
|  "hyprland", | ||||
|  "indexmap", | ||||
|  "lazy_static", | ||||
|  "libcorn", | ||||
|  | @ -1615,6 +1641,12 @@ dependencies = [ | |||
|  "windows-sys", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "paste" | ||||
| version = "1.0.11" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "pest" | ||||
| version = "2.5.1" | ||||
|  | @ -2300,6 +2332,7 @@ dependencies = [ | |||
|  "memchr", | ||||
|  "mio", | ||||
|  "num_cpus", | ||||
|  "parking_lot", | ||||
|  "pin-project-lite", | ||||
|  "signal-hook-registry", | ||||
|  "socket2", | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ notify = { version = "5.0.0", default-features = false } | |||
| mpd_client = "1.0.0" | ||||
| mpris = "2.0.0" | ||||
| swayipc-async = { version = "2.0.1" } | ||||
| hyprland = "0.3.0-alpha.0" | ||||
| sysinfo = "0.27.0" | ||||
| wayland-client = "0.29.5" | ||||
| wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] } | ||||
|  |  | |||
							
								
								
									
										250
									
								
								src/clients/compositor/hyprland.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								src/clients/compositor/hyprland.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,250 @@ | |||
| use super::{Workspace, WorkspaceClient, WorkspaceUpdate}; | ||||
| use crate::error::{ERR_CHANNEL_SEND, ERR_MUTEX_LOCK}; | ||||
| use hyprland::data::{Workspace as HWorkspace, Workspaces}; | ||||
| use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial}; | ||||
| use hyprland::event_listener::EventListenerMutable as EventListener; | ||||
| use hyprland::prelude::*; | ||||
| use hyprland::shared::WorkspaceType; | ||||
| use lazy_static::lazy_static; | ||||
| use std::sync::{Arc, Mutex}; | ||||
| use tokio::sync::broadcast::{channel, Receiver, Sender}; | ||||
| use tokio::task::spawn_blocking; | ||||
| use tracing::{error, info}; | ||||
| 
 | ||||
| pub struct EventClient { | ||||
|     workspaces: Arc<Mutex<Vec<Workspace>>>, | ||||
|     workspace_tx: Sender<WorkspaceUpdate>, | ||||
|     _workspace_rx: Receiver<WorkspaceUpdate>, | ||||
| } | ||||
| 
 | ||||
| impl EventClient { | ||||
|     fn new() -> Self { | ||||
|         let (workspace_tx, workspace_rx) = channel(16); | ||||
| 
 | ||||
|         let workspaces = Arc::new(Mutex::new(vec![])); | ||||
|         // load initial list
 | ||||
|         Self::refresh_workspaces(&workspaces); | ||||
| 
 | ||||
|         Self { | ||||
|             workspaces, | ||||
|             workspace_tx, | ||||
|             _workspace_rx: workspace_rx, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn listen_workspace_events(&self) { | ||||
|         info!("Starting Hyprland event listener"); | ||||
| 
 | ||||
|         let workspaces = self.workspaces.clone(); | ||||
|         let tx = self.workspace_tx.clone(); | ||||
| 
 | ||||
|         spawn_blocking(move || { | ||||
|             let mut event_listener = EventListener::new(); | ||||
| 
 | ||||
|             { | ||||
|                 let workspaces = workspaces.clone(); | ||||
|                 let tx = tx.clone(); | ||||
| 
 | ||||
|                 event_listener.add_workspace_added_handler(move |workspace_type, _state| { | ||||
|                     Self::refresh_workspaces(&workspaces); | ||||
| 
 | ||||
|                     let workspace = Self::get_workspace(&workspaces, workspace_type); | ||||
|                     workspace.map_or_else( | ||||
|                         || error!("Unable to locate workspace"), | ||||
|                         |workspace| { | ||||
|                             tx.send(WorkspaceUpdate::Add(workspace)) | ||||
|                                 .expect(ERR_CHANNEL_SEND); | ||||
|                         }, | ||||
|                     ); | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             { | ||||
|                 let workspaces = workspaces.clone(); | ||||
|                 let tx = tx.clone(); | ||||
| 
 | ||||
|                 event_listener.add_workspace_change_handler(move |workspace_type, _state| { | ||||
|                     let prev_workspace = Self::get_focused_workspace(&workspaces); | ||||
| 
 | ||||
|                     Self::refresh_workspaces(&workspaces); | ||||
| 
 | ||||
|                     let workspace = Self::get_workspace(&workspaces, workspace_type); | ||||
| 
 | ||||
|                     if let (Some(prev_workspace), Some(workspace)) = (prev_workspace, workspace) { | ||||
|                         if prev_workspace.id != workspace.id { | ||||
|                             tx.send(WorkspaceUpdate::Focus { | ||||
|                                 old: prev_workspace, | ||||
|                                 new: workspace.clone(), | ||||
|                             }) | ||||
|                             .expect(ERR_CHANNEL_SEND); | ||||
|                         } | ||||
| 
 | ||||
|                         // there may be another type of update so dispatch that regardless of focus change
 | ||||
|                         tx.send(WorkspaceUpdate::Update(workspace)) | ||||
|                             .expect(ERR_CHANNEL_SEND); | ||||
|                     } else { | ||||
|                         error!("Unable to locate workspace"); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             { | ||||
|                 let workspaces = workspaces.clone(); | ||||
|                 let tx = tx.clone(); | ||||
| 
 | ||||
|                 event_listener.add_workspace_destroy_handler(move |workspace_type, _state| { | ||||
|                     let workspace = Self::get_workspace(&workspaces, workspace_type); | ||||
|                     workspace.map_or_else( | ||||
|                         || error!("Unable to locate workspace"), | ||||
|                         |workspace| { | ||||
|                             tx.send(WorkspaceUpdate::Remove(workspace)) | ||||
|                                 .expect(ERR_CHANNEL_SEND); | ||||
|                         }, | ||||
|                     ); | ||||
| 
 | ||||
|                     Self::refresh_workspaces(&workspaces); | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             { | ||||
|                 let workspaces = workspaces.clone(); | ||||
|                 let tx = tx.clone(); | ||||
| 
 | ||||
|                 event_listener.add_workspace_moved_handler(move |event_data, _state| { | ||||
|                     let workspace_type = event_data.1; | ||||
| 
 | ||||
|                     Self::refresh_workspaces(&workspaces); | ||||
| 
 | ||||
|                     let workspace = Self::get_workspace(&workspaces, workspace_type); | ||||
|                     workspace.map_or_else( | ||||
|                         || error!("Unable to locate workspace"), | ||||
|                         |workspace| { | ||||
|                             tx.send(WorkspaceUpdate::Move(workspace)) | ||||
|                                 .expect(ERR_CHANNEL_SEND); | ||||
|                         }, | ||||
|                     ); | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             { | ||||
|                 let workspaces = workspaces.clone(); | ||||
| 
 | ||||
|                 event_listener.add_active_monitor_change_handler(move |event_data, _state| { | ||||
|                     let workspace_type = event_data.1; | ||||
| 
 | ||||
|                     let prev_workspace = Self::get_focused_workspace(&workspaces); | ||||
| 
 | ||||
|                     Self::refresh_workspaces(&workspaces); | ||||
| 
 | ||||
|                     let workspace = Self::get_workspace(&workspaces, workspace_type); | ||||
| 
 | ||||
|                     if let (Some(prev_workspace), Some(workspace)) = (prev_workspace, workspace) { | ||||
|                         if prev_workspace.id != workspace.id { | ||||
|                             tx.send(WorkspaceUpdate::Focus { | ||||
|                                 old: prev_workspace, | ||||
|                                 new: workspace.clone(), | ||||
|                             }) | ||||
|                             .expect(ERR_CHANNEL_SEND); | ||||
|                         } | ||||
|                     } else { | ||||
|                         error!("Unable to locate workspace"); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             event_listener | ||||
|                 .start_listener() | ||||
|                 .expect("Failed to start listener"); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     fn refresh_workspaces(workspaces: &Mutex<Vec<Workspace>>) { | ||||
|         let mut workspaces = workspaces.lock().expect(ERR_MUTEX_LOCK); | ||||
| 
 | ||||
|         let active = HWorkspace::get_active().expect("Failed to get active workspace"); | ||||
|         let new_workspaces = Workspaces::get() | ||||
|             .expect("Failed to get workspaces") | ||||
|             .collect() | ||||
|             .into_iter() | ||||
|             .map(|workspace| Workspace::from((workspace.id == active.id, workspace))); | ||||
| 
 | ||||
|         workspaces.clear(); | ||||
|         workspaces.extend(new_workspaces); | ||||
|     } | ||||
| 
 | ||||
|     fn get_workspace(workspaces: &Mutex<Vec<Workspace>>, id: WorkspaceType) -> Option<Workspace> { | ||||
|         let id_string = id_to_string(id); | ||||
| 
 | ||||
|         let workspaces = workspaces.lock().expect(ERR_MUTEX_LOCK); | ||||
|         workspaces | ||||
|             .iter() | ||||
|             .find(|workspace| workspace.id == id_string) | ||||
|             .cloned() | ||||
|     } | ||||
| 
 | ||||
|     fn get_focused_workspace(workspaces: &Mutex<Vec<Workspace>>) -> Option<Workspace> { | ||||
|         let workspaces = workspaces.lock().expect(ERR_MUTEX_LOCK); | ||||
|         workspaces | ||||
|             .iter() | ||||
|             .find(|workspace| workspace.focused) | ||||
|             .cloned() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl WorkspaceClient for EventClient { | ||||
|     fn focus(&self, id: String) -> color_eyre::Result<()> { | ||||
|         Dispatch::call(DispatchType::Workspace( | ||||
|             WorkspaceIdentifierWithSpecial::Name(&id), | ||||
|         ))?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn subscribe_workspace_change(&self) -> Receiver<WorkspaceUpdate> { | ||||
|         let rx = self.workspace_tx.subscribe(); | ||||
| 
 | ||||
|         { | ||||
|             let tx = self.workspace_tx.clone(); | ||||
| 
 | ||||
|             let workspaces = self.workspaces.clone(); | ||||
|             Self::refresh_workspaces(&workspaces); | ||||
| 
 | ||||
|             let workspaces = workspaces.lock().expect(ERR_MUTEX_LOCK); | ||||
| 
 | ||||
|             tx.send(WorkspaceUpdate::Init(workspaces.clone())) | ||||
|                 .expect(ERR_CHANNEL_SEND); | ||||
|         } | ||||
| 
 | ||||
|         rx | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| lazy_static! { | ||||
|     static ref CLIENT: EventClient = { | ||||
|         let client = EventClient::new(); | ||||
|         client.listen_workspace_events(); | ||||
|         client | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| pub fn get_client() -> &'static EventClient { | ||||
|     &CLIENT | ||||
| } | ||||
| 
 | ||||
| fn id_to_string(id: WorkspaceType) -> String { | ||||
|     match id { | ||||
|         WorkspaceType::Unnamed(id) => id.to_string(), | ||||
|         WorkspaceType::Named(name) => name, | ||||
|         WorkspaceType::Special(name) => name.unwrap_or_default(), | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<(bool, hyprland::data::Workspace)> for Workspace { | ||||
|     fn from((focused, workspace): (bool, hyprland::data::Workspace)) -> Self { | ||||
|         Self { | ||||
|             id: id_to_string(workspace.id), | ||||
|             name: workspace.name, | ||||
|             monitor: workspace.monitor, | ||||
|             focused, | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										89
									
								
								src/clients/compositor/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/clients/compositor/mod.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,89 @@ | |||
| use color_eyre::{Help, Report, Result}; | ||||
| use std::fmt::{Display, Formatter}; | ||||
| use tokio::sync::broadcast; | ||||
| use tracing::debug; | ||||
| 
 | ||||
| pub mod hyprland; | ||||
| pub mod sway; | ||||
| 
 | ||||
| pub enum Compositor { | ||||
|     Sway, | ||||
|     Hyprland, | ||||
|     Unsupported, | ||||
| } | ||||
| 
 | ||||
| impl Display for Compositor { | ||||
|     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||||
|         write!( | ||||
|             f, | ||||
|             "{}", | ||||
|             match self { | ||||
|                 Compositor::Sway => "Sway", | ||||
|                 Compositor::Hyprland => "Hyprland", | ||||
|                 Compositor::Unsupported => "Unsupported", | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Compositor { | ||||
|     /// Attempts to get the current compositor.
 | ||||
|     /// This is done by checking system env vars.
 | ||||
|     fn get_current() -> Self { | ||||
|         if std::env::var("SWAYSOCK").is_ok() { | ||||
|             Self::Sway | ||||
|         } else if std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() { | ||||
|             Self::Hyprland | ||||
|         } else { | ||||
|             Self::Unsupported | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Gets the workspace client for the current compositor
 | ||||
|     pub fn get_workspace_client() -> Result<&'static (dyn WorkspaceClient + Send)> { | ||||
|         let current = Self::get_current(); | ||||
|         debug!("Getting workspace client for: {current}"); | ||||
|         match current { | ||||
|             Compositor::Sway => Ok(sway::get_sub_client()), | ||||
|             Compositor::Hyprland => Ok(hyprland::get_client()), | ||||
|             Compositor::Unsupported => Err(Report::msg("Unsupported compositor") | ||||
|                 .note("Currently workspaces are only supported by Sway and Hyprland")), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct Workspace { | ||||
|     /// Unique identifier
 | ||||
|     pub id: String, | ||||
|     /// Workspace friendly name
 | ||||
|     pub name: String, | ||||
|     /// Name of the monitor (output) the workspace is located on
 | ||||
|     pub monitor: String, | ||||
|     /// Whether the workspace is in focus
 | ||||
|     pub focused: bool, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub enum WorkspaceUpdate { | ||||
|     /// Provides an initial list of workspaces.
 | ||||
|     /// This is re-sent to all subscribers when a new subscription is created.
 | ||||
|     Init(Vec<Workspace>), | ||||
|     Add(Workspace), | ||||
|     Remove(Workspace), | ||||
|     Update(Workspace), | ||||
|     Move(Workspace), | ||||
|     /// Declares focus moved from the old workspace to the new.
 | ||||
|     Focus { | ||||
|         old: Workspace, | ||||
|         new: Workspace, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| pub trait WorkspaceClient { | ||||
|     /// Requests the workspace with this name is focused.
 | ||||
|     fn focus(&self, name: String) -> Result<()>; | ||||
| 
 | ||||
|     /// Creates a new to workspace event receiver.
 | ||||
|     fn subscribe_workspace_change(&self) -> broadcast::Receiver<WorkspaceUpdate>; | ||||
| } | ||||
							
								
								
									
										148
									
								
								src/clients/compositor/sway.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								src/clients/compositor/sway.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,148 @@ | |||
| use super::{Workspace, WorkspaceClient, WorkspaceUpdate}; | ||||
| use crate::await_sync; | ||||
| use crate::error::ERR_CHANNEL_SEND; | ||||
| use async_once::AsyncOnce; | ||||
| use color_eyre::Report; | ||||
| use futures_util::StreamExt; | ||||
| use lazy_static::lazy_static; | ||||
| use std::sync::Arc; | ||||
| use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent}; | ||||
| use tokio::spawn; | ||||
| use tokio::sync::broadcast::{channel, Receiver, Sender}; | ||||
| use tokio::sync::Mutex; | ||||
| use tracing::{info, trace}; | ||||
| 
 | ||||
| pub struct SwayEventClient { | ||||
|     workspace_tx: Sender<WorkspaceUpdate>, | ||||
|     _workspace_rx: Receiver<WorkspaceUpdate>, | ||||
| } | ||||
| 
 | ||||
| impl SwayEventClient { | ||||
|     fn new() -> Self { | ||||
|         let (workspace_tx, workspace_rx) = channel(16); | ||||
| 
 | ||||
|         { | ||||
|             let workspace_tx = workspace_tx.clone(); | ||||
|             spawn(async move { | ||||
|                 let client = Connection::new().await?; | ||||
|                 info!("Sway IPC subscription client connected"); | ||||
| 
 | ||||
|                 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(ev) = event? { | ||||
|                         workspace_tx.send(WorkspaceUpdate::from(*ev))?; | ||||
|                     }; | ||||
|                 } | ||||
| 
 | ||||
|                 Ok::<(), Report>(()) | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         Self { | ||||
|             workspace_tx, | ||||
|             _workspace_rx: workspace_rx, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl WorkspaceClient for SwayEventClient { | ||||
|     fn focus(&self, id: String) -> color_eyre::Result<()> { | ||||
|         await_sync(async move { | ||||
|             let client = get_client().await; | ||||
|             let mut client = client.lock().await; | ||||
|             client.run_command(format!("workspace {id}")).await | ||||
|         })?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn subscribe_workspace_change(&self) -> Receiver<WorkspaceUpdate> { | ||||
|         let rx = self.workspace_tx.subscribe(); | ||||
| 
 | ||||
|         { | ||||
|             let tx = self.workspace_tx.clone(); | ||||
|             await_sync(async { | ||||
|                 let client = get_client().await; | ||||
|                 let mut client = client.lock().await; | ||||
| 
 | ||||
|                 let workspaces = client | ||||
|                     .get_workspaces() | ||||
|                     .await | ||||
|                     .expect("Failed to get workspaces"); | ||||
|                 let event = | ||||
|                     WorkspaceUpdate::Init(workspaces.into_iter().map(Workspace::from).collect()); | ||||
| 
 | ||||
|                 tx.send(event).expect(ERR_CHANNEL_SEND); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         rx | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| lazy_static! { | ||||
|     static ref CLIENT: AsyncOnce<Arc<Mutex<Connection>>> = AsyncOnce::new(async { | ||||
|         let client = Connection::new() | ||||
|             .await | ||||
|             .expect("Failed to connect to Sway socket"); | ||||
|         Arc::new(Mutex::new(client)) | ||||
|     }); | ||||
|     static ref SUB_CLIENT: SwayEventClient = SwayEventClient::new(); | ||||
| } | ||||
| 
 | ||||
| /// Gets the sway IPC client
 | ||||
| async fn get_client() -> Arc<Mutex<Connection>> { | ||||
|     let client = CLIENT.get().await; | ||||
|     Arc::clone(client) | ||||
| } | ||||
| 
 | ||||
| /// Gets the sway IPC event subscription client
 | ||||
| pub fn get_sub_client() -> &'static SwayEventClient { | ||||
|     &SUB_CLIENT | ||||
| } | ||||
| 
 | ||||
| impl From<Node> for Workspace { | ||||
|     fn from(node: Node) -> Self { | ||||
|         Self { | ||||
|             id: node.id.to_string(), | ||||
|             name: node.name.unwrap_or_default(), | ||||
|             monitor: node.output.unwrap_or_default(), | ||||
|             focused: node.focused, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<swayipc_async::Workspace> for Workspace { | ||||
|     fn from(workspace: swayipc_async::Workspace) -> Self { | ||||
|         Self { | ||||
|             id: workspace.id.to_string(), | ||||
|             name: workspace.name, | ||||
|             monitor: workspace.output, | ||||
|             focused: workspace.focused, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<WorkspaceEvent> for WorkspaceUpdate { | ||||
|     fn from(event: WorkspaceEvent) -> Self { | ||||
|         match event.change { | ||||
|             WorkspaceChange::Init => { | ||||
|                 Self::Add(event.current.expect("Missing current workspace").into()) | ||||
|             } | ||||
|             WorkspaceChange::Empty => { | ||||
|                 Self::Remove(event.current.expect("Missing current workspace").into()) | ||||
|             } | ||||
|             WorkspaceChange::Focus => Self::Focus { | ||||
|                 old: event.old.expect("Missing old workspace").into(), | ||||
|                 new: event.current.expect("Missing current workspace").into(), | ||||
|             }, | ||||
|             WorkspaceChange::Move => { | ||||
|                 Self::Move(event.current.expect("Missing current workspace").into()) | ||||
|             } | ||||
|             _ => Self::Update(event.current.expect("Missing current workspace").into()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,4 +1,4 @@ | |||
| pub mod compositor; | ||||
| pub mod music; | ||||
| pub mod sway; | ||||
| pub mod system_tray; | ||||
| pub mod wayland; | ||||
|  |  | |||
|  | @ -1,74 +0,0 @@ | |||
| use async_once::AsyncOnce; | ||||
| use color_eyre::Report; | ||||
| use futures_util::StreamExt; | ||||
| use lazy_static::lazy_static; | ||||
| use std::sync::Arc; | ||||
| use swayipc_async::{Connection, Event, EventType, WorkspaceEvent}; | ||||
| use tokio::spawn; | ||||
| use tokio::sync::broadcast::{channel, Receiver, Sender}; | ||||
| use tokio::sync::Mutex; | ||||
| use tracing::{info, trace}; | ||||
| 
 | ||||
| pub struct SwayEventClient { | ||||
|     workspace_tx: Sender<Box<WorkspaceEvent>>, | ||||
|     _workspace_rx: Receiver<Box<WorkspaceEvent>>, | ||||
| } | ||||
| 
 | ||||
| impl SwayEventClient { | ||||
|     fn new() -> Self { | ||||
|         let (workspace_tx, workspace_rx) = channel(16); | ||||
| 
 | ||||
|         let workspace_tx2 = workspace_tx.clone(); | ||||
| 
 | ||||
|         spawn(async move { | ||||
|             let workspace_tx = workspace_tx2; | ||||
| 
 | ||||
|             let client = Connection::new().await?; | ||||
|             info!("Sway IPC subscription client connected"); | ||||
| 
 | ||||
|             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(ev) = event? { | ||||
|                     workspace_tx.send(ev)?; | ||||
|                 }; | ||||
|             } | ||||
| 
 | ||||
|             Ok::<(), Report>(()) | ||||
|         }); | ||||
| 
 | ||||
|         Self { | ||||
|             workspace_tx, | ||||
|             _workspace_rx: workspace_rx, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Gets an event receiver for workspace events
 | ||||
|     pub fn subscribe_workspace(&self) -> Receiver<Box<WorkspaceEvent>> { | ||||
|         self.workspace_tx.subscribe() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| lazy_static! { | ||||
|     static ref CLIENT: AsyncOnce<Arc<Mutex<Connection>>> = AsyncOnce::new(async { | ||||
|         let client = Connection::new() | ||||
|             .await | ||||
|             .expect("Failed to connect to Sway socket"); | ||||
|         Arc::new(Mutex::new(client)) | ||||
|     }); | ||||
|     static ref SUB_CLIENT: SwayEventClient = SwayEventClient::new(); | ||||
| } | ||||
| 
 | ||||
| /// Gets the sway IPC client
 | ||||
| pub async fn get_client() -> Arc<Mutex<Connection>> { | ||||
|     let client = CLIENT.get().await; | ||||
|     Arc::clone(client) | ||||
| } | ||||
| 
 | ||||
| /// Gets the sway IPC event subscription client
 | ||||
| pub fn get_sub_client() -> &'static SwayEventClient { | ||||
|     &SUB_CLIENT | ||||
| } | ||||
|  | @ -1,13 +1,12 @@ | |||
| use crate::clients::sway::{get_client, get_sub_client}; | ||||
| use crate::clients::compositor::{Compositor, WorkspaceUpdate}; | ||||
| use crate::config::CommonConfig; | ||||
| use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; | ||||
| use crate::{await_sync, send_async, try_send}; | ||||
| use crate::{send_async, try_send}; | ||||
| use color_eyre::{Report, Result}; | ||||
| use gtk::prelude::*; | ||||
| use gtk::Button; | ||||
| use serde::Deserialize; | ||||
| use std::collections::HashMap; | ||||
| use swayipc_async::{Workspace, WorkspaceChange, WorkspaceEvent}; | ||||
| use tokio::spawn; | ||||
| use tokio::sync::mpsc::{Receiver, Sender}; | ||||
| use tracing::trace; | ||||
|  | @ -25,12 +24,6 @@ pub struct WorkspacesModule { | |||
|     pub common: Option<CommonConfig>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug)] | ||||
| pub enum WorkspaceUpdate { | ||||
|     Init(Vec<Workspace>), | ||||
|     Update(Box<WorkspaceEvent>), | ||||
| } | ||||
| 
 | ||||
| /// Creates a button from a workspace
 | ||||
| fn create_button( | ||||
|     name: &str, | ||||
|  | @ -70,58 +63,33 @@ impl Module<gtk::Box> for WorkspacesModule { | |||
| 
 | ||||
|     fn spawn_controller( | ||||
|         &self, | ||||
|         info: &ModuleInfo, | ||||
|         _info: &ModuleInfo, | ||||
|         tx: Sender<ModuleUpdateEvent<Self::SendMessage>>, | ||||
|         mut rx: Receiver<Self::ReceiveMessage>, | ||||
|     ) -> Result<()> { | ||||
|         let workspaces = { | ||||
|             trace!("Getting current workspaces"); | ||||
|             let workspaces = await_sync(async { | ||||
|                 let sway = get_client().await; | ||||
|                 let mut sway = sway.lock().await; | ||||
|                 sway.get_workspaces().await | ||||
|             })?; | ||||
| 
 | ||||
|             if self.all_monitors { | ||||
|                 workspaces | ||||
|             } else { | ||||
|                 trace!("Filtering workspaces to current monitor only"); | ||||
|                 workspaces | ||||
|                     .into_iter() | ||||
|                     .filter(|workspace| workspace.output == info.output_name) | ||||
|                     .collect() | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         try_send!( | ||||
|             tx, | ||||
|             ModuleUpdateEvent::Update(WorkspaceUpdate::Init(workspaces)) | ||||
|         ); | ||||
| 
 | ||||
|         // Subscribe & send events
 | ||||
|         spawn(async move { | ||||
|             let mut srx = { | ||||
|                 let sway = get_sub_client(); | ||||
|                 sway.subscribe_workspace() | ||||
|                 let client = | ||||
|                     Compositor::get_workspace_client().expect("Failed to get workspace client"); | ||||
|                 client.subscribe_workspace_change() | ||||
|             }; | ||||
| 
 | ||||
|             trace!("Set up Sway workspace subscription"); | ||||
| 
 | ||||
|             while let Ok(payload) = srx.recv().await { | ||||
|                 send_async!( | ||||
|                     tx, | ||||
|                     ModuleUpdateEvent::Update(WorkspaceUpdate::Update(payload)) | ||||
|                 ); | ||||
|                 send_async!(tx, ModuleUpdateEvent::Update(payload)); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         // Change workspace focus
 | ||||
|         spawn(async move { | ||||
|             trace!("Setting up UI event handler"); | ||||
|             let sway = get_client().await; | ||||
| 
 | ||||
|             while let Some(name) = rx.recv().await { | ||||
|                 let mut sway = sway.lock().await; | ||||
|                 sway.run_command(format!("workspace {}", name)).await?; | ||||
|                 let client = | ||||
|                     Compositor::get_workspace_client().expect("Failed to get workspace client"); | ||||
|                 client.focus(name)?; | ||||
|             } | ||||
| 
 | ||||
|             Ok::<(), Report>(()) | ||||
|  | @ -145,45 +113,64 @@ impl Module<gtk::Box> for WorkspacesModule { | |||
|             let container = container.clone(); | ||||
|             let output_name = info.output_name.to_string(); | ||||
| 
 | ||||
|             // keep track of whether init event has fired previously
 | ||||
|             // since it fires for every workspace subscriber
 | ||||
|             let mut has_initialized = false; | ||||
| 
 | ||||
|             context.widget_rx.attach(None, move |event| { | ||||
|                 match event { | ||||
|                     WorkspaceUpdate::Init(workspaces) => { | ||||
|                         trace!("Creating workspace buttons"); | ||||
|                         for workspace in workspaces { | ||||
|                             let item = create_button( | ||||
|                                 &workspace.name, | ||||
|                                 workspace.focused, | ||||
|                                 &name_map, | ||||
|                                 &context.controller_tx, | ||||
|                             ); | ||||
|                             container.add(&item); | ||||
|                             button_map.insert(workspace.name, item); | ||||
|                         if !has_initialized { | ||||
|                             trace!("Creating workspace buttons"); | ||||
|                             for workspace in workspaces { | ||||
|                                 if self.all_monitors || workspace.monitor == output_name { | ||||
|                                     let item = create_button( | ||||
|                                         &workspace.name, | ||||
|                                         workspace.focused, | ||||
|                                         &name_map, | ||||
|                                         &context.controller_tx, | ||||
|                                     ); | ||||
|                                     container.add(&item); | ||||
|                                     button_map.insert(workspace.name, item); | ||||
|                                 } | ||||
|                             } | ||||
|                             container.show_all(); | ||||
|                             has_initialized = true; | ||||
|                         } | ||||
|                         container.show_all(); | ||||
|                     } | ||||
|                     WorkspaceUpdate::Update(event) if event.change == WorkspaceChange::Focus => { | ||||
|                         let old = event | ||||
|                             .old | ||||
|                             .and_then(|old| old.name) | ||||
|                             .and_then(|name| button_map.get(&name)); | ||||
|                     WorkspaceUpdate::Focus { old, new } => { | ||||
|                         let old = button_map.get(&old.name); | ||||
|                         if let Some(old) = old { | ||||
|                             old.style_context().remove_class("focused"); | ||||
|                         } | ||||
| 
 | ||||
|                         let new = event | ||||
|                             .current | ||||
|                             .and_then(|old| old.name) | ||||
|                             .and_then(|new| button_map.get(&new)); | ||||
|                         let new = button_map.get(&new.name); | ||||
|                         if let Some(new) = new { | ||||
|                             new.style_context().add_class("focused"); | ||||
|                         } | ||||
|                     } | ||||
|                     WorkspaceUpdate::Update(event) if event.change == WorkspaceChange::Init => { | ||||
|                         if let Some(workspace) = event.current { | ||||
|                             if self.all_monitors | ||||
|                                 || workspace.output.unwrap_or_default() == output_name | ||||
|                             { | ||||
|                                 let name = workspace.name.unwrap_or_default(); | ||||
|                     WorkspaceUpdate::Add(workspace) => { | ||||
|                         if self.all_monitors || workspace.monitor == output_name { | ||||
|                             let name = workspace.name; | ||||
|                             let item = create_button( | ||||
|                                 &name, | ||||
|                                 workspace.focused, | ||||
|                                 &name_map, | ||||
|                                 &context.controller_tx, | ||||
|                             ); | ||||
| 
 | ||||
|                             item.show(); | ||||
|                             container.add(&item); | ||||
| 
 | ||||
|                             if !name.is_empty() { | ||||
|                                 button_map.insert(name, item); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     WorkspaceUpdate::Move(workspace) => { | ||||
|                         if !self.all_monitors { | ||||
|                             if workspace.monitor == output_name { | ||||
|                                 let name = workspace.name; | ||||
|                                 let item = create_button( | ||||
|                                     &name, | ||||
|                                     workspace.focused, | ||||
|  | @ -197,43 +184,17 @@ impl Module<gtk::Box> for WorkspacesModule { | |||
|                                 if !name.is_empty() { | ||||
|                                     button_map.insert(name, item); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     WorkspaceUpdate::Update(event) if event.change == WorkspaceChange::Move => { | ||||
|                         if let Some(workspace) = event.current { | ||||
|                             if !self.all_monitors { | ||||
|                                 if workspace.output.unwrap_or_default() == output_name { | ||||
|                                     let name = workspace.name.unwrap_or_default(); | ||||
|                                     let item = create_button( | ||||
|                                         &name, | ||||
|                                         workspace.focused, | ||||
|                                         &name_map, | ||||
|                                         &context.controller_tx, | ||||
|                                     ); | ||||
| 
 | ||||
|                                     item.show(); | ||||
|                                     container.add(&item); | ||||
| 
 | ||||
|                                     if !name.is_empty() { | ||||
|                                         button_map.insert(name, item); | ||||
|                                     } | ||||
|                                 } else if let Some(item) = | ||||
|                                     button_map.get(&workspace.name.unwrap_or_default()) | ||||
|                                 { | ||||
|                                     container.remove(item); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     WorkspaceUpdate::Update(event) if event.change == WorkspaceChange::Empty => { | ||||
|                         if let Some(workspace) = event.current { | ||||
|                             if let Some(item) = button_map.get(&workspace.name.unwrap_or_default()) | ||||
|                             { | ||||
|                             } else if let Some(item) = button_map.get(&workspace.name) { | ||||
|                                 container.remove(item); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     WorkspaceUpdate::Remove(workspace) => { | ||||
|                         let button = button_map.get(&workspace.name); | ||||
|                         if let Some(item) = button { | ||||
|                             container.remove(item); | ||||
|                         } | ||||
|                     } | ||||
|                     WorkspaceUpdate::Update(_) => {} | ||||
|                 }; | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue