mirror of
https://github.com/JakeStanger/ironbar.git
synced 2024-10-05 18:58:04 +03:00
Merge f53534baf8
into 8bae17a24a
This commit is contained in:
commit
997cb5eeb1
@ -75,9 +75,12 @@ 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", "niri"]
|
||||
|
||||
niri = []
|
||||
|
||||
sway = ["swayipc-async"]
|
||||
|
||||
|
@ -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;
|
||||
|
||||
@ -16,6 +18,8 @@ pub enum Compositor {
|
||||
Sway,
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
Hyprland,
|
||||
#[cfg(feature = "workspaces+niri")]
|
||||
Niri,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
@ -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",
|
||||
}
|
||||
)
|
||||
@ -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
|
||||
}
|
||||
@ -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")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
186
src/clients/compositor/niri.rs
Normal file
186
src/clients/compositor/niri.rs
Normal file
@ -0,0 +1,186 @@
|
||||
use crate::{
|
||||
await_sync,
|
||||
clients::{
|
||||
compositor::Visibility,
|
||||
niri::{Action, Connection, Event, Request, WorkspaceReferenceArg},
|
||||
},
|
||||
send, spawn,
|
||||
};
|
||||
use color_eyre::eyre::Result;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use tokio::sync::broadcast::channel;
|
||||
|
||||
use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client;
|
||||
|
||||
impl Client {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceClient for Client {
|
||||
fn focus(&self, name: String) -> Result<()> {
|
||||
await_sync(async {
|
||||
let mut conn = Connection::connect().await.unwrap();
|
||||
|
||||
let command = Request::Action(Action::FocusWorkspace {
|
||||
reference: WorkspaceReferenceArg::from_str(name.as_str()).unwrap(),
|
||||
});
|
||||
conn.send(command).await.unwrap().0.unwrap();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subscribe_workspace_change(
|
||||
&self,
|
||||
) -> tokio::sync::broadcast::Receiver<super::WorkspaceUpdate> {
|
||||
let (tx, rx) = channel(32);
|
||||
|
||||
spawn(async move {
|
||||
let mut conn = Connection::connect().await.unwrap();
|
||||
let mut event_listener = conn.send(Request::EventStream).await.unwrap().1;
|
||||
let mut workspace_state: Vec<Workspace> = Vec::new();
|
||||
let mut first_event = true;
|
||||
loop {
|
||||
let event_niri = event_listener().unwrap();
|
||||
let events = match event_niri {
|
||||
Event::WorkspacesChanged { workspaces } => {
|
||||
// Niri only has a WorkspacesChanged Event and Ironbar has 4 events which have to be handled: Add, Remove, Rename and Move. The way I am handling this here is by keeping a previous state of workspaces and comparing with the new state for changes. To do this efficiently, first I sort the new workspace state based on id. Then I do a linear scan on the states(old and new) togethor. At the end, I over write the old workspace state with the new one. Because of this, on the next event, the old workspace state is already sorted.
|
||||
let mut new_workspaces: Vec<Workspace> = workspaces
|
||||
.into_iter()
|
||||
.map(|w| Workspace::from(&w))
|
||||
.collect();
|
||||
let mut updates: Vec<WorkspaceUpdate> = vec![];
|
||||
if first_event {
|
||||
updates.push(WorkspaceUpdate::Init(new_workspaces.clone()));
|
||||
first_event = false;
|
||||
} else {
|
||||
new_workspaces.sort_by_key(|w| w.id);
|
||||
let mut old_index = 0;
|
||||
let mut new_index = 0;
|
||||
while old_index < workspace_state.len()
|
||||
&& new_index < new_workspaces.len()
|
||||
{
|
||||
let old_workspace = &workspace_state[old_index];
|
||||
let new_workspace = &new_workspaces[new_index];
|
||||
match old_workspace.id.cmp(&new_workspace.id) {
|
||||
std::cmp::Ordering::Greater => {
|
||||
// If there is a new id, I send a WorkspaceUpdate::Add event.
|
||||
updates.push(WorkspaceUpdate::Add(new_workspace.clone()));
|
||||
new_index += 1;
|
||||
}
|
||||
// If an id is missing, then I send a WorkspaceUpdate::Remove event.
|
||||
std::cmp::Ordering::Less => {
|
||||
updates.push(WorkspaceUpdate::Remove(old_workspace.id));
|
||||
old_index += 1;
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
// For workspaces with the same id, if the name of the workspace is different, WorkspaceUpdate::Rename is sent, if the name of the monitor is different then WorkspaceUpdate::Move is sent.
|
||||
if old_workspace.name != new_workspace.name {
|
||||
updates.push(WorkspaceUpdate::Rename {
|
||||
id: new_workspace.id,
|
||||
name: new_workspace.name.clone(),
|
||||
});
|
||||
}
|
||||
if old_workspace.monitor != new_workspace.monitor {
|
||||
updates
|
||||
.push(WorkspaceUpdate::Move(new_workspace.clone()));
|
||||
}
|
||||
old_index += 1;
|
||||
new_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle remaining workspaces
|
||||
while old_index < workspace_state.len() {
|
||||
updates
|
||||
.push(WorkspaceUpdate::Remove(workspace_state[old_index].id));
|
||||
old_index += 1;
|
||||
}
|
||||
while new_index < new_workspaces.len() {
|
||||
updates
|
||||
.push(WorkspaceUpdate::Add(new_workspaces[new_index].clone()));
|
||||
new_index += 1;
|
||||
}
|
||||
}
|
||||
workspace_state = new_workspaces;
|
||||
updates
|
||||
}
|
||||
Event::WorkspaceActivated { id, focused } => {
|
||||
// workspace with id is activated, if focus is true then it is also focused
|
||||
// if focuesd is true then focus has changed => find old focused workspace. set it to inactive and set current
|
||||
match workspace_state.iter().position(|w| w.id == id as i64) {
|
||||
Some(new_index) => {
|
||||
if focused {
|
||||
match workspace_state
|
||||
.iter()
|
||||
.position(|w| w.visibility.is_focused())
|
||||
{
|
||||
Some(old_index) => {
|
||||
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(),
|
||||
}]
|
||||
}
|
||||
None => {
|
||||
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();
|
||||
match workspace_state.iter().position(|w| {
|
||||
(w.visibility.is_focused() || w.visibility.is_visible())
|
||||
&& w.monitor == workspace_state[new_index].monitor
|
||||
}) {
|
||||
Some(old_index) => {
|
||||
workspace_state[old_index].visibility =
|
||||
Visibility::Hidden;
|
||||
vec![]
|
||||
}
|
||||
None => {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
tracing::warn!(
|
||||
"No workspace with id for new focus/visible workspace found"
|
||||
);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Other => {
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
for event in events {
|
||||
send!(tx, event);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(30));
|
||||
}
|
||||
});
|
||||
rx
|
||||
}
|
||||
}
|
@ -14,6 +14,8 @@ pub mod lua;
|
||||
pub mod music;
|
||||
#[cfg(feature = "network_manager")]
|
||||
pub mod networkmanager;
|
||||
#[cfg(feature = "niri")]
|
||||
pub mod niri;
|
||||
#[cfg(feature = "sway")]
|
||||
pub mod sway;
|
||||
#[cfg(feature = "notifications")]
|
||||
|
126
src/clients/niri.rs
Normal file
126
src/clients/niri.rs
Normal file
@ -0,0 +1,126 @@
|
||||
use core::str;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{env, io, path::Path};
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
net::UnixStream,
|
||||
};
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::await_sync;
|
||||
|
||||
use super::compositor::{self, Visibility};
|
||||
|
||||
#[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 compositor::Workspace {
|
||||
fn from(workspace: &Workspace) -> compositor::Workspace {
|
||||
// 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.
|
||||
compositor::Workspace {
|
||||
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() -> io::Result<Self> {
|
||||
let socket_path = env::var_os("NIRI_SOCKET")
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "NIRI_SOCKET not found!"))?;
|
||||
Self::connect_to(socket_path).await
|
||||
}
|
||||
|
||||
pub async fn connect_to(path: impl AsRef<Path>) -> io::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,
|
||||
) -> io::Result<(Reply, impl FnMut() -> io::Result<Event> + '_)> {
|
||||
let Self(stream) = self;
|
||||
let mut buf = serde_json::to_string(&request).unwrap();
|
||||
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();
|
||||
});
|
||||
let event: Event = serde_json::from_str(&buf).unwrap_or(Event::Other);
|
||||
Ok(event)
|
||||
};
|
||||
Ok((reply, events))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user