1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-02 03:01:04 +02:00

Merge pull request #24 from JakeStanger/feat/wayland-protocols

Implement Wayland protocol support
This commit is contained in:
Jake Stanger 2022-10-14 22:24:57 +01:00 committed by GitHub
commit 668fe4a308
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 873 additions and 368 deletions

197
Cargo.lock generated
View file

@ -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",

View file

@ -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"
@ -36,3 +36,6 @@ 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"
wayland-client = "0.29.5"
wayland-protocols = { version = "0.29.5", features=["unstable_protocols", "client"] }
smithay-client-toolkit = "0.16.0"

View file

@ -1,6 +1,6 @@
# 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.
@ -50,7 +50,8 @@ A full configuration guide can be found [here](https://github.com/JakeStanger/ir
## 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).
@ -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

View file

@ -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);

View file

@ -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<gtk::Box> for FocusedModule {
_rx: Receiver<Self::ReceiveMessage>,
) -> 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");
}
}

View file

@ -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<i64, Window>,
pub name: Option<String>,
pub is_xwayland: bool,
pub windows: Collection<usize, Window>,
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<String>) {
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<Node> 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<ToplevelInfo> 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<Node> for Item {
open_state,
windows,
name,
is_xwayland,
}
}
}
#[derive(Clone, Debug)]
pub struct Window {
pub id: i64,
pub name: Option<String>,
pub id: usize,
pub name: String,
pub open_state: OpenState,
}
impl From<Node> for Window {
fn from(node: Node) -> Self {
let open_state = OpenState::from_node(&node);
impl From<ToplevelInfo> 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();

View file

@ -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<String>),
/// Focuses first `app_id`, unfocuses second `app_id` (if present)
Focus(String, Option<String>),
/// 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<gtk::Box> 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<gtk::Box> 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<gtk::Box> 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<gtk::Box> 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<gtk::Box> 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<gtk::Box> 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<gtk::Box> 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<gtk::Box> for LauncherModule {
) -> Option<gtk::Box> {
let container = gtk::Box::new(Orientation::Vertical, 0);
let mut buttons = Collection::<String, Collection<i64, Button>>::new();
let mut buttons = Collection::<String, Collection<usize, Button>>::new();
{
let container = container.clone();
@ -458,7 +432,7 @@ impl Module<gtk::Box> 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<gtk::Box> 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<gtk::Box> 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);
}
}
}

View file

@ -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

View file

@ -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<Box<WorkspaceEvent>>,
_workspace_rx: Receiver<Box<WorkspaceEvent>>,
window_tx: Sender<Box<WindowEvent>>,
_window_rx: Receiver<Box<WindowEvent>>,
}
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<Box<WorkspaceEvent>> {
self.workspace_tx.subscribe()
}
/// Gets an event receiver for window events
pub fn subscribe_window(&self) -> Receiver<Box<WindowEvent>> {
self.window_tx.subscribe()
}
}
lazy_static! {

View file

@ -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<Node>) {
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<Vec<Node>> {
let root_node = client.get_tree().await?;
let mut window_nodes = vec![];
check_node(root_node, &mut window_nodes);
Ok(window_nodes)
}

125
src/wayland/client.rs Normal file
View file

@ -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<OutputInfo>,
pub seats: Vec<WlSeat>,
pub toplevels: Arc<RwLock<Collection<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
toplevel_tx: broadcast::Sender<ToplevelEvent>,
_toplevel_rx: broadcast::Receiver<ToplevelEvent>,
}
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::<Vec<WlSeat>>(),
)
.expect("Failed to send seats out of task");
let _toplevel_manager = env.require_global::<ZwlrForeignToplevelManagerV1>();
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<ToplevelEvent> {
self.toplevel_tx.subscribe()
}
fn get_outputs(env: &Environment<Env>) -> Vec<OutputInfo> {
let outputs = env.get_all_outputs();
outputs
.iter()
.filter_map(|output| with_output_info(output, Clone::clone))
.collect()
}
}

54
src/wayland/mod.rs Normal file
View file

@ -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<I: Interface> {
Unknown,
Seen { id: u32, version: u32 },
Bound(Attached<I>),
}
sctk::default_environment!(Env,
fields = [
toplevel: ToplevelHandler
],
singles = [
ZwlrForeignToplevelManagerV1 => toplevel
],
);
impl ToplevelHandling for Env {
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
where
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
{
self.toplevel.listen(f)
}
}
lazy_static! {
static ref CLIENT: AsyncOnce<WaylandClient> =
AsyncOnce::new(async { WaylandClient::new().await });
}
pub async fn get_client() -> &'static WaylandClient {
CLIENT.get().await
}

143
src/wayland/toplevel.rs Normal file
View file

@ -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<F>(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<u8>` 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::<HashSet<_>>();
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<F>(handle: &Main<ZwlrForeignToplevelHandleV1>, 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
}
}

View file

@ -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<ZwlrForeignToplevelManagerV1>,
registry: Option<Attached<WlRegistry>>,
toplevels: Vec<Toplevel>,
}
impl ToplevelHandlerInner {
const fn new() -> Self {
let toplevels = vec![];
Self {
registry: None,
manager: LazyGlobal::Unknown,
toplevels,
}
}
}
pub struct ToplevelHandler {
inner: Rc<RefCell<ToplevelHandlerInner>>,
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<ToplevelStatusCallback>>>>>,
}
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<ZwlrForeignToplevelManagerV1> for ToplevelHandler {
fn created(
&mut self,
registry: Attached<WlRegistry>,
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<Attached<ZwlrForeignToplevelManagerV1>> {
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::<ZwlrForeignToplevelManagerV1>(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<Vec<rc::Weak<RefCell<ToplevelStatusCallback>>>>,
) {
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<RefCell<ToplevelStatusCallback>>,
}
pub trait ToplevelHandling {
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
where
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static;
}
impl ToplevelHandling for ToplevelHandler {
fn listen<F>(&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<E, F>(env: Environment<E>, f: F) -> ToplevelStatusListener
where
E: ToplevelHandling,
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
{
env.with_inner(move |inner| ToplevelHandling::listen(inner, f))
}