mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-04-19 19:34:24 +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:
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",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "doc-comment"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "downcast-rs"
|
name = "downcast-rs"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -1084,6 +1090,25 @@ version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
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]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.53"
|
version = "0.1.53"
|
||||||
|
@ -1171,6 +1196,7 @@ dependencies = [
|
||||||
"glib",
|
"glib",
|
||||||
"gtk",
|
"gtk",
|
||||||
"gtk-layer-shell",
|
"gtk-layer-shell",
|
||||||
|
"hyprland",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libcorn",
|
"libcorn",
|
||||||
|
@ -1615,6 +1641,12 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest"
|
name = "pest"
|
||||||
version = "2.5.1"
|
version = "2.5.1"
|
||||||
|
@ -2300,6 +2332,7 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"mio",
|
"mio",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
|
|
@ -34,6 +34,7 @@ notify = { version = "5.0.0", default-features = false }
|
||||||
mpd_client = "1.0.0"
|
mpd_client = "1.0.0"
|
||||||
mpris = "2.0.0"
|
mpris = "2.0.0"
|
||||||
swayipc-async = { version = "2.0.1" }
|
swayipc-async = { version = "2.0.1" }
|
||||||
|
hyprland = "0.3.0-alpha.0"
|
||||||
sysinfo = "0.27.0"
|
sysinfo = "0.27.0"
|
||||||
wayland-client = "0.29.5"
|
wayland-client = "0.29.5"
|
||||||
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
|
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 music;
|
||||||
pub mod sway;
|
|
||||||
pub mod system_tray;
|
pub mod system_tray;
|
||||||
pub mod wayland;
|
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::config::CommonConfig;
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
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 color_eyre::{Report, Result};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Button;
|
use gtk::Button;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use swayipc_async::{Workspace, WorkspaceChange, WorkspaceEvent};
|
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
@ -25,12 +24,6 @@ pub struct WorkspacesModule {
|
||||||
pub common: Option<CommonConfig>,
|
pub common: Option<CommonConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum WorkspaceUpdate {
|
|
||||||
Init(Vec<Workspace>),
|
|
||||||
Update(Box<WorkspaceEvent>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a button from a workspace
|
/// Creates a button from a workspace
|
||||||
fn create_button(
|
fn create_button(
|
||||||
name: &str,
|
name: &str,
|
||||||
|
@ -70,58 +63,33 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
|
|
||||||
fn spawn_controller(
|
fn spawn_controller(
|
||||||
&self,
|
&self,
|
||||||
info: &ModuleInfo,
|
_info: &ModuleInfo,
|
||||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
mut rx: Receiver<Self::ReceiveMessage>,
|
mut rx: Receiver<Self::ReceiveMessage>,
|
||||||
) -> Result<()> {
|
) -> 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
|
// Subscribe & send events
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let mut srx = {
|
let mut srx = {
|
||||||
let sway = get_sub_client();
|
let client =
|
||||||
sway.subscribe_workspace()
|
Compositor::get_workspace_client().expect("Failed to get workspace client");
|
||||||
|
client.subscribe_workspace_change()
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!("Set up Sway workspace subscription");
|
trace!("Set up Sway workspace subscription");
|
||||||
|
|
||||||
while let Ok(payload) = srx.recv().await {
|
while let Ok(payload) = srx.recv().await {
|
||||||
send_async!(
|
send_async!(tx, ModuleUpdateEvent::Update(payload));
|
||||||
tx,
|
|
||||||
ModuleUpdateEvent::Update(WorkspaceUpdate::Update(payload))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Change workspace focus
|
// Change workspace focus
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
trace!("Setting up UI event handler");
|
trace!("Setting up UI event handler");
|
||||||
let sway = get_client().await;
|
|
||||||
while let Some(name) = rx.recv().await {
|
while let Some(name) = rx.recv().await {
|
||||||
let mut sway = sway.lock().await;
|
let client =
|
||||||
sway.run_command(format!("workspace {}", name)).await?;
|
Compositor::get_workspace_client().expect("Failed to get workspace client");
|
||||||
|
client.focus(name)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<(), Report>(())
|
Ok::<(), Report>(())
|
||||||
|
@ -145,45 +113,64 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
let container = container.clone();
|
let container = container.clone();
|
||||||
let output_name = info.output_name.to_string();
|
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| {
|
context.widget_rx.attach(None, move |event| {
|
||||||
match event {
|
match event {
|
||||||
WorkspaceUpdate::Init(workspaces) => {
|
WorkspaceUpdate::Init(workspaces) => {
|
||||||
trace!("Creating workspace buttons");
|
if !has_initialized {
|
||||||
for workspace in workspaces {
|
trace!("Creating workspace buttons");
|
||||||
let item = create_button(
|
for workspace in workspaces {
|
||||||
&workspace.name,
|
if self.all_monitors || workspace.monitor == output_name {
|
||||||
workspace.focused,
|
let item = create_button(
|
||||||
&name_map,
|
&workspace.name,
|
||||||
&context.controller_tx,
|
workspace.focused,
|
||||||
);
|
&name_map,
|
||||||
container.add(&item);
|
&context.controller_tx,
|
||||||
button_map.insert(workspace.name, item);
|
);
|
||||||
|
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 => {
|
WorkspaceUpdate::Focus { old, new } => {
|
||||||
let old = event
|
let old = button_map.get(&old.name);
|
||||||
.old
|
|
||||||
.and_then(|old| old.name)
|
|
||||||
.and_then(|name| button_map.get(&name));
|
|
||||||
if let Some(old) = old {
|
if let Some(old) = old {
|
||||||
old.style_context().remove_class("focused");
|
old.style_context().remove_class("focused");
|
||||||
}
|
}
|
||||||
|
|
||||||
let new = event
|
let new = button_map.get(&new.name);
|
||||||
.current
|
|
||||||
.and_then(|old| old.name)
|
|
||||||
.and_then(|new| button_map.get(&new));
|
|
||||||
if let Some(new) = new {
|
if let Some(new) = new {
|
||||||
new.style_context().add_class("focused");
|
new.style_context().add_class("focused");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorkspaceUpdate::Update(event) if event.change == WorkspaceChange::Init => {
|
WorkspaceUpdate::Add(workspace) => {
|
||||||
if let Some(workspace) = event.current {
|
if self.all_monitors || workspace.monitor == output_name {
|
||||||
if self.all_monitors
|
let name = workspace.name;
|
||||||
|| workspace.output.unwrap_or_default() == output_name
|
let item = create_button(
|
||||||
{
|
&name,
|
||||||
let name = workspace.name.unwrap_or_default();
|
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(
|
let item = create_button(
|
||||||
&name,
|
&name,
|
||||||
workspace.focused,
|
workspace.focused,
|
||||||
|
@ -197,43 +184,17 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
if !name.is_empty() {
|
if !name.is_empty() {
|
||||||
button_map.insert(name, item);
|
button_map.insert(name, item);
|
||||||
}
|
}
|
||||||
}
|
} else if let Some(item) = button_map.get(&workspace.name) {
|
||||||
}
|
|
||||||
}
|
|
||||||
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())
|
|
||||||
{
|
|
||||||
container.remove(item);
|
container.remove(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WorkspaceUpdate::Remove(workspace) => {
|
||||||
|
let button = button_map.get(&workspace.name);
|
||||||
|
if let Some(item) = button {
|
||||||
|
container.remove(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
WorkspaceUpdate::Update(_) => {}
|
WorkspaceUpdate::Update(_) => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue