Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support For Niri Workspaces #726

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@ upower = ["upower_dbus", "zbus", "futures-lite"]
volume = ["libpulse-binding"]

workspaces = ["futures-lite"]
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland", "workspaces+niri"]
"workspaces+sway" = ["workspaces", "sway"]
"workspaces+hyprland" = ["workspaces", "hyprland"]
"workspaces+niri" = ["workspaces"]

sway = ["swayipc-async"]

Expand Down Expand Up @@ -174,4 +175,4 @@ schemars = { version = "0.8.21", optional = true }

# -- PATCH --
# temp fix for tracing-appender/time
time = "0.3.36"
time = "0.3.36"
15 changes: 14 additions & 1 deletion src/clients/compositor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use tracing::debug;

#[cfg(feature = "workspaces+hyprland")]
pub mod hyprland;
#[cfg(feature = "workspaces+niri")]
pub mod niri;
#[cfg(feature = "workspaces+sway")]
pub mod sway;

Expand All @@ -16,6 +18,8 @@ pub enum Compositor {
Sway,
#[cfg(feature = "workspaces+hyprland")]
Hyprland,
#[cfg(feature = "workspaces+niri")]
Niri,
Unsupported,
}

Expand All @@ -29,6 +33,8 @@ impl Display for Compositor {
Self::Sway => "Sway",
#[cfg(feature = "workspaces+hyprland")]
Self::Hyprland => "Hyprland",
#[cfg(feature = "workspaces+niri")]
Self::Niri => "Niri",
Self::Unsupported => "Unsupported",
}
)
Expand All @@ -49,6 +55,11 @@ impl Compositor {
if #[cfg(feature = "workspaces+hyprland")] { Self::Hyprland }
else { tracing::error!("Not compiled with Hyprland support"); Self::Unsupported }
}
} else if std::env::var("NIRI_SOCKET").is_ok() {
cfg_if! {
if #[cfg(feature = "workspaces+niri")] { Self::Niri }
else {tracing::error!("Not compiled with Niri support"); Self::Unsupported }
}
} else {
Self::Unsupported
}
Expand All @@ -68,8 +79,10 @@ impl Compositor {
.map(|client| client as Arc<dyn WorkspaceClient + Send + Sync>),
#[cfg(feature = "workspaces+hyprland")]
Self::Hyprland => Ok(Arc::new(hyprland::Client::new())),
#[cfg(feature = "workspaces+niri")]
Self::Niri => Ok(Arc::new(niri::Client::new())),
Self::Unsupported => Err(Report::msg("Unsupported compositor")
.note("Currently workspaces are only supported by Sway and Hyprland")),
.note("Currently workspaces are only supported by Sway, Niri and Hyprland")),
}
}
}
Expand Down
124 changes: 124 additions & 0 deletions src/clients/compositor/niri/connection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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::str::FromStr;
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 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. So if the niri workspace has a name then it is assigned as is but if it does not have a name, the id is assigned as name.
IronWorkspace {
id: workspace.id as i64,
name: workspace.name.clone().unwrap_or(workspace.id.to_string()),
monitor: workspace.output.clone().unwrap_or_default(),
visibility: match workspace.is_focused {
true => Visibility::focused(),
false => match workspace.is_active {
true => Visibility::visible(),
false => Visibility::Hidden,
},
},
}
}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Event {
WorkspacesChanged { workspaces: Vec<Workspace> },
WorkspaceActivated { id: u64, focused: bool },
Other,
}

impl FromStr for WorkspaceReferenceArg {
type Err = &'static str;
// When a WorkspaceReferenceArg is parsed from a string(name), if it parses to a u64, it means that the workspace did not have a name but an id and it is handled as an id.
fn from_str(s: &str) -> Result<Self, Self::Err> {
let reference = if let Ok(id) = s.parse::<u64>() {
Self::Id(id)
} else {
Self::Name(s.to_string())
};
Ok(reference)
}
}

#[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))
}
}
Loading
Loading