mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-08-16 22:31:03 +02:00
Merge pull request #871 from JakeStanger/feat/niri-workspaces
feat(workspaces): niri support
This commit is contained in:
commit
5049226289
12 changed files with 367 additions and 31 deletions
|
@ -81,9 +81,10 @@ upower = ["zbus", "futures-lite"]
|
||||||
volume = ["libpulse-binding"]
|
volume = ["libpulse-binding"]
|
||||||
|
|
||||||
workspaces = ["futures-lite"]
|
workspaces = ["futures-lite"]
|
||||||
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland", "workspaces+niri"]
|
||||||
"workspaces+sway" = ["workspaces", "sway"]
|
"workspaces+sway" = ["workspaces", "sway"]
|
||||||
"workspaces+hyprland" = ["workspaces", "hyprland"]
|
"workspaces+hyprland" = ["workspaces", "hyprland"]
|
||||||
|
"workspaces+niri" = ["workspaces"]
|
||||||
|
|
||||||
sway = ["swayipc-async"]
|
sway = ["swayipc-async"]
|
||||||
|
|
||||||
|
|
|
@ -48,10 +48,10 @@ Ironbar is designed to support anything from a lightweight bar to a full desktop
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- First-class support for Sway and Hyprland
|
- First-class support for Sway and Hyprland, and partial support for Niri
|
||||||
- Fully themeable with hot-loaded CSS
|
- Fully themeable with hot-loaded CSS
|
||||||
- Popups to show rich content
|
- Popups to show rich content
|
||||||
- Ability to create custom widgets, run scripts and embed dynamic content
|
- Ability to create custom widgets, run scripts and embed dynamic content (including via Lua)
|
||||||
- Easy to configure anything from a single bar across all monitors, to multiple different unique bars per monitor
|
- Easy to configure anything from a single bar across all monitors, to multiple different unique bars per monitor
|
||||||
- Support for multiple config languages
|
- Support for multiple config languages
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,7 @@ cargo build --release --no-default-features \
|
||||||
| workspaces+all | Enables the `workspaces` module with support for all compositors. |
|
| workspaces+all | Enables the `workspaces` module with support for all compositors. |
|
||||||
| workspaces+sway | Enables the `workspaces` module with support for Sway. |
|
| workspaces+sway | Enables the `workspaces` module with support for Sway. |
|
||||||
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |
|
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |
|
||||||
|
| workspaces+niri | Enables the `workspaces` module with support for Niri. |
|
||||||
| **Other** | |
|
| **Other** | |
|
||||||
| schema | Enables JSON schema support and the CLI `--print-schema` flag. |
|
| schema | Enables JSON schema support and the CLI `--print-schema` flag. |
|
||||||
|
|
||||||
|
@ -200,4 +201,4 @@ codegen-backend = true
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
codegen-backend = "cranelift"
|
codegen-backend = "cranelift"
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
> ⚠ **This module is currently only supported on Sway and Hyprland**
|
> [!IMPORTANT]
|
||||||
|
> This module is currently only supported on Sway, Hyprland and Niri**
|
||||||
|
|
||||||
Shows all current workspaces. Clicking a workspace changes focus to it.
|
Shows all current workspaces. Clicking a workspace changes focus to it.
|
||||||
|
|
||||||
|
|
|
@ -327,11 +327,8 @@ impl Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkspaceClient for Client {
|
impl WorkspaceClient for Client {
|
||||||
fn focus(&self, id: String) {
|
fn focus(&self, id: i64) {
|
||||||
let identifier = id.parse::<i32>().map_or_else(
|
let identifier = WorkspaceIdentifierWithSpecial::Id(id as i32);
|
||||||
|_| WorkspaceIdentifierWithSpecial::Name(&id),
|
|
||||||
WorkspaceIdentifierWithSpecial::Id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Err(e) = Dispatch::call(DispatchType::Workspace(identifier)) {
|
if let Err(e) = Dispatch::call(DispatchType::Workspace(identifier)) {
|
||||||
error!("Couldn't focus workspace '{id}': {e:#}");
|
error!("Couldn't focus workspace '{id}': {e:#}");
|
||||||
|
|
|
@ -8,6 +8,8 @@ use tracing::debug;
|
||||||
|
|
||||||
#[cfg(feature = "workspaces+hyprland")]
|
#[cfg(feature = "workspaces+hyprland")]
|
||||||
pub mod hyprland;
|
pub mod hyprland;
|
||||||
|
#[cfg(feature = "workspaces+niri")]
|
||||||
|
pub mod niri;
|
||||||
#[cfg(feature = "workspaces+sway")]
|
#[cfg(feature = "workspaces+sway")]
|
||||||
pub mod sway;
|
pub mod sway;
|
||||||
|
|
||||||
|
@ -16,6 +18,8 @@ pub enum Compositor {
|
||||||
Sway,
|
Sway,
|
||||||
#[cfg(feature = "workspaces+hyprland")]
|
#[cfg(feature = "workspaces+hyprland")]
|
||||||
Hyprland,
|
Hyprland,
|
||||||
|
#[cfg(feature = "workspaces+niri")]
|
||||||
|
Niri,
|
||||||
Unsupported,
|
Unsupported,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +33,8 @@ impl Display for Compositor {
|
||||||
Self::Sway => "Sway",
|
Self::Sway => "Sway",
|
||||||
#[cfg(feature = "workspaces+hyprland")]
|
#[cfg(feature = "workspaces+hyprland")]
|
||||||
Self::Hyprland => "Hyprland",
|
Self::Hyprland => "Hyprland",
|
||||||
|
#[cfg(feature = "workspaces+niri")]
|
||||||
|
Self::Niri => "Niri",
|
||||||
Self::Unsupported => "Unsupported",
|
Self::Unsupported => "Unsupported",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -49,6 +55,11 @@ impl Compositor {
|
||||||
if #[cfg(feature = "workspaces+hyprland")] { Self::Hyprland }
|
if #[cfg(feature = "workspaces+hyprland")] { Self::Hyprland }
|
||||||
else { tracing::error!("Not compiled with Hyprland support"); Self::Unsupported }
|
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 {
|
} else {
|
||||||
Self::Unsupported
|
Self::Unsupported
|
||||||
}
|
}
|
||||||
|
@ -68,7 +79,7 @@ impl Compositor {
|
||||||
Self::Hyprland => clients
|
Self::Hyprland => clients
|
||||||
.hyprland()
|
.hyprland()
|
||||||
.map(|client| client as Arc<dyn KeyboardLayoutClient + Send + Sync>),
|
.map(|client| client as Arc<dyn KeyboardLayoutClient + Send + Sync>),
|
||||||
Self::Unsupported => Err(Report::msg("Unsupported compositor").note(
|
Self::Niri | Self::Unsupported => Err(Report::msg("Unsupported compositor").note(
|
||||||
"Currently keyboard layout functionality are only supported by Sway and Hyprland",
|
"Currently keyboard layout functionality are only supported by Sway and Hyprland",
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
|
@ -90,8 +101,10 @@ impl Compositor {
|
||||||
Self::Hyprland => clients
|
Self::Hyprland => clients
|
||||||
.hyprland()
|
.hyprland()
|
||||||
.map(|client| client as Arc<dyn WorkspaceClient + Send + Sync>),
|
.map(|client| client as Arc<dyn WorkspaceClient + Send + Sync>),
|
||||||
|
#[cfg(feature = "workspaces+niri")]
|
||||||
|
Self::Niri => Ok(Arc::new(niri::Client::new())),
|
||||||
Self::Unsupported => Err(Report::msg("Unsupported compositor")
|
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")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,6 +138,10 @@ impl Visibility {
|
||||||
Self::Visible { focused: true }
|
Self::Visible { focused: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_visible(self) -> bool {
|
||||||
|
matches!(self, Self::Visible { .. })
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_focused(self) -> bool {
|
pub fn is_focused(self) -> bool {
|
||||||
if let Self::Visible { focused } = self {
|
if let Self::Visible { focused } = self {
|
||||||
focused
|
focused
|
||||||
|
@ -170,8 +187,8 @@ pub enum WorkspaceUpdate {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WorkspaceClient: Debug + Send + Sync {
|
pub trait WorkspaceClient: Debug + Send + Sync {
|
||||||
/// Requests the workspace with this name is focused.
|
/// Requests the workspace with this id is focused.
|
||||||
fn focus(&self, name: String);
|
fn focus(&self, id: i64);
|
||||||
|
|
||||||
/// Creates a new to workspace event receiver.
|
/// Creates a new to workspace event receiver.
|
||||||
fn subscribe(&self) -> broadcast::Receiver<WorkspaceUpdate>;
|
fn subscribe(&self) -> broadcast::Receiver<WorkspaceUpdate>;
|
||||||
|
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,20 +2,36 @@ use super::{
|
||||||
KeyboardLayoutClient, KeyboardLayoutUpdate, Visibility, Workspace, WorkspaceClient,
|
KeyboardLayoutClient, KeyboardLayoutUpdate, Visibility, Workspace, WorkspaceClient,
|
||||||
WorkspaceUpdate,
|
WorkspaceUpdate,
|
||||||
};
|
};
|
||||||
|
use crate::clients::sway::Client;
|
||||||
use crate::{await_sync, error, send, spawn};
|
use crate::{await_sync, error, send, spawn};
|
||||||
|
use color_eyre::Report;
|
||||||
use swayipc_async::{InputChange, InputEvent, Node, WorkspaceChange, WorkspaceEvent};
|
use swayipc_async::{InputChange, InputEvent, Node, WorkspaceChange, WorkspaceEvent};
|
||||||
use tokio::sync::broadcast::{channel, Receiver};
|
use tokio::sync::broadcast::{channel, Receiver};
|
||||||
|
|
||||||
use crate::clients::sway::Client;
|
|
||||||
|
|
||||||
impl WorkspaceClient for Client {
|
impl WorkspaceClient for Client {
|
||||||
fn focus(&self, id: String) {
|
fn focus(&self, id: i64) {
|
||||||
let client = self.connection().clone();
|
let client = self.connection().clone();
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let mut client = client.lock().await;
|
let mut client = client.lock().await;
|
||||||
if let Err(e) = client.run_command(format!("workspace {id}")).await {
|
|
||||||
error!("Couldn't focus workspace '{id}': {e:#}");
|
let name = client
|
||||||
|
.get_workspaces()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.find(|w| w.id == id)
|
||||||
|
.map(|w| w.name);
|
||||||
|
|
||||||
|
let Some(name) = name else {
|
||||||
|
return Err(Report::msg(format!("couldn't find workspace with id {id}")));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = client.run_command(format!("workspace {name}")).await {
|
||||||
|
return Err(Report::msg(format!(
|
||||||
|
"Couldn't focus workspace '{id}': {e:#}"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +40,7 @@ impl WorkspaceClient for Client {
|
||||||
|
|
||||||
let client = self.connection().clone();
|
let client = self.connection().clone();
|
||||||
|
|
||||||
|
// TODO: this needs refactoring
|
||||||
await_sync(async {
|
await_sync(async {
|
||||||
let mut client = client.lock().await;
|
let mut client = client.lock().await;
|
||||||
let workspaces = client.get_workspaces().await.expect("to get workspaces");
|
let workspaces = client.get_workspaces().await.expect("to get workspaces");
|
||||||
|
@ -35,7 +52,7 @@ impl WorkspaceClient for Client {
|
||||||
|
|
||||||
drop(client);
|
drop(client);
|
||||||
|
|
||||||
self.add_listener::<swayipc_async::WorkspaceEvent>(move |event| {
|
self.add_listener::<WorkspaceEvent>(move |event| {
|
||||||
let update = WorkspaceUpdate::from(event.clone());
|
let update = WorkspaceUpdate::from(event.clone());
|
||||||
send!(tx, update);
|
send!(tx, update);
|
||||||
})
|
})
|
||||||
|
|
|
@ -22,9 +22,8 @@ impl Button {
|
||||||
|
|
||||||
let tx = context.tx.clone();
|
let tx = context.tx.clone();
|
||||||
|
|
||||||
let name = name.to_string();
|
|
||||||
button.connect_clicked(move |_item| {
|
button.connect_clicked(move |_item| {
|
||||||
try_send!(tx, name.clone());
|
try_send!(tx, id);
|
||||||
});
|
});
|
||||||
|
|
||||||
let btn = Self {
|
let btn = Self {
|
||||||
|
|
|
@ -12,6 +12,7 @@ pub enum Identifier {
|
||||||
/// Wrapper around a hashmap of workspace buttons,
|
/// Wrapper around a hashmap of workspace buttons,
|
||||||
/// which can be found using the workspace ID,
|
/// which can be found using the workspace ID,
|
||||||
/// or their name for favourites.
|
/// or their name for favourites.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct ButtonMap {
|
pub struct ButtonMap {
|
||||||
map: HashMap<Identifier, Button>,
|
map: HashMap<Identifier, Button>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,8 @@ pub struct WorkspacesModule {
|
||||||
///
|
///
|
||||||
/// If a workspace is not present in the map,
|
/// If a workspace is not present in the map,
|
||||||
/// it will fall back to using its actual name.
|
/// it will fall back to using its actual name.
|
||||||
name_map: Option<HashMap<String, String>>,
|
#[serde(default)]
|
||||||
|
name_map: HashMap<String, String>,
|
||||||
|
|
||||||
/// Workspaces which should always be shown.
|
/// Workspaces which should always be shown.
|
||||||
/// This can either be an array of workspace names,
|
/// This can either be an array of workspace names,
|
||||||
|
@ -140,7 +141,7 @@ pub struct WorkspaceItemContext {
|
||||||
name_map: HashMap<String, String>,
|
name_map: HashMap<String, String>,
|
||||||
icon_theme: IconTheme,
|
icon_theme: IconTheme,
|
||||||
icon_size: i32,
|
icon_size: i32,
|
||||||
tx: mpsc::Sender<String>,
|
tx: mpsc::Sender<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Re-orders the container children alphabetically,
|
/// Re-orders the container children alphabetically,
|
||||||
|
@ -182,7 +183,7 @@ fn reorder_workspaces(container: >k::Box, sort_order: SortOrder) {
|
||||||
|
|
||||||
impl Module<gtk::Box> for WorkspacesModule {
|
impl Module<gtk::Box> for WorkspacesModule {
|
||||||
type SendMessage = WorkspaceUpdate;
|
type SendMessage = WorkspaceUpdate;
|
||||||
type ReceiveMessage = String;
|
type ReceiveMessage = i64;
|
||||||
|
|
||||||
module_impl!("workspaces");
|
module_impl!("workspaces");
|
||||||
|
|
||||||
|
@ -212,8 +213,8 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
trace!("Setting up UI event handler");
|
trace!("Setting up UI event handler");
|
||||||
|
|
||||||
while let Some(name) = rx.recv().await {
|
while let Some(id) = rx.recv().await {
|
||||||
client.focus(name.clone());
|
client.focus(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<(), Report>(())
|
Ok::<(), Report>(())
|
||||||
|
@ -229,12 +230,10 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
) -> Result<ModuleParts<gtk::Box>> {
|
) -> Result<ModuleParts<gtk::Box>> {
|
||||||
let container = gtk::Box::new(info.bar_position.orientation(), 0);
|
let container = gtk::Box::new(info.bar_position.orientation(), 0);
|
||||||
|
|
||||||
let name_map = self.name_map.clone().unwrap_or_default();
|
|
||||||
|
|
||||||
let mut button_map = ButtonMap::new();
|
let mut button_map = ButtonMap::new();
|
||||||
|
|
||||||
let item_context = WorkspaceItemContext {
|
let item_context = WorkspaceItemContext {
|
||||||
name_map,
|
name_map: self.name_map.clone(),
|
||||||
icon_theme: info.icon_theme.clone(),
|
icon_theme: info.icon_theme.clone(),
|
||||||
icon_size: self.icon_size,
|
icon_size: self.icon_size,
|
||||||
tx: context.controller_tx.clone(),
|
tx: context.controller_tx.clone(),
|
||||||
|
@ -312,6 +311,7 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let name_map = self.name_map;
|
||||||
let mut handle_event = move |event: WorkspaceUpdate| match event {
|
let mut handle_event = move |event: WorkspaceUpdate| match event {
|
||||||
WorkspaceUpdate::Init(workspaces) => {
|
WorkspaceUpdate::Init(workspaces) => {
|
||||||
if has_initialized {
|
if has_initialized {
|
||||||
|
@ -381,7 +381,9 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
.or_else(|| button_map.get(&Identifier::Name(name.clone())))
|
.or_else(|| button_map.get(&Identifier::Name(name.clone())))
|
||||||
.map(Button::button)
|
.map(Button::button)
|
||||||
{
|
{
|
||||||
button.set_label(&name);
|
let display_name = name_map.get(&name).unwrap_or(&name);
|
||||||
|
|
||||||
|
button.set_label(display_name);
|
||||||
button.set_widget_name(&name);
|
button.set_widget_name(&name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue