mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-08-17 23:01:04 +02:00
feat(workspaces): niri support
Co-authored-by: Jake Stanger <mail@jstanger.dev>
This commit is contained in:
parent
57bfab1dcc
commit
02a8ddabf0
12 changed files with 367 additions and 31 deletions
117
src/clients/compositor/niri/connection.rs
Normal file
117
src/clients/compositor/niri/connection.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
/// Taken from the `niri_ipc` crate.
|
||||
/// Only a relevant snippet has been extracted
|
||||
/// to reduce compile times.
|
||||
use crate::clients::compositor::Workspace as IronWorkspace;
|
||||
use crate::{await_sync, clients::compositor::Visibility};
|
||||
use color_eyre::eyre::{eyre, Result};
|
||||
use core::str;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{env, path::Path};
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
net::UnixStream,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum Request {
|
||||
Action(Action),
|
||||
EventStream,
|
||||
}
|
||||
|
||||
pub type Reply = Result<Response, String>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum Response {
|
||||
Handled,
|
||||
Workspaces(Vec<Workspace>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum Action {
|
||||
FocusWorkspace { reference: WorkspaceReferenceArg },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WorkspaceReferenceArg {
|
||||
Name(String),
|
||||
Id(u64),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Workspace {
|
||||
pub id: u64,
|
||||
pub idx: u8,
|
||||
pub name: Option<String>,
|
||||
pub output: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub is_focused: bool,
|
||||
}
|
||||
|
||||
impl From<&Workspace> for IronWorkspace {
|
||||
fn from(workspace: &Workspace) -> IronWorkspace {
|
||||
// Workspaces in niri don't neccessarily have names.
|
||||
// If the niri workspace has a name then it is assigned as is,
|
||||
// but if it does not have a name, the monitor index is used.
|
||||
Self {
|
||||
id: workspace.id as i64,
|
||||
name: workspace.name.clone().unwrap_or(workspace.idx.to_string()),
|
||||
monitor: workspace.output.clone().unwrap_or_default(),
|
||||
visibility: if workspace.is_active {
|
||||
Visibility::Visible {
|
||||
focused: workspace.is_focused,
|
||||
}
|
||||
} else {
|
||||
Visibility::Hidden
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum Event {
|
||||
WorkspacesChanged { workspaces: Vec<Workspace> },
|
||||
WorkspaceActivated { id: u64, focused: bool },
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Connection(UnixStream);
|
||||
impl Connection {
|
||||
pub async fn connect() -> Result<Self> {
|
||||
let socket_path =
|
||||
env::var_os("NIRI_SOCKET").ok_or_else(|| eyre!("NIRI_SOCKET not found!"))?;
|
||||
Self::connect_to(socket_path).await
|
||||
}
|
||||
|
||||
pub async fn connect_to(path: impl AsRef<Path>) -> Result<Self> {
|
||||
let raw_stream = UnixStream::connect(path.as_ref()).await?;
|
||||
let stream = raw_stream;
|
||||
Ok(Self(stream))
|
||||
}
|
||||
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
request: Request,
|
||||
) -> Result<(Reply, impl FnMut() -> Result<Event> + '_)> {
|
||||
let Self(stream) = self;
|
||||
let mut buf = serde_json::to_string(&request)?;
|
||||
|
||||
stream.write_all(buf.as_bytes()).await?;
|
||||
stream.shutdown().await?;
|
||||
|
||||
buf.clear();
|
||||
let mut reader = BufReader::new(stream);
|
||||
reader.read_line(&mut buf).await?;
|
||||
let reply = serde_json::from_str(&buf)?;
|
||||
|
||||
let events = move || {
|
||||
buf.clear();
|
||||
await_sync(async {
|
||||
reader.read_line(&mut buf).await.unwrap_or(0);
|
||||
});
|
||||
let event: Event = serde_json::from_str(&buf).unwrap_or(Event::Other);
|
||||
Ok(event)
|
||||
};
|
||||
Ok((reply, events))
|
||||
}
|
||||
}
|
183
src/clients/compositor/niri/mod.rs
Normal file
183
src/clients/compositor/niri/mod.rs
Normal file
|
@ -0,0 +1,183 @@
|
|||
use crate::{clients::compositor::Visibility, send, spawn};
|
||||
use color_eyre::Report;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use super::{Workspace as IronWorkspace, WorkspaceClient, WorkspaceUpdate};
|
||||
mod connection;
|
||||
|
||||
use connection::{Action, Connection, Event, Request, WorkspaceReferenceArg};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
tx: broadcast::Sender<WorkspaceUpdate>,
|
||||
_rx: broadcast::Receiver<WorkspaceUpdate>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = broadcast::channel(32);
|
||||
let tx2 = tx.clone();
|
||||
|
||||
spawn(async move {
|
||||
let mut conn = Connection::connect().await?;
|
||||
let (_, mut event_listener) = conn.send(Request::EventStream).await?;
|
||||
|
||||
let mut workspace_state: Vec<IronWorkspace> = Vec::new();
|
||||
let mut first_event = true;
|
||||
|
||||
loop {
|
||||
let events = match event_listener() {
|
||||
Ok(Event::WorkspacesChanged { workspaces }) => {
|
||||
// Niri only has a WorkspacesChanged Event and Ironbar has 4 events which have to be handled: Add, Remove, Rename and Move.
|
||||
// This is handled by keeping a previous state of workspaces and comparing with the new state for changes.
|
||||
let new_workspaces: Vec<IronWorkspace> = workspaces
|
||||
.into_iter()
|
||||
.map(|w| IronWorkspace::from(&w))
|
||||
.collect();
|
||||
|
||||
let mut updates: Vec<WorkspaceUpdate> = vec![];
|
||||
|
||||
if first_event {
|
||||
updates.push(WorkspaceUpdate::Init(new_workspaces.clone()));
|
||||
first_event = false;
|
||||
} else {
|
||||
// first pass - add/update
|
||||
for workspace in &new_workspaces {
|
||||
let old_workspace =
|
||||
workspace_state.iter().find(|w| w.id == workspace.id);
|
||||
|
||||
match old_workspace {
|
||||
None => updates.push(WorkspaceUpdate::Add(workspace.clone())),
|
||||
Some(old_workspace) => {
|
||||
if workspace.name != old_workspace.name {
|
||||
updates.push(WorkspaceUpdate::Rename {
|
||||
id: workspace.id,
|
||||
name: workspace.name.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if workspace.monitor != old_workspace.monitor {
|
||||
updates.push(WorkspaceUpdate::Move(workspace.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// second pass - delete
|
||||
for workspace in &workspace_state {
|
||||
let exists = new_workspaces.iter().any(|w| w.id == workspace.id);
|
||||
|
||||
if !exists {
|
||||
updates.push(WorkspaceUpdate::Remove(workspace.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
workspace_state = new_workspaces;
|
||||
updates
|
||||
}
|
||||
|
||||
Ok(Event::WorkspaceActivated { id, focused }) => {
|
||||
// workspace with id is activated, if focus is true then it is also focused
|
||||
// if focused is true then focus has changed => find old focused workspace. set it to inactive and set current
|
||||
//
|
||||
// we use indexes here as both new/old need to be mutable
|
||||
|
||||
if let Some(new_index) =
|
||||
workspace_state.iter().position(|w| w.id == id as i64)
|
||||
{
|
||||
if focused {
|
||||
if let Some(old_index) = workspace_state
|
||||
.iter()
|
||||
.position(|w| w.visibility.is_focused())
|
||||
{
|
||||
workspace_state[new_index].visibility = Visibility::focused();
|
||||
|
||||
if workspace_state[old_index].monitor
|
||||
== workspace_state[new_index].monitor
|
||||
{
|
||||
workspace_state[old_index].visibility = Visibility::Hidden;
|
||||
} else {
|
||||
workspace_state[old_index].visibility =
|
||||
Visibility::visible();
|
||||
}
|
||||
|
||||
vec![WorkspaceUpdate::Focus {
|
||||
old: Some(workspace_state[old_index].clone()),
|
||||
new: workspace_state[new_index].clone(),
|
||||
}]
|
||||
} else {
|
||||
workspace_state[new_index].visibility = Visibility::focused();
|
||||
|
||||
vec![WorkspaceUpdate::Focus {
|
||||
old: None,
|
||||
new: workspace_state[new_index].clone(),
|
||||
}]
|
||||
}
|
||||
} else {
|
||||
// if focused is false means active workspace on a particular monitor has changed =>
|
||||
// change all workspaces on monitor to inactive and change current workspace as active
|
||||
workspace_state[new_index].visibility = Visibility::visible();
|
||||
|
||||
if let Some(old_index) = workspace_state.iter().position(|w| {
|
||||
(w.visibility.is_focused() || w.visibility.is_visible())
|
||||
&& w.monitor == workspace_state[new_index].monitor
|
||||
}) {
|
||||
workspace_state[old_index].visibility = Visibility::Hidden;
|
||||
|
||||
vec![]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("No workspace with id for new focus/visible workspace found");
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
Ok(Event::Other) => {
|
||||
vec![]
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{err:?}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
for event in events {
|
||||
send!(tx, event);
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), Report>(())
|
||||
});
|
||||
|
||||
Self { tx: tx2, _rx: rx }
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceClient for Client {
|
||||
fn focus(&self, id: i64) {
|
||||
// this does annoyingly require spawning a separate connection for every focus call
|
||||
// the alternative is sticking the conn behind a mutex which could perform worse
|
||||
spawn(async move {
|
||||
let mut conn = Connection::connect().await?;
|
||||
|
||||
let command = Request::Action(Action::FocusWorkspace {
|
||||
reference: WorkspaceReferenceArg::Id(id as u64),
|
||||
});
|
||||
|
||||
if let Err(err) = conn.send(command).await {
|
||||
error!("failed to send command: {err:?}");
|
||||
}
|
||||
|
||||
Ok::<(), Report>(())
|
||||
});
|
||||
}
|
||||
|
||||
fn subscribe(&self) -> broadcast::Receiver<WorkspaceUpdate> {
|
||||
self.tx.subscribe()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue