diff --git a/Cargo.lock b/Cargo.lock index d31563b..8e0a49b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,6 +280,19 @@ dependencies = [ "system-deps", ] +[[package]] +name = "calloop" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22a6a8f622f797120d452c630b0ab12e1331a1a753e2039ce7868d4ac77b4ee" +dependencies = [ + "log", + "nix 0.24.2", + "slotmap", + "thiserror", + "vec_map", +] + [[package]] name = "cc" version = "1.0.73" @@ -594,6 +607,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "dlib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + [[package]] name = "either" version = "1.7.0" @@ -1122,6 +1150,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "smithay-client-toolkit", "stray", "strip-ansi-escapes", "swayipc-async", @@ -1133,6 +1162,8 @@ dependencies = [ "tracing-error", "tracing-subscriber", "walkdir", + "wayland-client", + "wayland-protocols", ] [[package]] @@ -1173,6 +1204,16 @@ version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +[[package]] +name = "libloading" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "lock_api" version = "0.4.7" @@ -1207,6 +1248,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95af15f345b17af2efc8ead6080fb8bc376f8cec1b35277b935637595fe77498" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.6.5" @@ -1281,6 +1331,18 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "7.1.1" @@ -1720,6 +1782,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + [[package]] name = "scopeguard" version = "1.1.0" @@ -1843,12 +1911,40 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +[[package]] +name = "smithay-client-toolkit" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f307c47d32d2715eb2e0ece5589057820e0e5e70d07c247d1063e844e107f454" +dependencies = [ + "bitflags", + "calloop", + "dlib", + "lazy_static", + "log", + "memmap2", + "nix 0.24.2", + "pkg-config", + "wayland-client", + "wayland-cursor", + "wayland-protocols", +] + [[package]] name = "socket2" version = "0.4.4" @@ -2041,9 +2137,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.21.0" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89797afd69d206ccd11fb0ea560a44bbb87731d020670e79416d442919257d42" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg", "bytes", @@ -2051,7 +2147,6 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", "pin-project-lite", "socket2", "tokio-macros", @@ -2218,6 +2313,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version-compare" version = "0.1.0" @@ -2280,6 +2381,79 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wayland-client" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" +dependencies = [ + "bitflags", + "downcast-rs", + "libc", + "nix 0.24.2", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys", +] + +[[package]] +name = "wayland-commons" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" +dependencies = [ + "nix 0.24.2", + "once_cell", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-cursor" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" +dependencies = [ + "nix 0.24.2", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" +dependencies = [ + "bitflags", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-sys" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" +dependencies = [ + "dlib", + "lazy_static", + "pkg-config", +] + [[package]] name = "wepoll-ffi" version = "0.1.2" @@ -2363,6 +2537,21 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "xcursor" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" +dependencies = [ + "nom", +] + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + [[package]] name = "zbus" version = "2.3.2" @@ -2386,7 +2575,7 @@ dependencies = [ "futures-util", "hex", "lazy_static", - "nix", + "nix 0.23.1", "once_cell", "ordered-stream", "rand", diff --git a/Cargo.toml b/Cargo.toml index b4863ad..dd909f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ derive_builder = "0.11.2" gtk = "0.15.5" gtk-layer-shell = "0.4.1" glib = "0.15.12" -tokio = { version = "1.21.0", features = ["macros", "rt-multi-thread", "time"] } +tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time"] } tracing = "0.1.36" tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } tracing-error = "0.2.0" @@ -35,4 +35,7 @@ walkdir = "2.3.2" notify = "5.0.0" mpd_client = "1.0.0" swayipc-async = { git = "https://github.com/JakeStanger/swayipc-rs.git", branch = "feat/derive-clone" } -sysinfo = "0.26.2" \ No newline at end of file +sysinfo = "0.26.2" +wayland-client = "0.29.5" +wayland-protocols = { version = "0.29.5", features=["unstable_protocols", "client"] } +smithay-client-toolkit = "0.16.0" \ No newline at end of file diff --git a/README.md b/README.md index b5a080f..f369f6f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Ironbar -Ironbar is a customisable and feature-rich bar targeting the Sway compositor, written in Rust. +Ironbar is a customisable and feature-rich bar targeting wlroots compositors, written in Rust. It uses GTK3 and gtk-layer-shell. -The bar can be styled to your liking using CSS and hot-loads style changes. +The bar can be styled to your liking using CSS and hot-loads style changes. For information and examples on styling please see the [wiki](https://github.com/JakeStanger/ironbar/wiki). ![Screenshot of fully configured bar with MPD widget open](https://user-images.githubusercontent.com/5057870/184539623-92d56a44-a659-49a9-91f9-5cdc453e5dfb.png) @@ -42,29 +42,30 @@ install target/release/ironbar ~/.local/bin/ironbar ## Configuration -Ironbar gives a lot of flexibility when configuring, including multiple file formats -and options for scaling complexity: you can use a single config across all monitors, -or configure different/multiple bars per monitor. +Ironbar gives a lot of flexibility when configuring, including multiple file formats +and options for scaling complexity: you can use a single config across all monitors, +or configure different/multiple bars per monitor. A full configuration guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/configuration-guide). ## Styling -To get started, create a stylesheet at `.config/ironbar/style.css`. Changes will be hot-reloaded every time you save the file. +To get started, create a stylesheet at `.config/ironbar/style.css`. Changes will be hot-reloaded every time you save the +file. A full styling guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/styling-guide). ## Project Status -This project is in alpha, but should be usable. -Everything that is implemented works and should be documented. +This project is in alpha, but should be usable. +Everything that is implemented works and should be documented. Proper error handling is in place so things should either fail gracefully with detail, or not fail at all. There is currently room for lots more modules, and lots more configuration options for the existing modules. The current configuration schema is not set in stone and breaking changes could come along at any point; until the project matures I am more interested in ease of use than backwards compatibility. -A few bugs do exist, and I am sure there are plenty more to be found. +A few bugs do exist, and I am sure there are plenty more to be found. The project will be *actively developed* as I am using it on my daily driver. Bugs will be fixed, features will be added, code will be refactored. @@ -77,3 +78,4 @@ Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUT - [Waybar](https://github.com/Alexays/Waybar) - A lot of the initial inspiration, and a pretty great bar. - [Rustbar](https://github.com/zeroeightysix/rustbar) - Served as a good demo for writing a basic GTK bar in Rust +- [Smithay Client Toolkit](https://github.com/Smithay/client-toolkit) - Essential in being able to communicate to Wayland \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 419ea78..108013d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,11 +8,11 @@ mod modules; mod popup; mod style; mod sway; +mod wayland; use crate::bar::create_bar; use crate::config::{Config, MonitorConfig}; use crate::style::load_css; -use crate::sway::get_client; use color_eyre::eyre::Result; use color_eyre::Report; use dirs::config_dir; @@ -27,6 +27,7 @@ use tokio::task::block_in_place; use crate::logging::install_tracing; use tracing::{debug, error, info}; +use wayland::WaylandClient; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -46,6 +47,8 @@ async fn main() -> Result<()> { info!("Ironbar version {}", VERSION); info!("Starting application"); + let wayland_client = wayland::get_client().await; + let app = Application::builder() .application_id("dev.jstanger.ironbar") .build(); @@ -69,7 +72,7 @@ async fn main() -> Result<()> { }; debug!("Loaded config file"); - if let Err(err) = await_sync(create_bars(app, &display, &config)) { + if let Err(err) = create_bars(app, &display, wayland_client, &config) { error!("{:?}", err); exit(2); } @@ -99,26 +102,23 @@ async fn main() -> Result<()> { } /// Creates each of the bars across each of the (configured) outputs. -async fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<()> { - let outputs = { - let sway = get_client().await; - let mut sway = sway.lock().await; +fn create_bars( + app: &Application, + display: &Display, + wl: &WaylandClient, + config: &Config, +) -> Result<()> { + let outputs = wl.outputs.as_slice(); - let outputs = sway.get_outputs().await; - - match outputs { - Ok(outputs) => Ok(outputs), - Err(err) => Err(err), - } - }?; - - debug!("Received {} outputs from Sway IPC", outputs.len()); + debug!("Received {} outputs from Wayland", outputs.len()); + debug!("Output names: {:?}", outputs); let num_monitors = display.n_monitors(); for i in 0..num_monitors { let monitor = display.monitor(i).ok_or_else(|| Report::msg("GTK and Sway are reporting a different number of outputs - this is a severe bug and should never happen"))?; - let monitor_name = &outputs.get(i as usize).ok_or_else(|| Report::msg("GTK and Sway are reporting a different set of outputs - this is a severe bug and should never happen"))?.name; + let output = outputs.get(i as usize).ok_or_else(|| Report::msg("GTK and Sway are reporting a different set of outputs - this is a severe bug and should never happen"))?; + let monitor_name = &output.name; info!("Creating bar on '{}'", monitor_name); diff --git a/src/modules/focused.rs b/src/modules/focused.rs index e1cc5f7..39e577d 100644 --- a/src/modules/focused.rs +++ b/src/modules/focused.rs @@ -1,16 +1,13 @@ use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; -use crate::sway::node::{get_node_id, get_open_windows}; -use crate::sway::{get_client, get_sub_client}; -use crate::{await_sync, icon}; +use crate::wayland::ToplevelChange; +use crate::{await_sync, icon, wayland}; use color_eyre::Result; use glib::Continue; use gtk::prelude::*; use gtk::{IconTheme, Image, Label, Orientation}; use serde::Deserialize; -use swayipc_async::WindowChange; use tokio::spawn; use tokio::sync::mpsc::{Receiver, Sender}; -use tracing::trace; #[derive(Debug, Deserialize, Clone)] pub struct FocusedModule { @@ -43,50 +40,39 @@ impl Module for FocusedModule { _rx: Receiver, ) -> Result<()> { let focused = await_sync(async { - let sway = get_client().await; - let mut sway = sway.lock().await; - get_open_windows(&mut sway) - .await - .expect("Failed to get open windows") - .into_iter() - .find(|node| node.focused) + let wl = wayland::get_client().await; + let toplevels = wl + .toplevels + .read() + .expect("Failed to get read lock on toplevels") + .clone(); + + toplevels.into_iter().find(|(top, _)| top.active) }); - if let Some(node) = focused { - let id = get_node_id(&node); - let name = node.name.as_deref().unwrap_or(id); - - tx.try_send(ModuleUpdateEvent::Update(( - name.to_string(), - id.to_string(), - )))?; + if let Some((top, _)) = focused { + tx.try_send(ModuleUpdateEvent::Update((top.title.clone(), top.app_id)))?; } spawn(async move { - let mut srx = { - let sway = get_sub_client(); - sway.subscribe_window() + let mut wlrx = { + let wl = wayland::get_client().await; + wl.subscribe_toplevels() }; - trace!("Set up Sway window subscription"); - - while let Ok(payload) = srx.recv().await { - let update = match payload.change { - WindowChange::Focus => true, - WindowChange::Title => payload.container.focused, + while let Ok(event) = wlrx.recv().await { + let update = match event.change { + ToplevelChange::Focus(focus) => focus, + ToplevelChange::Title(_) => event.toplevel.active, _ => false, }; if update { - let node = payload.container; - - let id = get_node_id(&node); - let name = node.name.as_deref().unwrap_or(id); - - tx.try_send(ModuleUpdateEvent::Update(( - name.to_string(), - id.to_string(), + tx.send(ModuleUpdateEvent::Update(( + event.toplevel.title, + event.toplevel.app_id, ))) + .await .expect("Failed to send focus update"); } } diff --git a/src/modules/launcher/item.rs b/src/modules/launcher/item.rs index 99964a2..0e4fdd9 100644 --- a/src/modules/launcher/item.rs +++ b/src/modules/launcher/item.rs @@ -4,12 +4,11 @@ use crate::icon::get_icon; use crate::modules::launcher::{ItemEvent, LauncherUpdate}; use crate::modules::ModuleUpdateEvent; use crate::popup::Popup; -use crate::sway::node::{get_node_id, is_node_xwayland}; +use crate::wayland::ToplevelInfo; use gtk::prelude::*; use gtk::{Button, IconTheme, Image}; use std::rc::Rc; use std::sync::RwLock; -use swayipc_async::Node; use tokio::sync::mpsc::Sender; #[derive(Debug, Clone)] @@ -17,9 +16,8 @@ pub struct Item { pub app_id: String, pub favorite: bool, pub open_state: OpenState, - pub windows: Collection, - pub name: Option, - pub is_xwayland: bool, + pub windows: Collection, + pub name: String, } impl Item { @@ -29,21 +27,18 @@ impl Item { favorite, open_state, windows: Collection::new(), - name: None, - is_xwayland: false, + name: String::new(), } } /// Merges the provided node into this launcher item - pub fn merge_node(&mut self, node: Node) -> Window { + pub fn merge_toplevel(&mut self, node: ToplevelInfo) -> Window { let id = node.id; if self.windows.is_empty() { - self.name = node.name.clone(); + self.name = node.title.clone(); } - self.is_xwayland = self.is_xwayland || is_node_xwayland(&node); - let window: Window = node.into(); self.windows.insert(id, window.clone()); @@ -52,16 +47,12 @@ impl Item { window } - pub fn unmerge_node(&mut self, node: &Node) { + pub fn unmerge_toplevel(&mut self, node: &ToplevelInfo) { self.windows.remove(&node.id); self.recalculate_open_state(); } - pub fn get_name(&self) -> &str { - self.name.as_ref().unwrap_or(&self.app_id) - } - - pub fn set_window_name(&mut self, window_id: i64, name: Option) { + pub fn set_window_name(&mut self, window_id: usize, name: String) { if let Some(window) = self.windows.get_mut(&window_id) { if let OpenState::Open { focused: true, .. } = window.open_state { self.name = name.clone(); @@ -71,23 +62,7 @@ impl Item { } } - pub fn set_unfocused(&mut self) { - let focused = self - .windows - .iter_mut() - .find(|window| window.open_state.is_focused()); - - if let Some(focused) = focused { - focused.open_state = OpenState::Open { - focused: false, - urgent: focused.open_state.is_urgent(), - }; - - self.recalculate_open_state(); - } - } - - pub fn set_window_focused(&mut self, window_id: i64, focused: bool) { + pub fn set_window_focused(&mut self, window_id: usize, focused: bool) { if let Some(window) = self.windows.get_mut(&window_id) { window.open_state = OpenState::merge_states(&[&window.open_state, &OpenState::focused(focused)]); @@ -96,15 +71,6 @@ impl Item { } } - pub fn set_window_urgent(&mut self, window_id: i64, urgent: bool) { - if let Some(window) = self.windows.get_mut(&window_id) { - window.open_state = - OpenState::merge_states(&[&window.open_state, &OpenState::urgent(urgent)]); - - self.recalculate_open_state(); - } - } - /// Sets this item's open state /// to the merged result of its windows' open states fn recalculate_open_state(&mut self) { @@ -119,16 +85,14 @@ impl Item { } } -impl From for Item { - fn from(node: Node) -> Self { - let app_id = get_node_id(&node).to_string(); - let open_state = OpenState::from_node(&node); - let name = node.name.clone(); - - let is_xwayland = is_node_xwayland(&node); +impl From for Item { + fn from(toplevel: ToplevelInfo) -> Self { + let open_state = OpenState::from_toplevel(&toplevel); + let name = toplevel.title.clone(); + let app_id = toplevel.app_id.clone(); let mut windows = Collection::new(); - windows.insert(node.id, node.into()); + windows.insert(toplevel.id, toplevel.into()); Self { app_id, @@ -136,25 +100,24 @@ impl From for Item { open_state, windows, name, - is_xwayland, } } } #[derive(Clone, Debug)] pub struct Window { - pub id: i64, - pub name: Option, + pub id: usize, + pub name: String, pub open_state: OpenState, } -impl From for Window { - fn from(node: Node) -> Self { - let open_state = OpenState::from_node(&node); +impl From for Window { + fn from(node: ToplevelInfo) -> Self { + let open_state = OpenState::from_toplevel(&node); Self { id: node.id, - name: node.name, + name: node.title, open_state, } } @@ -183,7 +146,7 @@ impl ItemButton { let mut button = Button::builder(); if show_names { - button = button.label(item.get_name()); + button = button.label(&item.name); } if show_icons { @@ -208,9 +171,6 @@ impl ItemButton { if item.open_state.is_focused() { style_context.add_class("focused"); } - if item.open_state.is_urgent() { - style_context.add_class("urgent"); - } { let app_id = item.app_id.clone(); @@ -274,7 +234,6 @@ impl ItemButton { if !open { self.set_focused(false); - self.set_urgent(false); } } @@ -282,10 +241,6 @@ impl ItemButton { self.update_class("focused", focused); } - pub fn set_urgent(&self, urgent: bool) { - self.update_class("urgent", urgent); - } - /// Adds or removes a class to the button based on `toggle`. fn update_class(&self, class: &str, toggle: bool) { let style_context = self.button.style_context(); diff --git a/src/modules/launcher/mod.rs b/src/modules/launcher/mod.rs index a14b563..5070bb8 100644 --- a/src/modules/launcher/mod.rs +++ b/src/modules/launcher/mod.rs @@ -1,14 +1,13 @@ mod item; mod open_state; +use self::item::{Item, ItemButton, Window}; +use self::open_state::OpenState; use crate::collection::Collection; use crate::icon::find_desktop_file; -use crate::modules::launcher::item::{Item, ItemButton, Window}; -use crate::modules::launcher::open_state::OpenState; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; -use crate::sway::get_sub_client; -use crate::sway::node::{get_node_id, get_open_windows}; -use crate::{await_sync, get_client}; +use crate::wayland; +use crate::wayland::ToplevelChange; use color_eyre::{Help, Report}; use glib::Continue; use gtk::prelude::*; @@ -16,7 +15,6 @@ use gtk::{Button, IconTheme, Orientation}; use serde::Deserialize; use std::process::{Command, Stdio}; use std::sync::{Arc, Mutex}; -use swayipc_async::WindowChange; use tokio::spawn; use tokio::sync::mpsc; use tokio::sync::mpsc::{Receiver, Sender}; @@ -47,13 +45,11 @@ pub enum LauncherUpdate { /// Removes item with `app_id` RemoveItem(String), /// Removes window from item with `app_id`. - RemoveWindow(String, i64), + RemoveWindow(String, usize), /// Sets title for `app_id` - Title(String, i64, Option), - /// Focuses first `app_id`, unfocuses second `app_id` (if present) - Focus(String, Option), - /// Marks the item with `app_id` as urgent or not urgent - Urgent(String, bool), + Title(String, usize, String), + /// Marks the item with `app_id` as focused or not focused + Focus(String, bool), /// Declares the item with `app_id` has been hovered over Hover(String), } @@ -61,7 +57,7 @@ pub enum LauncherUpdate { #[derive(Debug)] pub enum ItemEvent { FocusItem(String), - FocusWindow(i64), + FocusWindow(usize), OpenItem(String), } @@ -100,69 +96,74 @@ impl Module for LauncherModule { let items = Arc::new(Mutex::new(items)); - let open_windows = await_sync(async { - let sway = get_client().await; - let mut sway = sway.lock().await; - get_open_windows(&mut sway).await - })?; - { - let mut items = items.lock().expect("Failed to get lock on items"); - for window in open_windows { - let id = get_node_id(&window).to_string(); + let items = Arc::clone(&items); + let tx = tx.clone(); + spawn(async move { + let wl = wayland::get_client().await; + let open_windows = wl + .toplevels + .read() + .expect("Failed to get read lock on toplevels"); - let item = items.get_mut(&id); - match item { - Some(item) => { - item.merge_node(window); - } - None => { - items.insert(id, window.into()); + let mut items = items.lock().expect("Failed to get lock on items"); + + for (window, _) in open_windows.clone() { + let item = items.get_mut(&window.app_id); + match item { + Some(item) => { + item.merge_toplevel(window); + } + None => { + items.insert(window.app_id.clone(), window.into()); + } } } - } - let items = items.iter(); - for item in items { - tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem( - item.clone(), - )))?; - } + let items = items.iter(); + for item in items { + tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem( + item.clone(), + )))?; + } + + Ok::<(), Report>(()) + }); } let items2 = Arc::clone(&items); spawn(async move { let items = items2; - let mut srx = { - let sway = get_sub_client(); - sway.subscribe_window() + let mut wlrx = { + let wl = wayland::get_client().await; + wl.subscribe_toplevels() }; - while let Ok(event) = srx.recv().await { + let send_update = |update: LauncherUpdate| tx.send(ModuleUpdateEvent::Update(update)); + + while let Ok(event) = wlrx.recv().await { trace!("event: {:?}", event); - let window = event.container; - let id = get_node_id(&window).to_string(); - - let send_update = - |update: LauncherUpdate| tx.send(ModuleUpdateEvent::Update(update)); + let window = event.toplevel; + let app_id = window.app_id.clone(); let items = || items.lock().expect("Failed to get lock on items"); match event.change { - WindowChange::New => { + ToplevelChange::New => { let new_item = { let mut items = items(); - match items.get_mut(&id) { + let item = items.get_mut(&app_id); + match item { None => { let item: Item = window.into(); - items.insert(id.clone(), item.clone()); + items.insert(app_id.clone(), item.clone()); ItemOrWindow::Item(item) } Some(item) => { - let window = item.merge_node(window); + let window = item.merge_toplevel(window); ItemOrWindow::Window(window) } } @@ -173,19 +174,20 @@ impl Module for LauncherModule { send_update(LauncherUpdate::AddItem(item)).await } ItemOrWindow::Window(window) => { - send_update(LauncherUpdate::AddWindow(id, window)).await + send_update(LauncherUpdate::AddWindow(app_id, window)).await } }?; } - WindowChange::Close => { + ToplevelChange::Close => { let remove_item = { let mut items = items(); - match items.get_mut(&id) { + let item = items.get_mut(&app_id); + match item { Some(item) => { - item.unmerge_node(&window); + item.unmerge_toplevel(&window); if item.windows.is_empty() { - items.remove(&id); + items.remove(&app_id); Some(ItemOrWindowId::Item) } else { Some(ItemOrWindowId::Window) @@ -197,60 +199,49 @@ impl Module for LauncherModule { match remove_item { Some(ItemOrWindowId::Item) => { - send_update(LauncherUpdate::RemoveItem(id)).await?; + send_update(LauncherUpdate::RemoveItem(app_id)).await?; } Some(ItemOrWindowId::Window) => { - send_update(LauncherUpdate::RemoveWindow(id, window.id)).await?; + send_update(LauncherUpdate::RemoveWindow(app_id, window.id)) + .await?; } None => {} }; } - WindowChange::Focus => { - let prev_id = { - let mut items = items(); + ToplevelChange::Focus(focused) => { + let update_title = if focused { + if let Some(item) = items().get_mut(&app_id) { + item.set_window_focused(window.id, true); - let prev_focused = - items.iter_mut().find(|item| item.open_state.is_focused()); - if let Some(prev_focused) = prev_focused { - prev_focused.set_unfocused(); - Some(prev_focused.app_id.to_string()) + // might be switching focus between windows of same app + if item.windows.len() > 1 { + item.set_window_name(window.id, window.title.clone()); + true + } else { + false + } } else { - None + false } + } else { + false }; - let mut update_title = false; - if let Some(item) = items().get_mut(&id) { - item.set_window_focused(window.id, true); - - // might be switching focus between windows of same app - if item.windows.len() > 1 { - item.set_window_name(window.id, window.name.clone()); - update_title = true; - } - } - - send_update(LauncherUpdate::Focus(id.clone(), prev_id)).await?; + send_update(LauncherUpdate::Focus(app_id.clone(), focused)).await?; if update_title { - send_update(LauncherUpdate::Title(id, window.id, window.name)).await?; + send_update(LauncherUpdate::Title(app_id, window.id, window.title)) + .await?; } } - WindowChange::Title => { - if let Some(item) = items().get_mut(&id) { - item.set_window_name(window.id, window.name.clone()); + ToplevelChange::Title(title) => { + if let Some(item) = items().get_mut(&app_id) { + item.set_window_name(window.id, title.clone()); } - send_update(LauncherUpdate::Title(id, window.id, window.name)).await?; + send_update(LauncherUpdate::Title(app_id, window.id, title)).await?; } - WindowChange::Urgent => { - if let Some(item) = items().get_mut(&id) { - item.set_window_urgent(window.id, window.urgent); - } - - send_update(LauncherUpdate::Urgent(id, window.urgent)).await?; - } - _ => {} + ToplevelChange::Fullscreen(_) => {} } } @@ -259,8 +250,6 @@ impl Module for LauncherModule { // listen to ui events spawn(async move { - let sway = get_client().await; - while let Some(event) = rx.recv().await { trace!("{:?}", event); @@ -287,25 +276,26 @@ impl Module for LauncherModule { }, ); } else { - let selector = { - let items = items.lock().expect("Failed to get lock on items"); + let wl = wayland::get_client().await; + let items = items.lock().expect("Failed to get lock on items"); - match event { - ItemEvent::FocusItem(app_id) => items.get(&app_id).map(|item| { - if item.is_xwayland { - format!("[class={}]", app_id) - } else { - format!("[app_id={}]", app_id) - } - }), - ItemEvent::FocusWindow(con_id) => Some(format!("[con_id={}]", con_id)), - ItemEvent::OpenItem(_) => unreachable!(), - } + let id = match event { + ItemEvent::FocusItem(app_id) => items + .get(&app_id) + .and_then(|item| item.windows.first().map(|win| win.id)), + ItemEvent::FocusWindow(id) => Some(id), + ItemEvent::OpenItem(_) => unreachable!(), }; - if let Some(selector) = selector { - let mut sway = sway.lock().await; - sway.run_command(format!("{} focus", selector)).await?; + if let Some(id) = id { + let toplevels = wl + .toplevels + .read() + .expect("Failed to get read lock on toplevels"); + let seat = wl.seats.first().expect("Failed to get Wayland seat"); + if let Some((_top, handle)) = toplevels.get(&id) { + handle.activate(seat); + }; } } } @@ -391,20 +381,11 @@ impl Module for LauncherModule { menu_state.num_windows -= 1; } } - LauncherUpdate::Focus(new, prev) => { - debug!( - "Changing focus to item with id {} (removing from {:?})", - new, prev - ); + LauncherUpdate::Focus(app_id, focus) => { + debug!("Changing focus to {} on item with id {}", focus, app_id); - if let Some(prev) = prev { - if let Some(button) = buttons.get(&prev) { - button.set_focused(false); - } - } - - if let Some(button) = buttons.get(&new) { - button.set_focused(true); + if let Some(button) = buttons.get(&app_id) { + button.set_focused(focus); } } LauncherUpdate::Title(app_id, _, name) => { @@ -412,17 +393,10 @@ impl Module for LauncherModule { if show_names { if let Some(button) = buttons.get(&app_id) { - button.button.set_label(&name.unwrap_or_default()); + button.button.set_label(&name); } } } - LauncherUpdate::Urgent(app_id, urgent) => { - debug!("Updating urgency for item with id {}: {}", app_id, urgent); - - if let Some(button) = buttons.get(&app_id) { - button.set_urgent(urgent); - } - } LauncherUpdate::Hover(_) => {} }; @@ -444,7 +418,7 @@ impl Module for LauncherModule { ) -> Option { let container = gtk::Box::new(Orientation::Vertical, 0); - let mut buttons = Collection::>::new(); + let mut buttons = Collection::>::new(); { let container = container.clone(); @@ -458,7 +432,7 @@ impl Module for LauncherModule { .into_iter() .map(|win| { let button = Button::builder() - .label(win.name.as_ref().unwrap_or(&String::new())) + .label(&win.name) .height_request(40) .width_request(100) .build(); @@ -484,7 +458,7 @@ impl Module for LauncherModule { LauncherUpdate::AddWindow(app_id, win) => { if let Some(buttons) = buttons.get_mut(&app_id) { let button = Button::builder() - .label(win.name.as_ref().unwrap_or(&String::new())) + .label(&win.name) .height_request(40) .width_request(100) .build(); @@ -512,9 +486,7 @@ impl Module for LauncherModule { LauncherUpdate::Title(app_id, win_id, title) => { if let Some(buttons) = buttons.get_mut(&app_id) { if let Some(button) = buttons.get(&win_id) { - if let Some(title) = title { - button.set_label(&title); - } + button.set_label(&title); } } } diff --git a/src/modules/launcher/open_state.rs b/src/modules/launcher/open_state.rs index cabd7c1..e355059 100644 --- a/src/modules/launcher/open_state.rs +++ b/src/modules/launcher/open_state.rs @@ -1,35 +1,23 @@ -use swayipc_async::Node; +use crate::wayland::ToplevelInfo; /// Open state for a launcher item, or item window. #[derive(Debug, Clone, Eq, PartialEq, Copy)] pub enum OpenState { Closed, - Open { focused: bool, urgent: bool }, + Open { focused: bool }, } impl OpenState { /// Creates from `SwayNode` - pub const fn from_node(node: &Node) -> Self { + pub const fn from_toplevel(toplevel: &ToplevelInfo) -> Self { Self::Open { - focused: node.focused, - urgent: node.urgent, + focused: toplevel.active, } } /// Creates open with focused pub const fn focused(focused: bool) -> Self { - Self::Open { - focused, - urgent: false, - } - } - - /// Creates open with urgent - pub const fn urgent(urgent: bool) -> Self { - Self::Open { - focused: false, - urgent, - } + Self::Open { focused } } /// Checks if open @@ -39,12 +27,7 @@ impl OpenState { /// Checks if open with focus pub const fn is_focused(self) -> bool { - matches!(self, Self::Open { focused: true, .. }) - } - - /// check if open with urgent - pub const fn is_urgent(self) -> bool { - matches!(self, Self::Open { urgent: true, .. }) + matches!(self, Self::Open { focused: true }) } /// Merges states together to produce a single state. @@ -56,7 +39,6 @@ impl OpenState { if merged.is_open() || current.is_open() { Self::Open { focused: merged.is_focused() || current.is_focused(), - urgent: merged.is_urgent() || current.is_urgent(), } } else { Self::Closed diff --git a/src/sway/mod.rs b/src/sway.rs similarity index 67% rename from src/sway/mod.rs rename to src/sway.rs index e1c4f74..073ba47 100644 --- a/src/sway/mod.rs +++ b/src/sway.rs @@ -3,50 +3,37 @@ use color_eyre::Report; use futures_util::StreamExt; use lazy_static::lazy_static; use std::sync::Arc; -use swayipc_async::{Connection, Event, EventType, WindowEvent, WorkspaceEvent}; +use swayipc_async::{Connection, Event, EventType, WorkspaceEvent}; use tokio::spawn; use tokio::sync::broadcast::{channel, Receiver, Sender}; use tokio::sync::Mutex; use tracing::{info, trace}; -pub mod node; - pub struct SwayEventClient { workspace_tx: Sender>, _workspace_rx: Receiver>, - window_tx: Sender>, - _window_rx: Receiver>, } impl SwayEventClient { fn new() -> Self { let (workspace_tx, workspace_rx) = channel(16); - let (window_tx, window_rx) = channel(16); let workspace_tx2 = workspace_tx.clone(); - let window_tx2 = window_tx.clone(); spawn(async move { let workspace_tx = workspace_tx2; - let window_tx = window_tx2; let client = Connection::new().await?; info!("Sway IPC subscription client connected"); - let event_types = [EventType::Window, EventType::Workspace]; + let event_types = [EventType::Workspace]; let mut events = client.subscribe(event_types).await?; while let Some(event) = events.next().await { trace!("event: {:?}", event); - match event? { - Event::Workspace(ev) => { - workspace_tx.send(ev)?; - } - Event::Window(ev) => { - window_tx.send(ev)?; - } - _ => {} + if let Event::Workspace(ev) = event? { + workspace_tx.send(ev)?; }; } @@ -56,8 +43,6 @@ impl SwayEventClient { Self { workspace_tx, _workspace_rx: workspace_rx, - window_tx, - _window_rx: window_rx, } } @@ -65,11 +50,6 @@ impl SwayEventClient { pub fn subscribe_workspace(&self) -> Receiver> { self.workspace_tx.subscribe() } - - /// Gets an event receiver for window events - pub fn subscribe_window(&self) -> Receiver> { - self.window_tx.subscribe() - } } lazy_static! { diff --git a/src/sway/node.rs b/src/sway/node.rs deleted file mode 100644 index 6796170..0000000 --- a/src/sway/node.rs +++ /dev/null @@ -1,50 +0,0 @@ -use color_eyre::Result; -use swayipc_async::{Connection, Node, NodeType, ShellType}; - -pub fn get_node_id(node: &Node) -> &str { - node.app_id.as_ref().map_or_else( - || { - node.window_properties - .as_ref() - .expect("Cannot find node window properties") - .class - .as_ref() - .expect("Cannot find node name") - }, - |app_id| app_id, - ) -} - -/// Checks whether this application -/// is running under xwayland. -pub fn is_node_xwayland(node: &Node) -> bool { - node.shell == Some(ShellType::Xwayland) -} - -/// Recursively checks the provided node for any child application nodes. -/// Returns a list of any found application nodes. -fn check_node(node: Node, window_nodes: &mut Vec) { - if node.name.is_some() - && (node.node_type == NodeType::Con || node.node_type == NodeType::FloatingCon) - { - window_nodes.push(node); - } else { - node.nodes.into_iter().for_each(|node| { - check_node(node, window_nodes); - }); - - node.floating_nodes.into_iter().for_each(|node| { - check_node(node, window_nodes); - }); - } -} - -/// Gets a flat vector of all currently open windows. -pub async fn get_open_windows(client: &mut Connection) -> Result> { - let root_node = client.get_tree().await?; - - let mut window_nodes = vec![]; - check_node(root_node, &mut window_nodes); - - Ok(window_nodes) -} diff --git a/src/wayland/client.rs b/src/wayland/client.rs new file mode 100644 index 0000000..9763d9e --- /dev/null +++ b/src/wayland/client.rs @@ -0,0 +1,125 @@ +use super::{Env, ToplevelHandler}; +use crate::collection::Collection; +use crate::wayland::toplevel::{ToplevelEvent, ToplevelInfo}; +use crate::wayland::toplevel_manager::listen_for_toplevels; +use crate::wayland::ToplevelChange; +use smithay_client_toolkit::environment::Environment; +use smithay_client_toolkit::output::{with_output_info, OutputInfo}; +use smithay_client_toolkit::reexports::calloop; +use smithay_client_toolkit::{new_default_environment, WaylandSource}; +use std::sync::{Arc, RwLock}; +use std::time::Duration; +use tokio::sync::{broadcast, oneshot}; +use tokio::task::spawn_blocking; +use tracing::trace; +use wayland_client::protocol::wl_seat::WlSeat; +use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{ + zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, + zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1, +}; + +pub struct WaylandClient { + pub outputs: Vec, + pub seats: Vec, + pub toplevels: Arc>>, + toplevel_tx: broadcast::Sender, + _toplevel_rx: broadcast::Receiver, +} + +impl WaylandClient { + pub(super) async fn new() -> Self { + let (output_tx, output_rx) = oneshot::channel(); + let (seat_tx, seat_rx) = oneshot::channel(); + + let (toplevel_tx, toplevel_rx) = broadcast::channel(32); + + let toplevel_tx2 = toplevel_tx.clone(); + + let toplevels = Arc::new(RwLock::new(Collection::new())); + let toplevels2 = toplevels.clone(); + + // `queue` is not send so we need to handle everything inside the task + spawn_blocking(move || { + let (env, _display, queue) = + new_default_environment!(Env, fields = [toplevel: ToplevelHandler::init()]) + .expect("Failed to connect to Wayland compositor"); + + let outputs = Self::get_outputs(&env); + output_tx + .send(outputs) + .expect("Failed to send outputs out of task"); + + let seats = env.get_all_seats(); + seat_tx + .send( + seats + .into_iter() + .map(|seat| seat.detach()) + .collect::>(), + ) + .expect("Failed to send seats out of task"); + + let _toplevel_manager = env.require_global::(); + + let _listener = listen_for_toplevels(env, move |handle, event, _ddata| { + trace!("Received toplevel event: {:?}", event); + + if event.change == ToplevelChange::Close { + toplevels2 + .write() + .expect("Failed to get write lock on toplevels") + .remove(&event.toplevel.id); + } else { + toplevels2 + .write() + .expect("Failed to get write lock on toplevels") + .insert(event.toplevel.id, (event.toplevel.clone(), handle)); + } + + toplevel_tx2 + .send(event) + .expect("Failed to send toplevel event"); + }); + + let mut event_loop = + calloop::EventLoop::<()>::try_new().expect("Failed to create new event loop"); + WaylandSource::new(queue) + .quick_insert(event_loop.handle()) + .expect("Failed to insert event loop into wayland event queue"); + + loop { + // TODO: Avoid need for duration here - can we force some event when sending requests? + event_loop + .dispatch(Duration::from_millis(50), &mut ()) + .expect("Failed to dispatch pending wayland events"); + } + }); + + let outputs = output_rx + .await + .expect("Failed to receive outputs from task"); + + let seats = seat_rx.await.expect("Failed to receive seats from task"); + + Self { + outputs, + seats, + toplevels, + toplevel_tx, + _toplevel_rx: toplevel_rx, + } + } + + pub fn subscribe_toplevels(&self) -> broadcast::Receiver { + self.toplevel_tx.subscribe() + } + + fn get_outputs(env: &Environment) -> Vec { + let outputs = env.get_all_outputs(); + + outputs + .iter() + .filter_map(|output| with_output_info(output, Clone::clone)) + .collect() + } +} diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs new file mode 100644 index 0000000..fdd0357 --- /dev/null +++ b/src/wayland/mod.rs @@ -0,0 +1,54 @@ +mod client; +mod toplevel; +mod toplevel_manager; + +extern crate smithay_client_toolkit as sctk; + +use self::toplevel_manager::ToplevelHandler; +pub use crate::wayland::toplevel::{ToplevelChange, ToplevelEvent, ToplevelInfo}; +use crate::wayland::toplevel_manager::{ToplevelHandling, ToplevelStatusListener}; +use async_once::AsyncOnce; +use lazy_static::lazy_static; +use wayland_client::{Attached, DispatchData, Interface}; +use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{ + zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, + zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1, +}; + +pub use client::WaylandClient; + +/// A utility for lazy-loading globals. +/// Taken from `smithay_client_toolkit` where it's not exposed +#[derive(Debug)] +enum LazyGlobal { + Unknown, + Seen { id: u32, version: u32 }, + Bound(Attached), +} + +sctk::default_environment!(Env, + fields = [ + toplevel: ToplevelHandler + ], + singles = [ + ZwlrForeignToplevelManagerV1 => toplevel + ], +); + +impl ToplevelHandling for Env { + fn listen(&mut self, f: F) -> ToplevelStatusListener + where + F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static, + { + self.toplevel.listen(f) + } +} + +lazy_static! { + static ref CLIENT: AsyncOnce = + AsyncOnce::new(async { WaylandClient::new().await }); +} + +pub async fn get_client() -> &'static WaylandClient { + CLIENT.get().await +} diff --git a/src/wayland/toplevel.rs b/src/wayland/toplevel.rs new file mode 100644 index 0000000..d02d534 --- /dev/null +++ b/src/wayland/toplevel.rs @@ -0,0 +1,143 @@ +use std::collections::HashSet; +use std::sync::{Arc, RwLock}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use wayland_client::{DispatchData, Main}; +use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{Event, ZwlrForeignToplevelHandleV1}; + +const STATE_ACTIVE: u32 = 2; +const STATE_FULLSCREEN: u32 = 3; + +static COUNTER: AtomicUsize = AtomicUsize::new(1); +fn get_id() -> usize { + COUNTER.fetch_add(1, Ordering::Relaxed) +} + +#[derive(Debug, Clone, Default)] +pub struct ToplevelInfo { + pub id: usize, + pub app_id: String, + pub title: String, + pub active: bool, + pub fullscreen: bool, + + ready: bool, +} + +impl ToplevelInfo { + fn new() -> Self { + let id = get_id(); + Self { + id, + ..Default::default() + } + } +} + +pub struct Toplevel; + +#[derive(Debug, Clone)] +pub struct ToplevelEvent { + pub toplevel: ToplevelInfo, + pub change: ToplevelChange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ToplevelChange { + New, + Close, + Title(String), + Focus(bool), + Fullscreen(bool), +} + +fn toplevel_implem(event: Event, info: &mut ToplevelInfo, implem: &mut F, ddata: DispatchData) +where + F: FnMut(ToplevelEvent, DispatchData), +{ + let change = match event { + Event::AppId { app_id } => { + info.app_id = app_id; + None + } + Event::Title { title } => { + info.title = title.clone(); + + if info.ready { + Some(ToplevelChange::Title(title)) + } else { + None + } + } + Event::State { state } => { + // state is received as a `Vec` where every 4 bytes make up a `u32` + // the u32 then represents a value in the `State` enum. + assert_eq!(state.len() % 4, 0); + + let state = (0..state.len() / 4) + .map(|i| { + let slice: [u8; 4] = state[i * 4..i * 4 + 4] + .try_into() + .expect("Received invalid state length"); + u32::from_le_bytes(slice) + }) + .collect::>(); + + let new_active = state.contains(&STATE_ACTIVE); + let new_fullscreen = state.contains(&STATE_FULLSCREEN); + + let change = if info.ready && new_active != info.active { + Some(ToplevelChange::Focus(new_active)) + } else if info.ready && new_fullscreen != info.fullscreen { + Some(ToplevelChange::Fullscreen(new_fullscreen)) + } else { + None + }; + + info.active = new_active; + info.fullscreen = new_fullscreen; + + change + } + Event::Closed => Some(ToplevelChange::Close), + Event::OutputEnter { output: _ } => None, + Event::OutputLeave { output: _ } => None, + Event::Parent { parent: _ } => None, + Event::Done => { + assert_ne!(info.app_id, ""); + if info.ready { + None + } else { + info.ready = true; + Some(ToplevelChange::New) + } + } + _ => unreachable!(), + }; + + if let Some(change) = change { + let event = ToplevelEvent { + change, + toplevel: info.clone(), + }; + + implem(event, ddata); + } +} + +impl Toplevel { + pub fn init(handle: &Main, mut callback: F) -> Self + where + F: FnMut(ToplevelEvent, DispatchData) + 'static, + { + let inner = Arc::new(RwLock::new(ToplevelInfo::new())); + + handle.quick_assign(move |_handle, event, ddata| { + let mut inner = inner + .write() + .expect("Failed to get write lock on toplevel inner state"); + toplevel_implem(event, &mut inner, &mut callback, ddata); + }); + + Self + } +} diff --git a/src/wayland/toplevel_manager.rs b/src/wayland/toplevel_manager.rs new file mode 100644 index 0000000..97d3a39 --- /dev/null +++ b/src/wayland/toplevel_manager.rs @@ -0,0 +1,164 @@ +use crate::wayland::toplevel::{Toplevel, ToplevelEvent}; +use crate::wayland::LazyGlobal; +use smithay_client_toolkit::environment::{Environment, GlobalHandler}; +use std::cell::RefCell; +use std::rc; +use std::rc::Rc; +use tracing::warn; +use wayland_client::protocol::wl_registry::WlRegistry; +use wayland_client::{Attached, DispatchData}; +use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{ + zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, + zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1}, +}; + +struct ToplevelHandlerInner { + manager: LazyGlobal, + registry: Option>, + toplevels: Vec, +} + +impl ToplevelHandlerInner { + const fn new() -> Self { + let toplevels = vec![]; + + Self { + registry: None, + manager: LazyGlobal::Unknown, + toplevels, + } + } +} + +pub struct ToplevelHandler { + inner: Rc>, + status_listeners: Rc>>>>, +} + +impl ToplevelHandler { + pub fn init() -> Self { + let inner = Rc::new(RefCell::new(ToplevelHandlerInner::new())); + + Self { + inner, + status_listeners: Rc::new(RefCell::new(Vec::new())), + } + } +} + +impl GlobalHandler for ToplevelHandler { + fn created( + &mut self, + registry: Attached, + id: u32, + version: u32, + _ddata: DispatchData, + ) { + let mut inner = RefCell::borrow_mut(&self.inner); + if inner.registry.is_none() { + inner.registry = Some(registry); + } + if let LazyGlobal::Unknown = inner.manager { + inner.manager = LazyGlobal::Seen { id, version } + } else { + warn!( + "Compositor advertised zwlr_foreign_toplevel_manager_v1 multiple times, ignoring." + ); + } + } + + fn get(&self) -> Option> { + let mut inner = RefCell::borrow_mut(&self.inner); + + match inner.manager { + LazyGlobal::Bound(ref mgr) => Some(mgr.clone()), + LazyGlobal::Unknown => None, + LazyGlobal::Seen { id, version } => { + let registry = inner.registry.as_ref().expect("Failed to get registry"); + // current max protocol version = 3 + let version = std::cmp::min(version, 3); + let manager = registry.bind::(version, id); + + { + let inner = self.inner.clone(); + let status_listeners = self.status_listeners.clone(); + + manager.quick_assign(move |_, event, _ddata| { + let mut inner = RefCell::borrow_mut(&inner); + let status_listeners = status_listeners.clone(); + + match event { + zwlr_foreign_toplevel_manager_v1::Event::Toplevel { + toplevel: handle, + } => { + let toplevel = + Toplevel::init(&handle.clone(), move |event, ddata| { + notify_status_listeners( + &handle, + &event, + ddata, + &status_listeners, + ); + }); + + inner.toplevels.push(toplevel); + } + zwlr_foreign_toplevel_manager_v1::Event::Finished => {} + _ => unreachable!(), + } + }); + } + + inner.manager = LazyGlobal::Bound((*manager).clone()); + Some((*manager).clone()) + } + } + } +} + +type ToplevelStatusCallback = + dyn FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static; + +/// Notifies the callbacks of an event on the toplevel +fn notify_status_listeners( + toplevel: &ZwlrForeignToplevelHandleV1, + event: &ToplevelEvent, + mut ddata: DispatchData, + listeners: &RefCell>>>, +) { + listeners.borrow_mut().retain(|lst| { + rc::Weak::upgrade(lst).map_or(false, |cb| { + (cb.borrow_mut())(toplevel.clone(), event.clone(), ddata.reborrow()); + true + }) + }); +} + +pub struct ToplevelStatusListener { + _cb: Rc>, +} + +pub trait ToplevelHandling { + fn listen(&mut self, f: F) -> ToplevelStatusListener + where + F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static; +} + +impl ToplevelHandling for ToplevelHandler { + fn listen(&mut self, f: F) -> ToplevelStatusListener + where + F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static, + { + let rc = Rc::new(RefCell::new(f)) as Rc<_>; + self.status_listeners.borrow_mut().push(Rc::downgrade(&rc)); + ToplevelStatusListener { _cb: rc } + } +} + +pub fn listen_for_toplevels(env: Environment, f: F) -> ToplevelStatusListener +where + E: ToplevelHandling, + F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static, +{ + env.with_inner(move |inner| ToplevelHandling::listen(inner, f)) +}