Skip to content

Commit 5049226

Browse files
authored
Merge pull request #871 from JakeStanger/feat/niri-workspaces
feat(workspaces): niri support
2 parents 57bfab1 + 02a8dda commit 5049226

File tree

12 files changed

+367
-31
lines changed

12 files changed

+367
-31
lines changed

Diff for: Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,10 @@ upower = ["zbus", "futures-lite"]
8181
volume = ["libpulse-binding"]
8282

8383
workspaces = ["futures-lite"]
84-
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
84+
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland", "workspaces+niri"]
8585
"workspaces+sway" = ["workspaces", "sway"]
8686
"workspaces+hyprland" = ["workspaces", "hyprland"]
87+
"workspaces+niri" = ["workspaces"]
8788

8889
sway = ["swayipc-async"]
8990

Diff for: README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ Ironbar is designed to support anything from a lightweight bar to a full desktop
4848

4949
## Features
5050

51-
- First-class support for Sway and Hyprland
51+
- First-class support for Sway and Hyprland, and partial support for Niri
5252
- Fully themeable with hot-loaded CSS
5353
- Popups to show rich content
54-
- Ability to create custom widgets, run scripts and embed dynamic content
54+
- Ability to create custom widgets, run scripts and embed dynamic content (including via Lua)
5555
- Easy to configure anything from a single bar across all monitors, to multiple different unique bars per monitor
5656
- Support for multiple config languages
5757

Diff for: docs/Compiling.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ cargo build --release --no-default-features \
118118
| workspaces+all | Enables the `workspaces` module with support for all compositors. |
119119
| workspaces+sway | Enables the `workspaces` module with support for Sway. |
120120
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |
121+
| workspaces+niri | Enables the `workspaces` module with support for Niri. |
121122
| **Other** | |
122123
| schema | Enables JSON schema support and the CLI `--print-schema` flag. |
123124

@@ -200,4 +201,4 @@ codegen-backend = true
200201

201202
[profile.dev]
202203
codegen-backend = "cranelift"
203-
```
204+
```

Diff for: docs/modules/Workspaces.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
> **This module is currently only supported on Sway and Hyprland**
1+
> [!IMPORTANT]
2+
> This module is currently only supported on Sway, Hyprland and Niri**
23
34
Shows all current workspaces. Clicking a workspace changes focus to it.
45

Diff for: src/clients/compositor/hyprland.rs

+2-5
Original file line numberDiff line numberDiff line change
@@ -327,11 +327,8 @@ impl Client {
327327
}
328328

