1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-02 03:01:04 +02: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:
Jake Stanger 2023-01-27 20:08:14 +00:00
parent a79900d842
commit 6e5d0c1e8c
No known key found for this signature in database
GPG key ID: C51FC8F9CB0BEA61
8 changed files with 585 additions and 177 deletions

View 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,
}
}
}

View 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>;
}

View 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()),
}
}
}

View file

@ -1,4 +1,4 @@
pub mod compositor;
pub mod music;
pub mod sway;
pub mod system_tray;
pub mod wayland;

View file

@ -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
}