329329
impl WorkspaceClient for Client {
330-
fn focus(&self, id: String) {
331-
let identifier = id.parse::<i32>().map_or_else(
332-
|_| WorkspaceIdentifierWithSpecial::Name(&id),
333-
WorkspaceIdentifierWithSpecial::Id,
334-
);
330+
fn focus(&self, id: i64) {
331+
let identifier = WorkspaceIdentifierWithSpecial::Id(id as i32);
335332

336333
if let Err(e) = Dispatch::call(DispatchType::Workspace(identifier)) {
337334
error!("Couldn't focus workspace '{id}': {e:#}");

Diff for: src/clients/compositor/mod.rs

+21-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use tracing::debug;
88

99
#[cfg(feature = "workspaces+hyprland")]
1010
pub mod hyprland;
11+
#[cfg(feature = "workspaces+niri")]
12+
pub mod niri;
1113
#[cfg(feature = "workspaces+sway")]
1214
pub mod sway;
1315

@@ -16,6 +18,8 @@ pub enum Compositor {
1618
Sway,
1719
#[cfg(feature = "workspaces+hyprland")]
1820
Hyprland,
21+
#[cfg(feature = "workspaces+niri")]
22+
Niri,
1923
Unsupported,
2024
}
2125

@@ -29,6 +33,8 @@ impl Display for Compositor {
2933
Self::Sway => "Sway",
3034
#[cfg(feature = "workspaces+hyprland")]
3135
Self::Hyprland => "Hyprland",
36+
#[cfg(feature = "workspaces+niri")]
37+
Self::Niri => "Niri",
3238
Self::Unsupported => "Unsupported",
3339
}
3440
)
@@ -49,6 +55,11 @@ impl Compositor {
4955
if #[cfg(feature = "workspaces+hyprland")] { Self::Hyprland }
5056
else { tracing::error!("Not compiled with Hyprland support"); Self::Unsupported }
5157
}
58+
} else if std::env::var("NIRI_SOCKET").is_ok() {
59+
cfg_if! {
60+
if #[cfg(feature = "workspaces+niri")] { Self::Niri }
61+
else {tracing::error!("Not compiled with Niri support"); Self::Unsupported }
62+
}
5263
} else {
5364
Self::Unsupported
5465
}
@@ -68,7 +79,7 @@ impl Compositor {
6879
Self::Hyprland => clients
6980
.hyprland()
7081
.map(|client| client as Arc<dyn KeyboardLayoutClient + Send + Sync>),
71-
Self::Unsupported => Err(Report::msg("Unsupported compositor").note(
82+
Self::Niri | Self::Unsupported => Err(Report::msg("Unsupported compositor").note(
7283
"Currently keyboard layout functionality are only supported by Sway and Hyprland",
7384
)),
7485
}
@@ -90,8 +101,10 @@ impl Compositor {
90101
Self::Hyprland => clients
91102
.hyprland()
92103
.map(|client| client as Arc<dyn WorkspaceClient + Send + Sync>),
104+
#[cfg(feature = "workspaces+niri")]
105+
Self::Niri => Ok(Arc::new(niri::Client::new())),
93106
Self::Unsupported => Err(Report::msg("Unsupported compositor")
94-
.note("Currently workspaces are only supported by Sway and Hyprland")),
107+
.note("Currently workspaces are only supported by Sway, Niri and Hyprland")),
95108
}
96109
}
97110
}
@@ -125,6 +138,10 @@ impl Visibility {
125138
Self::Visible { focused: true }
126139
}
127140

141+
pub fn is_visible(self) -> bool {
142+
matches!(self, Self::Visible { .. })
143+
}
144+
128145
pub fn is_focused(self) -> bool {
129146
if let Self::Visible { focused } = self {
130147
focused
@@ -170,8 +187,8 @@ pub enum WorkspaceUpdate {
170187
}
171188

172189
pub trait WorkspaceClient: Debug + Send + Sync {
173-
/// Requests the workspace with this name is focused.
174-
fn focus(&self, name: String);
190+
/// Requests the workspace with this id is focused.
191+
fn focus(&self, id: i64);
175192

176193
/// Creates a new to workspace event receiver.
177194
fn subscribe(&self) -> broadcast::Receiver<WorkspaceUpdate>;

Diff for: src/clients/compositor/niri/connection.rs

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/// Taken from the `niri_ipc` crate.
2+
/// Only a relevant snippet has been extracted
3+
/// to reduce compile times.
4+
use crate::clients::compositor::Workspace as IronWorkspace;
5+
use crate::{await_sync, clients::compositor::Visibility};
6+
use color_eyre::eyre::{eyre, Result};
7+
use core::str;
8+
use serde::{Deserialize, Serialize};
9+
use std::{env, path::Path};
10+
use tokio::{
11+
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
12+
net::UnixStream,
13+
};
14+
15+
#[derive(Debug, Serialize, Deserialize, Clone)]
16+
pub enum Request {
17+
Action(Action),
18+
EventStream,
19+
}
20+
21+
pub type Reply = Result<Response, String>;
22+
23+
#[derive(Debug, Serialize, Deserialize, Clone)]
24+
pub enum Response {
25+
Handled,
26+
Workspaces(Vec<Workspace>),
27+
}
28+
29+
#[derive(Debug, Serialize, Deserialize, Clone)]
30+
pub enum Action {
31+
FocusWorkspace { reference: WorkspaceReferenceArg },
32+
}
33+
34+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
35+
pub enum WorkspaceReferenceArg {
36+
Name(String),
37+
Id(u64),
38+
}
39+
40+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
41+
pub struct Workspace {
42+
pub id: u64,
43+
pub idx: u8,
44+
pub name: Option<String>,
45+
pub output: Option<String>,
46+
pub is_active: bool,
47+
pub is_focused: bool,
48+
}
49+
50+
impl From<&Workspace> for IronWorkspace {
51+
fn from(workspace: &Workspace) -> IronWorkspace {
52+
// Workspaces in niri don't neccessarily have names.
53+
// If the niri workspace has a name then it is assigned as is,
54+
// but if it does not have a name, the monitor index is used.
55+
Self {
56+
id: workspace.id as i64,
57+
name: workspace.name.clone().unwrap_or(workspace.idx.to_string()),
58+
monitor: workspace.output.clone().unwrap_or_default(),
59+
visibility: if workspace.is_active {
60+
Visibility::Visible {
61+
focused: workspace.is_focused,
62+
}
63+
} else {
64+
Visibility::Hidden
65+
},
66+
}
67+
}
68+
}
69+
70+
#[derive(Serialize, Deserialize, Debug, Clone)]
71+
pub enum Event {
72+
WorkspacesChanged { workspaces: Vec<Workspace> },
73+
WorkspaceActivated { id: u64, focused: bool },
74+
Other,
75+
}
76+
77+
#[derive(Debug)]
78+
pub struct Connection(UnixStream);
79+
impl Connection {
80+
pub async fn connect() -> Result<Self> {
81+
let socket_path =
82+
env::var_os("NIRI_SOCKET").ok_or_else(|| eyre!("NIRI_SOCKET not found!"))?;
83+
Self::connect_to(socket_path).await
84+
}
85+
86+
pub async fn connect_to(path: impl AsRef<Path>) -> Result<Self> {
87+
let raw_stream = UnixStream::connect(path.as_ref()).await?;
88+
let stream = raw_stream;
89+
Ok(Self(stream))
90+
}
91+
92+
pub async fn send(
93+
&mut self,
94+
request: Request,
95+
) -> Result<(Reply, impl FnMut() -> Result<Event> + '_)> {
96+
let Self(stream) = self;
97+
let mut buf = serde_json::to_string(&request)?;
98+
99+
stream.write_all(buf.as_bytes()).await?;
100+
stream.shutdown().await?;
101+
102+
buf.clear();
103+
let mut reader = BufReader::new(stream);
104+
reader.read_line(&mut buf).await?;
105+
let reply = serde_json::from_str(&buf)?;
106+
107+
let events = move || {
108+
buf.clear();
109+
await_sync(async {
110+
reader.read_line(&mut buf).await.unwrap_or(0);
111+
});
112+
let event: Event = serde_json::from_str(&buf).unwrap_or(Event::Other);
113+
Ok(event)
114+
};
115+
Ok((reply, events))
116+
}
117+
}

0 commit comments

Comments
 (0)